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