Skip to content

Commit fd5affd

Browse files
committed
Live linter with red squiggles, unified AST pipeline, macOS install wizard fixes
- CppLinter: g++ -fsyntax-only background lint with 800ms debounce, PCH caching, multi-tab line mapping - CppProblem: Problem interface impl for squiggle rendering via setProblemList() - CppBuild: prepareCode() single source of truth for pre-AST transforms, runAstPipeline() shared between IDE and CLI, isFileScopePreprocessorLine() routing helper, #if __has_include block extraction/preservation - CppCLI: now uses prepareCode() + runAstPipeline(), no more reflection/Unsafe - InstallWizard: macOS now checks headers not just dylibs, triggers even when g++ present but GLFW/GLEW missing, auto-Homebrew install, Apple Silicon PATH fix - CppEditor: document listener wires live lint on every keystroke
1 parent fcbfd4e commit fd5affd

7 files changed

Lines changed: 668 additions & 191 deletions

File tree

mode/CppMode.jar

16.4 KB
Binary file not shown.

src/java/CppBuild.java

Lines changed: 235 additions & 105 deletions
Large diffs are not rendered by default.

src/java/CppCLI.java

Lines changed: 104 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
package processing.mode.cpp;
22

33
import java.io.*;
4-
import java.lang.reflect.*;
54
import java.util.*;
65
import java.nio.charset.StandardCharsets;
76

