JScratch
Loading...
Searching...
No Matches
CodeWorkspace.java
Go to the documentation of this file.
1package com.jscratch.ui;
2
3import com.jscratch.Project;
4import com.jscratch.Project.SpriteData;
5import com.jscratch.compiler.ProjectRunner;
6import com.jscratch.compiler.CompilationResult;
7import java.lang.reflect.*;
8import org.fife.ui.autocomplete.*;
9import org.fife.ui.rsyntaxtextarea.RSyntaxTextArea;
10import org.fife.ui.rsyntaxtextarea.RSyntaxDocument;
11import org.fife.ui.rsyntaxtextarea.SyntaxConstants;
12import org.fife.ui.rsyntaxtextarea.Theme;
13import org.fife.ui.rsyntaxtextarea.parser.AbstractParser;
14import org.fife.ui.rsyntaxtextarea.parser.ParseResult;
15import org.fife.ui.rsyntaxtextarea.parser.Parser;
16import org.fife.ui.rsyntaxtextarea.parser.DefaultParseResult;
17import org.fife.ui.rsyntaxtextarea.parser.ParserNotice;
18import org.fife.ui.rsyntaxtextarea.parser.DefaultParserNotice;
19import org.fife.ui.rtextarea.RTextScrollPane;
20import javax.swing.*;
21import javax.swing.event.DocumentEvent;
22import javax.swing.event.DocumentListener;
23import javax.swing.text.BadLocationException;
24import javax.swing.text.DefaultHighlighter;
25import javax.swing.text.Highlighter;
26import javax.swing.text.JTextComponent;
27import java.awt.*;
28import java.awt.datatransfer.DataFlavor;
29import java.awt.datatransfer.StringSelection;
30import java.awt.datatransfer.Transferable;
31import java.awt.event.MouseAdapter;
32import java.awt.event.MouseEvent;
33import java.io.BufferedReader;
34import java.io.InputStreamReader;
35import java.io.Serializable;
36import java.util.ArrayList;
37import java.util.HashMap;
38import java.util.List;
39import java.util.Map;
40import java.util.regex.Matcher;
41import java.util.regex.Pattern;
42import java.util.HashSet;
43import java.util.Set;
44
45public class CodeWorkspace extends JPanel {
46 private RSyntaxTextArea codeEditor;
47 private JTabbedPane paletteTabs;
50 private AutoCompletion autoCompletion;
51
52 private final List<SlotRange> currentSlots = new ArrayList<>();
53 private Object activeHoverHighlight;
54 private Object activeDropHighlight;
55
56 private final Highlighter.HighlightPainter slotPainter = new Highlighter.HighlightPainter() {
57 @Override public void paint(Graphics g, int p0, int p1, Shape bounds, JTextComponent c) {
58 try {
59 Rectangle r0 = c.modelToView(p0);
60 Rectangle r1 = c.modelToView(p1);
61 if (r0 == null || r1 == null) return;
62 g.setColor(new Color(0, 120, 215, 30));
63 g.fillRect(0, r0.y, c.getWidth(), (r1.y + r1.height) - r0.y);
64 } catch (Exception e) {}
65 }
66 };
67
68 private final Highlighter.HighlightPainter dropPainter = new Highlighter.HighlightPainter() {
69 @Override public void paint(Graphics g, int p0, int p1, Shape bounds, JTextComponent c) {
70 try {
71 Rectangle r0 = c.modelToView(p0);
72 Rectangle r1 = c.modelToView(p1);
73 if (r0 == null || r1 == null) return;
74 g.setColor(new Color(0, 255, 0, 50));
75 g.fillRect(0, r0.y, c.getWidth(), (r1.y + r1.height) - r0.y);
76 } catch (Exception e) {}
77 }
78 };
79
80 private boolean isUpdatingSlots = false;
81
82 private Timer autoCompileTimer;
83 private Runnable onAutoCompile;
85 private String lastCompiledText = "";
86
87 public static final DataFlavor BLOCK_FLAVOR = new DataFlavor(BlockSnippet.class, "BlockSnippet");
88
89 public static class BlockSnippet implements Serializable {
90 public String name;
91 public String category;
92 public String code;
93 public List<String> tags = new ArrayList<>();
94 }
95
96 private static class SlotRange {
97 int start;
98 int end;
101 String id;
102 List<String> tags = new ArrayList<>();
103 boolean isOnlyOne = false;
104
105 boolean hasTag(String tag) { return tags.contains(tag); }
106 boolean contains(int offset) { return offset >= start && offset <= end; }
107 int length() { return end - start; }
108 }
109
111 this.project = project;
112 this.onAutoCompile = onAutoCompile;
113 setLayout(new BorderLayout());
114
115 // 1. Setup Editor
116 codeEditor = new RSyntaxTextArea(20, 60);
117 codeEditor.setSyntaxEditingStyle(SyntaxConstants.SYNTAX_STYLE_JAVA);
118 codeEditor.setCodeFoldingEnabled(true);
119 codeEditor.setAntiAliasingEnabled(true);
120 codeEditor.setTransferHandler(new RestrictedTransferHandler());
121
123 codeEditor.addParser(errorParser);
124
125 autoCompileTimer = new Timer(1000, e -> {
126 if (this.onAutoCompile != null) {
127 lastCompiledText = codeEditor.getText();
128 this.onAutoCompile.run();
129 }
130 });
131 autoCompileTimer.setRepeats(false);
132
134
135 try {
136 Theme theme = Theme.load(getClass().getResourceAsStream("/org/fife/ui/rsyntaxtextarea/themes/eclipse.xml"));
137 theme.apply(codeEditor);
138 } catch (Exception ioe) {
139 codeEditor.setBackground(Color.WHITE);
140 codeEditor.setForeground(Color.BLACK);
141 codeEditor.setCaretColor(Color.BLACK);
142 }
143
144 codeEditor.getDocument().addDocumentListener(new DocumentListener() {
145 public void insertUpdate(DocumentEvent e) { scheduleSlotRefresh(); triggerAutoCompile(); }
146 public void removeUpdate(DocumentEvent e) { scheduleSlotRefresh(); triggerAutoCompile(); }
147 public void changedUpdate(DocumentEvent e) { scheduleSlotRefresh(); triggerAutoCompile(); }
148 });
149
150 MouseAdapter hoverAdapter = new MouseAdapter() {
151 @Override public void mouseMoved(MouseEvent e) { updateHoverHighlight(e.getPoint()); }
152 @Override public void mouseExited(MouseEvent e) { clearHoverHighlight(); }
153 };
154 codeEditor.addMouseListener(hoverAdapter);
155 codeEditor.addMouseMotionListener(hoverAdapter);
156
157 // Add DropTargetListener to clear highlights when drag leaves
158 try {
159 codeEditor.getDropTarget().addDropTargetListener(new java.awt.dnd.DropTargetAdapter() {
160 @Override public void dragExit(java.awt.dnd.DropTargetEvent dte) { clearDropHighlight(); }
161 @Override public void drop(java.awt.dnd.DropTargetDropEvent dtde) { clearDropHighlight(); }
162 });
163 } catch (Exception e) {}
164
165 // 2. Setup Palette Tabs
166 paletteTabs = new JTabbedPane();
167 paletteTabs.setPreferredSize(new Dimension(250, 0));
169
170 // 3. Layout with SplitPane
171 JSplitPane splitPane = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT, paletteTabs, new RTextScrollPane(codeEditor));
172 splitPane.setDividerLocation(250);
173 add(splitPane, BorderLayout.CENTER);
174 }
175
176 private void triggerAutoCompile() {
177 if (codeEditor.getText().equals(lastCompiledText)) return;
178 autoCompileTimer.restart();
179 }
180
181 public void setCompilationErrors(List<CompilationResult.CompilationError> errors) {
182 errorParser.setErrors(errors);
183 codeEditor.repaint();
184 }
185
186 private class CustomParser extends AbstractParser {
187 private List<CompilationResult.CompilationError> errors = new ArrayList<>();
188
189 public void setErrors(List<CompilationResult.CompilationError> errors) {
190 this.errors = errors;
191 }
192
193 @Override
194 public ParseResult parse(RSyntaxDocument doc, String styleKey) {
195 DefaultParseResult result = new DefaultParseResult(this);
196 if (currentSprite == null) return result;
197
199 if (error.fileName.equals(currentSprite.name + ".java")) {
200 try {
201 int line = error.line - 1;
202 if (line >= 0 && line < doc.getDefaultRootElement().getElementCount()) {
203 int start = doc.getDefaultRootElement().getElement(line).getStartOffset();
204 int end = doc.getDefaultRootElement().getElement(line).getEndOffset();
205 DefaultParserNotice notice = new DefaultParserNotice(this, error.message, line, start, end - start);
206 notice.setLevel(ParserNotice.Level.ERROR);
207 result.addNotice(notice);
208 }
209 } catch (Exception e) {
210 // Ignore
211 }
212 }
213 }
214 return result;
215 }
216 }
217
218 private void updateHoverHighlight(Point p) {
219 int offset = codeEditor.viewToModel(p);
220 SlotRange bestMatch = null;
221 for (SlotRange slot : currentSlots) {
222 if (slot.contains(offset)) {
223 if (bestMatch == null || slot.length() < bestMatch.length()) {
224 bestMatch = slot;
225 }
226 }
227 }
229 if (bestMatch != null) {
230 try {
231 activeHoverHighlight = codeEditor.getHighlighter().addHighlight(bestMatch.start, bestMatch.end, slotPainter);
232 } catch (BadLocationException ex) {}
233 }
234 }
235
236 private void clearHoverHighlight() {
237 if (activeHoverHighlight != null) {
238 codeEditor.getHighlighter().removeHighlight(activeHoverHighlight);
240 }
241 }
242
243 private void clearDropHighlight() {
244 if (activeDropHighlight != null) {
245 codeEditor.getHighlighter().removeHighlight(activeDropHighlight);
246 activeDropHighlight = null;
247 }
248 }
249
250 private void applyDropHighlight(SlotRange slot) {
251 if (activeDropHighlight != null) return;
252 try {
253 activeDropHighlight = codeEditor.getHighlighter().addHighlight(slot.start, slot.end, dropPainter);
254 } catch (BadLocationException ex) {}
255 }
256
257 private void loadLiteralBlocks() {
258 Map<String, JPanel> categoryPanels = new HashMap<>();
259 try (BufferedReader reader = new BufferedReader(new InputStreamReader(getClass().getResourceAsStream("/blocks.txt")))) {
260 String line;
261 BlockSnippet currentBlock = null;
262 StringBuilder codeBuffer = new StringBuilder();
263
264 while ((line = reader.readLine()) != null) {
265 if (line.startsWith("# ")) {
266 String meta = line.substring(2);
267 String[] parts = meta.split(" : ");
268 currentBlock = new BlockSnippet();
269 currentBlock.name = parts[0].trim();
270 currentBlock.category = parts[1].trim();
271 if (parts.length > 2) {
272 for (String t : parts[2].split(",")) currentBlock.tags.add(t.trim());
273 }
274 codeBuffer.setLength(0);
275 } else if (line.equals("###")) {
276 if (currentBlock != null) {
277 currentBlock.code = codeBuffer.toString().trim();
278 JPanel panel = categoryPanels.computeIfAbsent(currentBlock.category, k -> {
279 JPanel p = new JPanel();
280 p.setLayout(new BoxLayout(p, BoxLayout.Y_AXIS));
281 p.setBackground(Color.WHITE);
282 p.setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10));
283 paletteTabs.addTab(k, new JScrollPane(p));
284 return p;
285 });
286 panel.add(createBlockUI(currentBlock));
287 panel.add(Box.createVerticalStrut(5));
288 currentBlock = null;
289 }
290 } else if (currentBlock != null) {
291 codeBuffer.append(line).append("\n");
292 }
293 }
294 } catch (Exception e) {
295 e.printStackTrace();
296 }
297 }
298
299 private JComponent createBlockUI(final BlockSnippet block) {
300 JLabel label = new JLabel(block.name);
301 label.setOpaque(true);
302 label.setBackground(getCategoryColor(block.category));
303 label.setForeground(Color.WHITE);
304 label.setFont(new Font("Arial", Font.BOLD, 12));
305 label.setBorder(BorderFactory.createEmptyBorder(8, 12, 8, 12));
306 label.setMaximumSize(new Dimension(220, 40));
307 label.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR));
308 label.setTransferHandler(new TransferHandler() {
309 @Override public int getSourceActions(JComponent c) { return COPY; }
310 @Override protected Transferable createTransferable(JComponent c) { return new BlockTransferable(block); }
311 });
312 label.addMouseListener(new MouseAdapter() {
313 @Override public void mousePressed(MouseEvent e) {
314 ((JComponent)e.getSource()).getTransferHandler().exportAsDrag((JComponent)e.getSource(), e, TransferHandler.COPY);
315 }
316 });
317 return label;
318 }
319
320 private Color getCategoryColor(String category) {
321 switch (category) {
322 case "Motion": return new Color(76, 151, 255);
323 case "Looks": return new Color(153, 102, 255);
324 case "Sound": return new Color(207, 99, 207);
325 case "Events": return new Color(255, 191, 0);
326 case "Control": return new Color(255, 171, 25);
327 case "Sensing": return new Color(92, 177, 214);
328 case "Operators": return new Color(89, 192, 89);
329 case "Variables": return new Color(255, 140, 26);
330 case "My Blocks": return new Color(255, 102, 128);
331 case "Structure": return new Color(214, 92, 214);
332 case "Helpers": return new Color(235, 64, 52);
333
334 default: return Color.GRAY;
335 }
336 }
337
338 private void scheduleSlotRefresh() {
339 if (isUpdatingSlots) return;
340 SwingUtilities.invokeLater(this::refreshSlots);
341 }
342
343 private void refreshSlots() {
344 isUpdatingSlots = true;
345 currentSlots.clear();
346 String text = codeEditor.getText();
347
348 Pattern startPattern = Pattern.compile("/\\* slot start \\[(.*?)\\] \\*/");
349 Matcher matcher = startPattern.matcher(text);
350 while (matcher.find()) {
351 String attrContent = matcher.group(1);
352 SlotRange slot = new SlotRange();
353 slot.start = matcher.start();
354 slot.startCommentEnd = matcher.end();
355
356 Matcher idM = Pattern.compile("id:\"(.*?)\"").matcher(attrContent);
357 if (idM.find()) slot.id = idM.group(1);
358
359 Matcher tagsM = Pattern.compile("tags:\"(.*?)\"").matcher(attrContent);
360 if (tagsM.find()) {
361 for (String t : tagsM.group(1).split(",")) slot.tags.add(t.trim());
362 }
363
364 Matcher onlyOneM = Pattern.compile("onlyOne:(true|false)").matcher(attrContent);
365 if (onlyOneM.find()) slot.isOnlyOne = Boolean.parseBoolean(onlyOneM.group(1));
366
367 if (slot.id == null) continue;
368
369 String endRegex = "/\\* slot end \\[(.*?id:\"" + Pattern.quote(slot.id) + "\".*?)\\] \\*/";
370 Pattern endPattern = Pattern.compile(endRegex);
371 Matcher endMatcher = endPattern.matcher(text);
372 if (endMatcher.find(matcher.end())) {
373 slot.end = endMatcher.end();
374 slot.endCommentStart = endMatcher.start();
375 currentSlots.add(slot);
376 }
377 }
378 isUpdatingSlots = false;
379 }
380
381 private static class BlockTransferable implements Transferable {
382 private final BlockSnippet block;
383 public BlockTransferable(BlockSnippet block) { this.block = block; }
384 @Override public DataFlavor[] getTransferDataFlavors() { return new DataFlavor[]{BLOCK_FLAVOR, DataFlavor.stringFlavor}; }
385 @Override public boolean isDataFlavorSupported(DataFlavor flavor) { return flavor.equals(BLOCK_FLAVOR) || flavor.equals(DataFlavor.stringFlavor); }
386 @Override public Object getTransferData(DataFlavor flavor) {
387 if (flavor.equals(BLOCK_FLAVOR)) return block;
388 return (flavor.equals(DataFlavor.stringFlavor)) ? block.code : null;
389 }
390 }
391
392 private class RestrictedTransferHandler extends TransferHandler {
393 @Override
394 public int getSourceActions(JComponent c) {
395 return COPY_OR_MOVE;
396 }
397
398 @Override
399 protected Transferable createTransferable(JComponent c) {
400 return new StringSelection(codeEditor.getSelectedText());
401 }
402
403 @Override
404 public boolean canImport(TransferSupport support) {
405 // Allow string paste
406 if (!support.isDrop() && support.isDataFlavorSupported(DataFlavor.stringFlavor)) {
407 return true;
408 }
409
410 if (!support.isDrop() || !support.isDataFlavorSupported(BLOCK_FLAVOR)) return false;
411 try {
412 BlockSnippet block = (BlockSnippet) support.getTransferable().getTransferData(BLOCK_FLAVOR);
413 JTextComponent.DropLocation dl = (JTextComponent.DropLocation) support.getDropLocation();
414 int offset = dl.getIndex();
415 String text = codeEditor.getText();
416
417 SlotRange activeSlot = null;
418 for (SlotRange slot : currentSlots) {
419 if (slot.contains(offset)) {
420 if (activeSlot == null || slot.length() < activeSlot.length()) activeSlot = slot;
421 }
422 }
423
424 boolean isValid = false;
425 if (text.trim().isEmpty() && block.tags.contains("allowEmpty")) {
426 isValid = true;
427 } else if (activeSlot == null) {
428 isValid = block.tags.contains("anywhere");
429 } else {
430 boolean tagMatch = false;
431 for (String bt : block.tags) if (activeSlot.hasTag(bt)) { tagMatch = true; break; }
432
433 if (tagMatch) {
434 if (activeSlot.isOnlyOne) {
435 String body = text.substring(activeSlot.startCommentEnd, activeSlot.endCommentStart).trim();
436 isValid = body.isEmpty();
437 } else {
438 // --- Extra Safe Insertion Checks ---
439 int line = codeEditor.getLineOfOffset(offset);
440 int lineStart = codeEditor.getLineStartOffset(line);
441 int lineEnd = codeEditor.getLineEndOffset(line);
442 String lineText = text.substring(lineStart, lineEnd);
443
444 boolean isEmptyLine = lineText.trim().isEmpty();
445 boolean isInline = codeEditor.getLineOfOffset(activeSlot.start) == codeEditor.getLineOfOffset(activeSlot.end);
446
447 if (isInline) {
448 if (offset >= 2 && offset <= text.length() - 2) {
449 boolean leftSafe = Character.isWhitespace(text.charAt(offset - 1)) && Character.isWhitespace(text.charAt(offset - 2));
450 boolean rightSafe = Character.isWhitespace(text.charAt(offset)) && Character.isWhitespace(text.charAt(offset + 1));
451 isValid = leftSafe && rightSafe;
452 }
453 } else {
454 isValid = isEmptyLine;
455 }
456 }
457 }
458 }
459
461 if (isValid && activeSlot != null) {
462 applyDropHighlight(activeSlot);
463 }
464 return isValid;
465 } catch (Exception e) { return false; }
466 }
467
468 @Override
469 public boolean importData(TransferSupport support) {
470 if (!canImport(support)) return false;
471
472 if (!support.isDrop() && support.isDataFlavorSupported(DataFlavor.stringFlavor)) {
473 try {
474 String text = (String) support.getTransferable().getTransferData(DataFlavor.stringFlavor);
475 codeEditor.replaceSelection(text);
476 return true;
477 } catch (Exception e) { return false; }
478 }
479
480 try {
481 BlockSnippet block = (BlockSnippet) support.getTransferable().getTransferData(BLOCK_FLAVOR);
482 JTextComponent.DropLocation dl = (JTextComponent.DropLocation) support.getDropLocation();
483
484 String code = block.code;
485 if (code.contains("${id}")) {
486 code = code.replace("${id}", String.valueOf(project.blockIdCounter++));
487 }
488
489 codeEditor.insert(code + "\n", dl.getIndex());
490 return true;
491 } catch (Exception e) { return false; }
492 }
493 }
494
495 public void loadSpriteCode(SpriteData sprite) {
496 if (currentSprite != null) saveCurrentCode();
497 this.currentSprite = sprite;
498 codeEditor.setText(sprite.sourceCode);
500 codeEditor.setCaretPosition(0);
501 codeEditor.discardAllEdits();
502 refreshSlots();
503 }
504
505 public void saveCurrentCode() {
506 if (currentSprite != null) currentSprite.sourceCode = codeEditor.getText();
507 }
508
509 private void setupAutocomplete() {
510 DefaultCompletionProvider provider = new DefaultCompletionProvider() {
511 @Override
512 protected List<Completion> getCompletionsImpl(JTextComponent comp) {
513 List<Completion> ret = new ArrayList<>();
514 String text = comp.getText();
515 int caret = comp.getCaretPosition();
516
517 String prefix = getAlreadyEnteredText(comp);
518 int lineStart = 0;
519 try { lineStart = codeEditor.getLineStartOffset(codeEditor.getLineOfOffset(caret)); } catch (Exception e) {}
520 String currentLine = text.substring(lineStart, caret);
521
522 int dotIdx = -1;
523 for (int i = caret - 1; i >= 0 && i >= caret - 50; i--) {
524 char c = text.charAt(i);
525 if (c == '.') { dotIdx = i; break; }
526 if (!Character.isJavaIdentifierPart(c)) break;
527 }
528
529 if (currentLine.trim().startsWith("import ")) {
530 // --- IMPORT COMPLETION ---
531 addImportPathCompletions(this, ret, currentLine, prefix);
532 } else if (dotIdx != -1) {
533 // --- DOT COMPLETION ---
534 String expr = getExpressionBeforeDot(text, dotIdx);
535 Class<?> type = resolveType(expr);
536 if (type != null) {
537 boolean isStatic = isClass(expr);
538 addTypeMembers(this, ret, type, isStatic);
539 }
540 } else {
541 // --- GLOBAL COMPLETION ---
542 addGlobalCompletions(this, ret);
543 }
544
545 List<Completion> filtered = new ArrayList<>();
546 for (Completion c : ret) {
547 if (c.getReplacementText().startsWith(prefix)) filtered.add(c);
548 }
549 filtered.sort((a, b) -> a.getReplacementText().compareToIgnoreCase(b.getReplacementText()));
550 return filtered;
551 }
552 };
553
554 autoCompletion = new AutoCompletion(provider);
555 autoCompletion.install(codeEditor);
556
557 autoCompletion.setListCellRenderer(new CompletionCellRenderer());
558 // CheerpJ Fixes:
559 // 1. Use default renderer (custom one often fails to size correctly in CheerpJ)
560 // autoCompletion.setListCellRenderer(new CompletionCellRenderer());
561
562 // 2. Disable description window (prevents focus-fighting between two popup windows)
563 autoCompletion.setShowDescWindow(false);
564
565 // 3. Change trigger to Ctrl+I (Ctrl+Space often conflicts with Browser/OS IME shortcuts)
566 autoCompletion.setTriggerKey(KeyStroke.getKeyStroke(java.awt.event.KeyEvent.VK_I, java.awt.event.InputEvent.CTRL_DOWN_MASK));
567 }
568
569 private void addImportPathCompletions(CompletionProvider provider, List<Completion> list, String line, String prefix) {
570 String trimmedLine = line.trim();
571 if (trimmedLine.length() < 7) return;
572 String path = trimmedLine.substring(7).trim(); // remove "import "
573 if (path.endsWith(".")) path = path.substring(0, path.length() - 1);
574
575 Set<String> suggestions = new HashSet<>();
576 Package[] packages = Package.getPackages();
577 for (Package p : packages) {
578 String name = p.getName();
579 if (name.startsWith(path)) {
580 int startIdx = path.isEmpty() ? 0 : path.length();
581 if (!path.isEmpty() && name.length() > startIdx && name.charAt(startIdx) == '.') startIdx++;
582
583 if (startIdx < name.length()) {
584 String sub = name.substring(startIdx);
585 int nextDot = sub.indexOf('.');
586 suggestions.add(nextDot == -1 ? sub : sub.substring(0, nextDot));
587 }
588 }
589 }
590
591 // Also add classes in that package (if path is a full package)
592 try {
593 // This is limited as we can't easily list all classes in a package in standard Java
594 // But we can add common ones or project ones
595 if (path.equals("com.jscratch")) {
596 suggestions.add("Sprite"); suggestions.add("Stage"); suggestions.add("Costume");
597 } else if (path.equals("java.util")) {
598 suggestions.add("List"); suggestions.add("ArrayList"); suggestions.add("HashMap"); suggestions.add("Map");
599 } else if (path.equals("java.awt")) {
600 suggestions.add("Color"); suggestions.add("Graphics2D");
601 }
602 } catch (Exception e) {}
603
604 for (String s : suggestions) list.add(new BasicCompletion(provider, s));
605 }
606
607 private String getExpressionBeforeDot(String text, int dotIdx) {
608 int start = dotIdx - 1;
609 while (start >= 0) {
610 char c = text.charAt(start);
611 if (!Character.isJavaIdentifierPart(c) && c != '.') break;
612 start--;
613 }
614 return text.substring(start + 1, dotIdx);
615 }
616
617 private boolean isClass(String expr) {
618 try {
619 Class.forName(expr); return true;
620 } catch (Exception e) {
621 try { Class.forName("java.lang." + expr); return true; } catch (Exception e2) {}
622 try { Class.forName("java.util." + expr); return true; } catch (Exception e2) {}
623 ClassLoader loader = ProjectRunner.getProjectClassLoader();
624 if (loader != null) {
625 try { loader.loadClass("generated." + expr); return true; } catch (Exception e3) {}
626 }
627 }
628 return false;
629 }
630
631 private Class<?> resolveType(String expr) {
632 if (expr.equals("this")) {
633 if (currentSprite == null) return null;
634 ClassLoader loader = ProjectRunner.getProjectClassLoader();
635 if (loader != null) {
636 try { return loader.loadClass("generated." + currentSprite.name); } catch (Exception e) {}
637 }
638 // If it's the "Main" class, don't fall back to Sprite.class
639 if ("Main".equals(currentSprite.name)) return null;
640 return com.jscratch.Sprite.class;
641 }
642 if (expr.equals("Stage") || expr.equals("Stage.getInstance()")) return com.jscratch.Stage.class;
643
644 try { return Class.forName(expr); } catch (Exception e) {}
645 try { return Class.forName("java.lang." + expr); } catch (Exception e) {}
646 try { return Class.forName("java.util." + expr); } catch (Exception e) {}
647
648 ClassLoader loader = ProjectRunner.getProjectClassLoader();
649 if (loader != null) {
650 try { return loader.loadClass("generated." + expr); } catch (Exception e) {}
651 }
652
653 String varType = findVariableType(expr);
654 if (varType != null) return resolveType(varType);
655
656 return null;
657 }
658
659 private String findVariableType(String varName) {
660 String text = codeEditor.getText();
661 Pattern p = Pattern.compile("\\b([A-Z][a-zA-Z0-9_]*)\\s+\\b" + Pattern.quote(varName) + "\\b");
662 Matcher m = p.matcher(text);
663 if (m.find()) return m.group(1);
664
665 p = Pattern.compile("(?:public|private|protected)?\\s+([A-Z][a-zA-Z0-9_]*)\\s+\\b" + Pattern.quote(varName) + "\\b\\s*[;=]");
666 m = p.matcher(text);
667 if (m.find()) return m.group(1);
668
669 return null;
670 }
671
672 private void addTypeMembers(CompletionProvider provider, List<Completion> list, Class<?> clazz, boolean staticOnly) {
673 Class<?> current = clazz;
674 Set<String> seenMethods = new HashSet<>();
675 Set<String> seenFields = new HashSet<>();
676
677 while (current != null && current != Object.class) {
678 try {
679 for (Method m : current.getDeclaredMethods()) {
680 boolean isStatic = Modifier.isStatic(m.getModifiers());
681 if (staticOnly == isStatic) {
682 // Use name + parameter types as signature to handle overrides
683 String sig = m.getName() + java.util.Arrays.toString(m.getParameterTypes());
684 if (seenMethods.add(sig)) {
685 FunctionCompletion fc = new FunctionCompletion(provider, m.getName(), m.getReturnType().getSimpleName());
686 fc.setShortDescription(createMethodDesc(m));
687 list.add(fc);
688 }
689 }
690 }
691 for (Field f : current.getDeclaredFields()) {
692 boolean isStatic = Modifier.isStatic(f.getModifiers());
693 if (staticOnly == isStatic) {
694 if (seenFields.add(f.getName())) {
695 VariableCompletion vc = new VariableCompletion(provider, f.getName(), f.getType().getSimpleName());
696 list.add(vc);
697 }
698 }
699 }
700 } catch (SecurityException e) {
701 // Fallback to public members if security restriction applies
702 if (current == clazz) { // Only fallback for the main class once
703 for (Method m : clazz.getMethods()) {
704 boolean isStatic = Modifier.isStatic(m.getModifiers());
705 if (staticOnly == isStatic) {
706 String sig = m.getName() + java.util.Arrays.toString(m.getParameterTypes());
707 if (seenMethods.add(sig)) {
708 FunctionCompletion fc = new FunctionCompletion(provider, m.getName(), m.getReturnType().getSimpleName());
709 fc.setShortDescription(createMethodDesc(m));
710 list.add(fc);
711 }
712 }
713 }
714 for (Field f : clazz.getFields()) {
715 boolean isStatic = Modifier.isStatic(f.getModifiers());
716 if (staticOnly == isStatic) {
717 if (seenFields.add(f.getName())) {
718 VariableCompletion vc = new VariableCompletion(provider, f.getName(), f.getType().getSimpleName());
719 list.add(vc);
720 }
721 }
722 }
723 break;
724 }
725 }
726 current = current.getSuperclass();
727 }
728 }
729
730 private boolean isInStaticContext() {
731 try {
732 int caret = codeEditor.getCaretPosition();
733 int line = codeEditor.getLineOfOffset(caret);
734 for (int i = line; i >= 0; i--) {
735 int start = codeEditor.getLineStartOffset(i);
736 int end = codeEditor.getLineEndOffset(i);
737 String lineText = codeEditor.getText(start, end - start);
738 if (lineText.contains("{")) {
739 // Heuristic: If the line containing the last opening brace has 'static', we are in a static block/method
740 if (lineText.contains("static")) return true;
741 // If we see 'class', we are at class level (not inside a method)
742 if (lineText.contains("class")) return false;
743 // If we see a brace but no 'static', it might be an instance method or a control block.
744 // We continue searching up to find the method header.
745 }
746 }
747 } catch (Exception e) {}
748 return false;
749 }
750
751 private void addGlobalCompletions(CompletionProvider provider, List<Completion> list) {
752 Set<String> suggestions = new HashSet<>();
753
754 // Add common core classes
755 String[] coreClasses = {"String", "Integer", "Math", "System", "ArrayList", "List", "Color", "KeyEvent", "Sprite", "Stage", "Thread"};
756 for (String c : coreClasses) suggestions.add(c);
757
758 // Add all sprites from the project
759 for (Project.SpriteData sd : project.sprites) {
760 suggestions.add(sd.name);
761 }
762
763 for (String s : suggestions) list.add(new BasicCompletion(provider, s));
764
765 boolean isStatic = isInStaticContext();
766 Class<?> thisType = resolveType("this");
767
768 if (thisType != null) {
769 // Only suggest instance members if we aren't in a static context
770 if (!isStatic) {
771 addTypeMembers(provider, list, thisType, false);
772 }
773 // Static members of 'this' are always valid
774 addTypeMembers(provider, list, thisType, true);
775 }
776
777 // Also pick up variables/fields from the text (useful before first compile)
778 Pattern p = Pattern.compile("\\b(?:[A-Z][a-zA-Z0-9_]*|int|double|float|long|boolean|char|byte|short|String)\\s+\\b([a-z][a-zA-Z0-9_]*)\\b");
779 Matcher m = p.matcher(codeEditor.getText());
780 Set<String> seen = new HashSet<>();
781 while (m.find()) {
782 String name = m.group(1);
783 if (seen.add(name)) {
784 list.add(new VariableCompletion(provider, name, ""));
785 }
786 }
787
788 String[] imports = {"import java.util.*;", "import java.awt.Color;", "import com.jscratch.*;"};
789 for (String i : imports) list.add(new BasicCompletion(provider, i));
790 }
791
792 private String createMethodDesc(Method m) {
793 StringBuilder desc = new StringBuilder("<html><b>" + m.getName() + "</b>(");
794 Class<?>[] params = m.getParameterTypes();
795 for (int i = 0; i < params.length; i++) {
796 desc.append(params[i].getSimpleName());
797 if (i < params.length - 1) desc.append(", ");
798 }
799 desc.append(")<br>Returns: <i>" + m.getReturnType().getSimpleName() + "</i></html>");
800 return desc.toString();
801 }
802}
static java.net.URLClassLoader getProjectClassLoader()
ParseResult parse(RSyntaxDocument doc, String styleKey)
List< CompilationResult.CompilationError > errors
void setErrors(List< CompilationResult.CompilationError > errors)
final Highlighter.HighlightPainter dropPainter
JComponent createBlockUI(final BlockSnippet block)
String getExpressionBeforeDot(String text, int dotIdx)
CodeWorkspace(Project project, Runnable onAutoCompile)
final List< SlotRange > currentSlots
void loadSpriteCode(SpriteData sprite)
final Highlighter.HighlightPainter slotPainter
String findVariableType(String varName)
void setCompilationErrors(List< CompilationResult.CompilationError > errors)
Class<?> resolveType(String expr)
void addGlobalCompletions(CompletionProvider provider, List< Completion > list)
Color getCategoryColor(String category)
static final DataFlavor BLOCK_FLAVOR
void addImportPathCompletions(CompletionProvider provider, List< Completion > list, String line, String prefix)
void addTypeMembers(CompletionProvider provider, List< Completion > list, Class<?> clazz, boolean staticOnly)
void applyDropHighlight(SlotRange slot)