@@ -11,11 +10,8 @@
1110
*/
1211
public class CppCLI {
1312

14-
private static final Set<String> LIFECYCLE = Set.of(
15-
"setup","draw","mousePressed","mouseReleased","mouseClicked",
16-
"mouseMoved","mouseDragged","mouseWheel",
17-
"keyPressed","keyReleased","keyTyped","settings"
18-
);
13+
// Single source of truth for lifecycle method names -- defined in CppBuild.
14+
private static final Set<String> LIFECYCLE = CppBuild.LIFECYCLE_METHOD_NAMES;
1915

2016
public static void main(String[] args) throws Exception {
2117
String input = new String(System.in.readAllBytes(), StandardCharsets.UTF_8);
@@ -28,95 +24,115 @@ public static void main(String[] args) throws Exception {
2824
}
2925

3026
private static String translate(String input) throws Exception {
31-
CppBuild b = allocate();
32-
String code = input;
33-
code = invoke(b, "sanitize", code);
34-
code = invoke(b, "removeUserIncludes", code);
35-
code = code.replaceAll("(\\bfinal_suspend\\s*\\([^)]*\\))(\\s*\\{)", "$1 noexcept$2");
36-
code = code.replaceAll("(\\binitial_suspend\\s*\\([^)]*\\))(\\s*\\{)", "$1 noexcept$2");
37-
code = code.replaceAll("\\btranslate\\s*\\(\\s*0+\\.?0*f?\\s*,\\s*0+\\.?0*f?\\s*\\)\\s*;", "");
38-
code = code.replaceAll("\\btranslate\\s*\\(\\s*0+\\.?0*f?\\s*,\\s*0+\\.?0*f?\\s*,\\s*0+\\.?0*f?\\s*\\)\\s*;", "");
39-
code = invoke(b, "stripRawStringLiterals", code);
40-
code = code.replaceAll("(?<=[0-9a-fA-FxXbB])'(?=[0-9a-fA-F])", "");
41-
code = invoke(b, "javaToC", code);
42-
code = invoke(b, "stripNamespaceProcessing", code);
43-
code = invoke(b, "preprocessMacros", code);
27+
// Delegate all pre-AST transforms to prepareCode() -- single source of truth.
28+
CppBuild.PreparedCode prepared = CppBuild.prepareCode(input);
29+
String code = prepared.code;
4430

4531
boolean hasSetup = code.contains("void setup(");
4632
boolean hasDraw = code.contains("void draw(");
4733

34+
// __has_include blocks go to file scope directly -- never into the parser.
35+
StringBuilder hasIncludePreNs = new StringBuilder();
36+
for (String block : prepared.hasIncludeBlocks.values()) {
37+
hasIncludePreNs.append(block);
38+
}
39+
4840
StringBuilder out = new StringBuilder();
4941
StringBuilder preNs = new StringBuilder();
5042
appendHeader(out);
5143

5244
if (hasSetup || hasDraw) {
5345
String result = runPipeline(code, preNs);
46+
System.out.print(hasIncludePreNs);
5447
System.out.print(preNs);
5548
System.out.print(out);
5649
System.out.print(result);
57-
System.out.print("\nint main() { Processing::_PSketch sketch; sketch.run(); return 0; }\n");
50+
if (!code.contains("int main")) {
51+
System.out.print("\nint main() { Processing::_PSketch sketch; sketch.run(); return 0; }\n");
52+
}
5853
return "";
54+
} else if (code.contains("int main")) {
55+
return hasIncludePreNs.toString() + preNs.toString() + out.toString() + code;
5956
} else {
6057
out.append("\nnamespace Processing {\n\n");
6158
out.append(code);
6259
out.append("\n} // namespace Processing\n\n");
6360
out.append("int main() { Processing::_PSketch sketch; sketch.run(); return 0; }\n");
64-
return out.toString();
61+
return hasIncludePreNs.toString() + preNs.toString() + out.toString();
6562
}
6663
}
6764

6865
private static String runPipeline(String code, StringBuilder preNs) throws Exception {
6966
CompilationUnit cu = Parser.parse(code);
70-
EnumScopeExtractor.Result enumResult = EnumScopeExtractor.extract(cu.items());
71-
List<TopLevelItem> afterLifecycle = LifecycleRewriter.rewrite(enumResult.rest, LIFECYCLE);
72-
ClassHoister.Result classResult = ClassHoister.hoist(afterLifecycle);
73-
List<PSketchInjector.Result> injected = PSketchInjector.injectAll(classResult.hoistedClasses);
74-
List<TypeDef> finalClasses = injected.stream().map(PSketchInjector.Result::typeDef).toList();
75-
ArrayHoister.Result arrayResult = ArrayHoister.hoist(classResult.rest);
76-
DependencyHoister.Result depResult = DependencyHoister.hoist(arrayResult.rest, finalClasses, LIFECYCLE);
77-
List<FunctionDecl> forwardDecls = ForwardDeclGenerator.generate(depResult.hoistedFunctions);
67+
CppBuild.AstPipelineResult pipe = CppBuild.runAstPipeline(cu);
7868

7969
StringBuilder sb = new StringBuilder();
8070
sb.append("\nnamespace Processing {\n\n");
8171

82-
List<TopLevelItem> filteredRest = new ArrayList<>();
83-
for (TopLevelItem item : depResult.rest) {
72+
// Route preprocessor directives and namespace decls to file scope or namespace body.
73+
List<TopLevelItem> effectiveRest = new ArrayList<>();
74+
for (TopLevelItem item : pipe.filteredRest) {
8475
if (item instanceof PreprocessorLine pl) {
85-
if (pl.rawText().startsWith("#include")) preNs.append(CodeGen.generateNode(item, 0));
76+
if (CppBuild.isFileScopePreprocessorLine(pl)) preNs.append(CodeGen.generateNode(item, 0));
8677
else sb.append(CodeGen.generateNode(item, 0));
8778
continue;
8879
}
89-
if (item instanceof NamespaceDecl || item instanceof UsingNamespaceDecl) {
80+
if (item instanceof NamespaceDecl nd) {
81+
if (nd.name().equals("std")) preNs.append(CodeGen.generateNode(nd, 0));
82+
else sb.append(CodeGen.generateNode(nd, 0));
83+
continue;
84+
}
85+
if (item instanceof UsingNamespaceDecl) {
9086
sb.append(CodeGen.generateNode(item, 0)); continue;
9187
}
92-
filteredRest.add(item);
88+
effectiveRest.add(item);
9389
}
9490

95-
for (FunctionDecl fd : forwardDecls) sb.append(CodeGen.generateNode(fd, 0));
96-
for (TopLevelItem e : enumResult.enums) sb.append(CodeGen.generateNode(e, 0));
91+
// Emit in same order as writeSketchImpl.
92+
for (TopLevelItem e : pipe.enumResult.enums) sb.append(CodeGen.generateNode(e, 0));
93+
for (FunctionDecl fd : pipe.forwardDecls) sb.append(CodeGen.generateNode(fd, 0));
94+
for (var v : pipe.arrayResult.hoistedSizingConstants) sb.append(CodeGen.generateNode(v, 0));
95+
java.util.Set<String> autoHoistedNames = new java.util.HashSet<>();
96+
for (TopLevelItem av : pipe.autoHoisted) if (av instanceof VariableDecl avd) autoHoistedNames.add(avd.name());
97+
for (var v : pipe.depResult.hoistedVariables) {
98+
if (!autoHoistedNames.contains(v.name())) sb.append(CodeGen.generateNode(v, 0));
99+
}
100+
for (TypeDef td : pipe.finalClasses) sb.append(CodeGen.generateNode(td, 0));
101+
for (TopLevelItem av : pipe.autoHoisted) sb.append(CodeGen.generateNode(av, 0));
102+
for (var v : pipe.arrayResult.hoistedArrays) sb.append(CodeGen.generateNode(v, 0));
103+
for (FunctionDecl fd : pipe.depResult.hoistedFunctions) sb.append(CodeGen.generateNode(fd, 0));
97104

98-
// Collect lifecycle bodies from hoisted functions
105+
// Collect lifecycle bodies; remaining items go into _PSketch.
99106
List<FunctionDecl> lifecycleBodies = new ArrayList<>();
100-
for (TopLevelItem item : depResult.hoistedFunctions) {
101-
if (item instanceof FunctionDecl fd && LIFECYCLE.contains(fd.name()) && fd.body() != null)
107+
List<TopLevelItem> sketchMembers = new ArrayList<>();
108+
for (TopLevelItem item : effectiveRest) {
109+
if (item instanceof FunctionDecl fd && fd.body() != null && LIFECYCLE.contains(fd.name())) {
102110
lifecycleBodies.add(fd);
103-
else sb.append(CodeGen.generateNode(item, 0));
111+
} else {
112+
sketchMembers.add(item);
113+
}
104114
}
105-
for (TopLevelItem item : depResult.hoistedVariables) sb.append(CodeGen.generateNode(item, 0));
106-
for (TopLevelItem item : arrayResult.hoistedArrays) sb.append(CodeGen.generateNode(item, 0));
107-
for (TypeDef td : finalClasses) sb.append(CodeGen.generateNode(td, 0));
108-
109-
// Also collect from filteredRest
110-
List<TopLevelItem> nonLifecycle = new ArrayList<>();
111-
for (TopLevelItem item : filteredRest) {
112-
if (item instanceof FunctionDecl fd && LIFECYCLE.contains(fd.name()) && fd.body() != null)
113-
lifecycleBodies.add(fd);
114-
else nonLifecycle.add(item);
115+
116+
// Strip redundant forward decls (same as writeSketchImpl).
117+
java.util.Set<String> definedFns = new java.util.HashSet<>();
118+
for (TopLevelItem item : sketchMembers)
119+
if (item instanceof FunctionDecl fd && fd.body() != null)
120+
definedFns.add(fd.name() + "/" + fd.params().size());
121+
sketchMembers.removeIf(item -> item instanceof FunctionDecl fd
122+
&& fd.body() == null && !fd.isPureVirtual()
123+
&& definedFns.contains(fd.name() + "/" + fd.params().size()));
124+
125+
// Hoist TopLevelStatements to namespace scope.
126+
List<TopLevelItem> nsHoisted = new ArrayList<>(), realMembers = new ArrayList<>();
127+
for (TopLevelItem item : sketchMembers) {
128+
if (item instanceof TopLevelStatement) nsHoisted.add(item);
129+
else realMembers.add(item);
115130
}
116-
for (TopLevelItem item : nonLifecycle) sb.append(CodeGen.generateNode(item, 0));
131+
for (TopLevelItem item : nsHoisted) sb.append(CodeGen.generateNode(item, 0));
117132

118-
// Build _PSketch with inline lifecycle bodies
133+
// Build _PSketch with lifecycle methods inlined.
119134
sb.append("class _PSketch : public PApplet {\npublic:\n");
135+
for (TopLevelItem item : realMembers) sb.append(CodeGen.generateNode(item, 1));
120136
Map<String,FunctionDecl> bodyMap = new LinkedHashMap<>();
121137
for (FunctionDecl fd : lifecycleBodies) bodyMap.put(fd.name(), fd);
122138
for (String name : LIFECYCLE) {
@@ -133,30 +149,46 @@ private static String runPipeline(String code, StringBuilder preNs) throws Excep
133149
return sb.toString();
134150
}
135151

136-
private static void appendHeader(StringBuilder out) {
137-
out.append("#include \"Processing.h\"\n");
138-
out.append("using namespace std;\n");
139-
}
152+
/**
153+
* Produces the full C++ source string for a sketch, including #line directives,
154+
* for use by CppLinter (g++ -fsyntax-only).
155+
*/
156+
public static String translateForLint(String sketchCode) throws Exception {
157+
CppBuild.PreparedCode prepared = CppBuild.prepareCode(sketchCode);
158+
String code = prepared.code;
159+
boolean hasSetup = code.contains("void setup(");
160+
boolean hasDraw = code.contains("void draw(");
140161

141-
private static String invoke(CppBuild b, String method, String code) throws Exception {
142-
Method m = getMethod(CppBuild.class, method, String.class);
143-
m.setAccessible(true);
144-
return (String) m.invoke(b, code);
145-
}
162+
StringBuilder hasIncludePreNs = new StringBuilder();
163+
for (String block : prepared.hasIncludeBlocks.values())
164+
hasIncludePreNs.append(block);
146165

147-
private static Method getMethod(Class<?> cls, String name, Class<?>... params) throws NoSuchMethodException {
148-
while (cls != null) {
149-
try { return cls.getDeclaredMethod(name, params); }
150-
catch (NoSuchMethodException e) { cls = cls.getSuperclass(); }
166+
StringBuilder preNs = new StringBuilder();
167+
StringBuilder result = new StringBuilder();
168+
169+
if (hasSetup || hasDraw) {
170+
String body = runPipeline(code, preNs);
171+
result.append(hasIncludePreNs);
172+
result.append(preNs);
173+
result.append("#include \"Processing.h\"\n");
174+
result.append("using namespace std;\n");
175+
result.append(body);
176+
} else {
177+
result.append(hasIncludePreNs);
178+
result.append(preNs);
179+
result.append("#include \"Processing.h\"\n");
180+
result.append("using namespace std;\n");
181+
result.append("\nnamespace Processing {\n\n");
182+
result.append(code);
183+
result.append("\n} // namespace Processing\n");
151184
}
152-
throw new NoSuchMethodException(name);
185+
return result.toString();
153186
}
154187

155-
private static CppBuild allocate() throws Exception {
156-
Class<?> unsafeClass = Class.forName("sun.misc.Unsafe");
157-
Field f = unsafeClass.getDeclaredField("theUnsafe");
158-
f.setAccessible(true);
159-
sun.misc.Unsafe unsafe = (sun.misc.Unsafe) f.get(null);
160-
return (CppBuild) unsafe.allocateInstance(CppBuild.class);
188+
private static void appendHeader(StringBuilder out) {
189+
out.append("#include \"Processing.h\"\n");
190+
out.append("using namespace std;\n");
161191
}
192+
193+
162194
}

src/java/CppEditor.java

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,17 +9,41 @@
99
import processing.app.*;
1010
import processing.app.syntax.*;
1111
import processing.app.ui.*;
12+
import java.util.List;
1213

1314

1415
public class CppEditor extends Editor {
1516

1617
private CppRunner currentRunner;
1718
private final Set<Integer> errorLines = new HashSet<>();
1819
private ErrorOverlay overlay;
20+
private CppLinter linter;
1921

2022
public CppEditor(Base base, String path,
2123
EditorState state, CppMode mode) throws EditorException {
2224
super(base, path, state, mode);
25+
linter = new CppLinter(mode, this::setProblemList);
26+
// Hook into text area changes for live linting
27+
javax.swing.SwingUtilities.invokeLater(() -> {
28+
textarea.getDocument().addDocumentListener(new javax.swing.event.DocumentListener() {
29+
public void insertUpdate(javax.swing.event.DocumentEvent e) { scheduleLint(); }
30+
public void removeUpdate(javax.swing.event.DocumentEvent e) { scheduleLint(); }
31+
public void changedUpdate(javax.swing.event.DocumentEvent e) { scheduleLint(); }
32+
});
33+
scheduleLint(); // initial lint on open
34+
});
35+
}
36+
37+
private void scheduleLint() {
38+
if (linter == null) return;
39+
// Pass sketch for tab metadata, plus live text for the current tab
40+
linter.scheduleCheck(sketch, sketch.getCurrentCodeIndex(), getText());
41+
}
42+
43+
@Override
44+
public void dispose() {
45+
if (linter != null) linter.shutdown();
46+
super.dispose();
2347
}
2448

2549
// ── Bug report link (placed in the footer's tab bar, just left of the

0 commit comments

Comments
 (0)