diff --git a/app/build.xml b/app/build.xml
index d88c13dd1..dcf718f02 100644
--- a/app/build.xml
+++ b/app/build.xml
@@ -182,8 +182,8 @@
diff --git a/app/processing4-app.iml b/app/processing4-app.iml
index 62e9a8c3a..e2df2c5b2 100644
--- a/app/processing4-app.iml
+++ b/app/processing4-app.iml
@@ -65,4 +65,4 @@
-
\ No newline at end of file
+
diff --git a/build/build.xml b/build/build.xml
index 2dd0bb262..639f39b86 100644
--- a/build/build.xml
+++ b/build/build.xml
@@ -377,6 +377,11 @@
+
+
+
+
+
-
+
@@ -18,6 +18,34 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -32,6 +60,11 @@
+
+
+
+
+
@@ -71,6 +104,8 @@
+
+
@@ -123,7 +158,7 @@
-
+
@@ -140,7 +175,7 @@
-
+
diff --git a/java/ivy.xml b/java/ivy.xml
new file mode 100644
index 000000000..68bc5a354
--- /dev/null
+++ b/java/ivy.xml
@@ -0,0 +1,7 @@
+
+
+
+
+
+
+
diff --git a/java/lib/.gitignore b/java/lib/.gitignore
new file mode 100644
index 000000000..d392f0e82
--- /dev/null
+++ b/java/lib/.gitignore
@@ -0,0 +1 @@
+*.jar
diff --git a/java/src/processing/mode/java/CompletionGenerator.java b/java/src/processing/mode/java/CompletionGenerator.java
index e5c127872..5a23816e9 100644
--- a/java/src/processing/mode/java/CompletionGenerator.java
+++ b/java/src/processing/mode/java/CompletionGenerator.java
@@ -1762,7 +1762,7 @@ public class CompletionGenerator {
}
- protected static DefaultListModel filterPredictions(List candidates) {
+ public static DefaultListModel filterPredictions(List candidates) {
Messages.log("* filterPredictions");
DefaultListModel defListModel = new DefaultListModel<>();
if (candidates.isEmpty())
diff --git a/java/src/processing/mode/java/ErrorChecker.java b/java/src/processing/mode/java/ErrorChecker.java
index 44f9f1944..f302b5078 100644
--- a/java/src/processing/mode/java/ErrorChecker.java
+++ b/java/src/processing/mode/java/ErrorChecker.java
@@ -28,7 +28,7 @@ import processing.app.Language;
import processing.app.Problem;
-class ErrorChecker {
+public class ErrorChecker {
// Delay delivering error check result after last sketch change
// https://github.com/processing/processing/issues/2677
private final static long DELAY_BEFORE_UPDATE = 650;
@@ -41,11 +41,11 @@ class ErrorChecker {
private final Consumer errorHandlerListener =
this::handleSketchProblems;
- final private JavaEditor editor;
+ final private Consumer> editor;
final private PreprocService pps;
- public ErrorChecker(JavaEditor editor, PreprocService pps) {
+ public ErrorChecker(Consumer> editor, PreprocService pps) {
this.editor = editor;
this.pps = pps;
@@ -69,7 +69,7 @@ class ErrorChecker {
pps.registerListener(errorHandlerListener);
} else {
pps.unregisterListener(errorHandlerListener);
- editor.setProblemList(Collections.emptyList());
+ editor.accept(Collections.emptyList());
nextUiUpdate = 0;
}
}
@@ -136,7 +136,7 @@ class ErrorChecker {
long delay = nextUiUpdate - System.currentTimeMillis();
Runnable uiUpdater = () -> {
if (nextUiUpdate > 0 && System.currentTimeMillis() >= nextUiUpdate) {
- EventQueue.invokeLater(() -> editor.setProblemList(problems));
+ EventQueue.invokeLater(() -> editor.accept(problems));
}
};
scheduledUiUpdate =
diff --git a/java/src/processing/mode/java/JavaEditor.java b/java/src/processing/mode/java/JavaEditor.java
index efb37b180..cc1feb740 100644
--- a/java/src/processing/mode/java/JavaEditor.java
+++ b/java/src/processing/mode/java/JavaEditor.java
@@ -129,7 +129,7 @@ public class JavaEditor extends Editor {
box.add(textAndError);
*/
- preprocService = new PreprocService(this);
+ preprocService = new PreprocService(this.jmode, this.sketch);
// long t5 = System.currentTimeMillis();
@@ -141,7 +141,7 @@ public class JavaEditor extends Editor {
astViewer = new ASTViewer(this, preprocService);
}
- errorChecker = new ErrorChecker(this, preprocService);
+ errorChecker = new ErrorChecker(this::setProblemList, preprocService);
// long t7 = System.currentTimeMillis();
diff --git a/java/src/processing/mode/java/JavaTextArea.java b/java/src/processing/mode/java/JavaTextArea.java
index 1dedbf3fc..ecab00eed 100644
--- a/java/src/processing/mode/java/JavaTextArea.java
+++ b/java/src/processing/mode/java/JavaTextArea.java
@@ -341,7 +341,7 @@ public class JavaTextArea extends PdeTextArea {
}
- protected static String parsePhrase(final String lineText) {
+ public static String parsePhrase(final String lineText) {
boolean overloading = false;
{ // Check if we can provide suggestions for this phrase ending
diff --git a/java/src/processing/mode/java/PreprocService.java b/java/src/processing/mode/java/PreprocService.java
index 4d19f7a64..8750720f7 100644
--- a/java/src/processing/mode/java/PreprocService.java
+++ b/java/src/processing/mode/java/PreprocService.java
@@ -78,7 +78,8 @@ public class PreprocService {
private final static int TIMEOUT_MILLIS = 100;
private final static int BLOCKING_TIMEOUT_SECONDS = 3000;
- protected final JavaEditor editor;
+ protected final JavaMode javaMode;
+ protected final Sketch sketch;
protected final ASTParser parser = ASTParser.newParser(AST.JLS11);
@@ -104,8 +105,9 @@ public class PreprocService {
* Create a new preprocessing service to support an editor.
* @param editor The editor supported by this service and receives issues.
*/
- public PreprocService(JavaEditor editor) {
- this.editor = editor;
+ public PreprocService(JavaMode javaMode, Sketch sketch) {
+ this.javaMode = javaMode;
+ this.sketch = sketch;
// Register listeners for first run
whenDone(this::fireListeners);
@@ -342,8 +344,7 @@ public class PreprocService {
List codeFolderImports = result.codeFolderImports;
List programImports = result.programImports;
- JavaMode javaMode = (JavaMode) editor.getMode();
- Sketch sketch = result.sketch = editor.getSketch();
+ result.sketch = this.sketch;
String className = sketch.getMainName();
StringBuilder workBuffer = new StringBuilder();
@@ -385,7 +386,7 @@ public class PreprocService {
// Core and default imports
PdePreprocessor preProcessor =
- editor.createPreprocessor(editor.getSketch().getMainName());
+ PdePreprocessor.builderFor(this.sketch.getName()).build();
if (coreAndDefaultImports == null) {
coreAndDefaultImports = buildCoreAndDefaultImports(preProcessor);
}
@@ -421,7 +422,7 @@ public class PreprocService {
final int endNumLines = numLines;
preprocessorResult.getPreprocessIssues().stream()
- .map((x) -> ProblemFactory.build(x, tabLineStarts, endNumLines, editor))
+ .map((x) -> ProblemFactory.build(x, tabLineStarts))
.forEach(result.otherProblems::add);
result.hasSyntaxErrors = true;
diff --git a/java/src/processing/mode/java/languageServer/App.java b/java/src/processing/mode/java/languageServer/App.java
new file mode 100644
index 000000000..bc884c405
--- /dev/null
+++ b/java/src/processing/mode/java/languageServer/App.java
@@ -0,0 +1,26 @@
+package processing.mode.java.languageServer;
+
+import org.eclipse.lsp4j.launch.LSPLauncher;
+import java.io.File;
+import java.net.ServerSocket;
+import java.io.InputStream;
+import java.io.OutputStream;
+
+public class App {
+ public static void main(String[] args) {
+ var input = System.in;
+ var output = System.out;
+ System.setOut(System.err);
+
+ var server = new ProcessingLanguageServer();
+ var launcher =
+ LSPLauncher.createServerLauncher(
+ server,
+ input,
+ output
+ );
+ var client = launcher.getRemoteProxy();
+ server.connect(client);
+ launcher.startListening();
+ }
+}
diff --git a/java/src/processing/mode/java/languageServer/ProcessingAdapter.java b/java/src/processing/mode/java/languageServer/ProcessingAdapter.java
new file mode 100644
index 000000000..44cd10a00
--- /dev/null
+++ b/java/src/processing/mode/java/languageServer/ProcessingAdapter.java
@@ -0,0 +1,346 @@
+package processing.mode.java.languageServer;
+
+import org.eclipse.lsp4j.services.LanguageServer;
+import org.eclipse.lsp4j.services.TextDocumentService;
+import org.eclipse.lsp4j.services.WorkspaceService;
+import org.eclipse.lsp4j.InitializeResult;
+import org.eclipse.lsp4j.InitializeParams;
+import java.util.concurrent.CompletableFuture;
+import org.eclipse.lsp4j.ServerCapabilities;
+import org.eclipse.lsp4j.TextDocumentSyncKind;
+import org.eclipse.lsp4j.CompletionOptions;
+import org.eclipse.lsp4j.CompletionParams;
+import org.eclipse.lsp4j.CompletionItem;
+import org.eclipse.lsp4j.CompletionList;
+import org.eclipse.lsp4j.jsonrpc.CompletableFutures;
+import java.util.List;
+import processing.app.Base;
+import processing.app.Platform;
+import processing.app.Console;
+import processing.app.Language;
+import processing.app.Preferences;
+import processing.app.contrib.ModeContribution;
+import processing.mode.java.JavaMode;
+import java.io.File;
+import processing.app.Sketch;
+import processing.mode.java.JavaBuild;
+import processing.mode.java.CompletionGenerator;
+import processing.mode.java.PreprocService;
+import org.eclipse.lsp4j.WorkspaceFoldersOptions;
+import org.eclipse.lsp4j.services.LanguageClientAware;
+import org.eclipse.lsp4j.services.LanguageClient;
+import processing.mode.java.ErrorChecker;
+import processing.app.Problem;
+import org.eclipse.lsp4j.PublishDiagnosticsParams;
+import org.eclipse.lsp4j.Diagnostic;
+import org.eclipse.lsp4j.Range;
+import org.eclipse.lsp4j.Position;
+import org.eclipse.lsp4j.DiagnosticSeverity;
+import processing.mode.java.PreprocSketch;
+import processing.mode.java.JavaTextArea;
+import java.util.Collections;
+import processing.mode.java.CompletionCandidate;
+import javax.swing.DefaultListModel;
+import org.eclipse.lsp4j.InsertTextFormat;
+import org.eclipse.lsp4j.CompletionItemKind;
+import org.jsoup.Jsoup;
+import java.net.URI;
+import processing.app.SketchCode;
+import org.eclipse.lsp4j.TextEdit;
+import processing.mode.java.AutoFormat;
+import java.util.Optional;
+import java.util.HashSet;
+import java.util.Arrays;
+import java.util.stream.Collectors;
+import java.util.stream.IntStream;
+import java.util.Map;
+import java.util.AbstractMap;
+import java.util.HashMap;
+import java.util.Set;
+
+class Offset {
+ int line;
+ int col;
+
+ Offset(int line, int col) {
+ this.line = line;
+ this.col = col;
+ }
+}
+
+class ProcessingAdapter {
+ File rootPath;
+ LanguageClient client;
+ JavaMode javaMode;
+ File pdeFile;
+ Sketch sketch;
+ CompletionGenerator completionGenerator;
+ PreprocService preprocService;
+ ErrorChecker errorChecker;
+ CompletableFuture cps;
+ CompletionGenerator suggestionGenerator;
+ Set prevDiagnosticReportUris = new HashSet();
+
+
+ ProcessingAdapter(File rootPath, LanguageClient client) {
+ this.rootPath = rootPath;
+ this.client = client;
+ this.javaMode = (JavaMode) ModeContribution
+ .load(
+ null,
+ Platform.getContentFile("modes/java"),
+ "processing.mode.java.JavaMode"
+ )
+ .getMode();
+ this.pdeFile = new File(rootPath, rootPath.getName() + ".pde");
+ this.sketch = new Sketch(pdeFile.toString(), javaMode);
+ this.completionGenerator = new CompletionGenerator(javaMode);
+ this.preprocService = new PreprocService(javaMode, sketch);
+ this.errorChecker = new ErrorChecker(
+ this::updateProblems,
+ preprocService
+ );
+ this.cps = CompletableFutures.computeAsync(_x -> {
+ throw new RuntimeException("unreachable");
+ });
+ this.suggestionGenerator = new CompletionGenerator(this.javaMode);
+
+ this.notifySketchChanged();
+ }
+
+ static Optional uriToPath(URI uri) {
+ try {
+ return Optional.of(new File(uri));
+ } catch (Exception e) {
+ return Optional.empty();
+ }
+ }
+
+ static URI pathToUri(File path) {
+ return path.toURI();
+ }
+
+
+ static Offset toLineCol(String s, int offset) {
+ int line = (int)s.substring(0, offset).chars().filter(c -> c == '\n').count();
+ int col = offset - s.substring(0, offset).lastIndexOf('\n');
+ return new Offset(line, col);
+ }
+
+ static void init() {
+ Base.setCommandLine();
+ Platform.init();
+ Preferences.init();
+ }
+
+ void notifySketchChanged() {
+ CompletableFuture cps = new CompletableFuture();
+ this.cps = cps;
+ preprocService.notifySketchChanged();
+ errorChecker.notifySketchChanged();
+ preprocService.whenDone(ps -> {
+ cps.complete(ps);
+ });
+ }
+
+ Optional findCodeByUri(URI uri) {
+ return ProcessingAdapter.uriToPath(uri)
+ .flatMap(path -> Arrays.stream(sketch.getCode())
+ .filter(code -> code.getFile().equals(path))
+ .findFirst()
+ );
+ }
+
+ void updateProblems(List probs) {
+ Map> dias = probs.stream()
+ .map(prob -> {
+ SketchCode code = sketch.getCode(prob.getTabIndex());
+ Diagnostic dia = new Diagnostic(
+ new Range(
+ new Position(
+ prob.getLineNumber(),
+ ProcessingAdapter
+ .toLineCol(code.getProgram(), prob.getStartOffset())
+ .col - 1
+ ),
+ new Position(
+ prob.getLineNumber(),
+ ProcessingAdapter
+ .toLineCol(code.getProgram(), prob.getStopOffset())
+ .col - 1
+ )
+ ),
+ prob.getMessage()
+ );
+ dia.setSeverity(
+ prob.isError()
+ ? DiagnosticSeverity.Error
+ : DiagnosticSeverity.Warning
+ );
+ return new AbstractMap.SimpleEntry(
+ ProcessingAdapter.pathToUri(code.getFile()),
+ dia
+ );
+ })
+ .collect(Collectors.groupingBy(
+ AbstractMap.SimpleEntry::getKey,
+ Collectors.mapping(
+ AbstractMap.SimpleEntry::getValue,
+ Collectors.toList()
+ )
+ ));
+
+ for (Map.Entry> entry : dias.entrySet()) {
+ PublishDiagnosticsParams params = new PublishDiagnosticsParams();
+ params.setUri(entry.getKey().toString());
+ params.setDiagnostics(entry.getValue());
+ client.publishDiagnostics(params);
+ }
+
+ for (URI uri : prevDiagnosticReportUris) {
+ if (!dias.containsKey(uri)) {
+ PublishDiagnosticsParams params = new PublishDiagnosticsParams();
+ params.setUri(uri.toString());
+ params.setDiagnostics(Collections.emptyList());
+ client.publishDiagnostics(params);
+ }
+ }
+ prevDiagnosticReportUris = dias.keySet();
+ }
+
+ CompletionItem convertCompletionCandidate(CompletionCandidate c) {
+ CompletionItem item = new CompletionItem();
+ item.setLabel(c.getElementName());
+ item.setInsertTextFormat(InsertTextFormat.Snippet);
+ String insert = c.getCompletionString();
+ if (insert.contains("( )")) {
+ insert = insert.replace("( )", "($1)");
+ } else if (insert.contains(",")) {
+ int n = 1;
+ char[] chs = insert.replace("(,", "($1,").toCharArray();
+ insert = "";
+ for (char ch : chs) {
+ switch (ch) {
+ case ',': {
+ n += 1;
+ insert += ",$" + n;
+ }
+ default: insert += ch;
+ }
+ }
+ }
+ item.setInsertText(insert);
+ CompletionItemKind kind;
+ switch (c.getType()) {
+ case 0: // PREDEF_CLASS
+ kind = CompletionItemKind.Class;
+ break;
+ case 1: // PREDEF_FIELD
+ kind = CompletionItemKind.Constant;
+ break;
+ case 2: // PREDEF_METHOD
+ kind = CompletionItemKind.Function;
+ break;
+ case 3: // LOCAL_CLASS
+ kind = CompletionItemKind.Class;
+ break;
+ case 4: // LOCAL_METHOD
+ kind = CompletionItemKind.Method;
+ break;
+ case 5: // LOCAL_FIELD
+ kind = CompletionItemKind.Field;
+ break;
+ case 6: // LOCAL_VARIABLE
+ kind = CompletionItemKind.Variable;
+ break;
+ default:
+ throw new IllegalArgumentException("Unknown completion type: " + c.getType());
+ }
+ item.setKind(kind);
+ item.setDetail(Jsoup.parse(c.getLabel()).text());
+ return item;
+ }
+
+ Optional parsePhrase(String text) {
+ return Optional.ofNullable(JavaTextArea.parsePhrase(text));
+ }
+
+ List filterPredictions(
+ List candidates
+ ) {
+ return Collections.list(CompletionGenerator.filterPredictions(candidates).elements());
+ }
+
+ CompletableFuture> generateCompletion(
+ URI uri,
+ int line,
+ int col
+ ) {
+ return cps.thenApply(ps -> {
+ Optional> result =
+ findCodeByUri(uri)
+ .flatMap(code -> {
+ int codeIndex = IntStream.range(0, sketch.getCodeCount())
+ .filter(i -> sketch.getCode(i).equals(code))
+ .findFirst()
+ .getAsInt();
+ int lineStartOffset = String.join(
+ "\n",
+ Arrays.copyOfRange(code.getProgram().split("\n"), 0, line + 1)
+ )
+ .length();
+ int lineNumber = ps.tabOffsetToJavaLine(codeIndex, lineStartOffset);
+
+ String text = code.getProgram()
+ .split("\n")[line] // TODO: 範囲外のエラー処理
+ .substring(0, col);
+ return parsePhrase(text)
+ .map(phrase -> {
+ System.out.println("phrase: " + phrase);
+ System.out.println("lineNumber: " + lineNumber);
+ return Optional.ofNullable(
+ suggestionGenerator
+ .preparePredictions(ps, phrase, lineNumber)
+ )
+ .filter(x -> !x.isEmpty())
+ .map(candidates -> {
+ Collections.sort(candidates);
+ System.out.println("candidates: " + candidates);
+ List filtered = filterPredictions(candidates);
+ System.out.println("filtered: " + filtered);
+ return filtered.stream()
+ .map(this::convertCompletionCandidate)
+ .collect(Collectors.toList());
+ });
+ })
+ .orElse(Optional.empty());
+ });
+
+ return result.orElse(Collections.emptyList());
+ });
+ }
+
+ void onChange(URI uri, String text) {
+ findCodeByUri(uri)
+ .ifPresent(code -> {
+ code.setProgram(text);
+ notifySketchChanged();
+ });
+ }
+
+ Optional format(URI uri) {
+ return findCodeByUri(uri)
+ .map(SketchCode::getProgram)
+ .map(code -> {
+ String newCode = new AutoFormat().format(code);
+ Offset end = ProcessingAdapter.toLineCol(code, code.length());
+ return new TextEdit(
+ new Range(
+ new Position(0, 0),
+ new Position(end.line, end.col)
+ ),
+ newCode
+ );
+ });
+ }
+}
diff --git a/java/src/processing/mode/java/languageServer/ProcessingLanguageServer.java b/java/src/processing/mode/java/languageServer/ProcessingLanguageServer.java
new file mode 100644
index 000000000..e680ada48
--- /dev/null
+++ b/java/src/processing/mode/java/languageServer/ProcessingLanguageServer.java
@@ -0,0 +1,112 @@
+package processing.mode.java.languageServer;
+
+
+import org.eclipse.lsp4j.services.LanguageServer;
+import org.eclipse.lsp4j.services.TextDocumentService;
+import org.eclipse.lsp4j.services.WorkspaceService;
+import org.eclipse.lsp4j.InitializeResult;
+import org.eclipse.lsp4j.InitializeParams;
+import java.util.concurrent.CompletableFuture;
+import org.eclipse.lsp4j.ServerCapabilities;
+import org.eclipse.lsp4j.TextDocumentSyncKind;
+import org.eclipse.lsp4j.CompletionOptions;
+import org.eclipse.lsp4j.CompletionParams;
+import org.eclipse.lsp4j.CompletionItem;
+import org.eclipse.lsp4j.CompletionList;
+import org.eclipse.lsp4j.jsonrpc.CompletableFutures;
+import processing.app.Base;
+import processing.app.Platform;
+import processing.app.Console;
+import processing.app.Language;
+import processing.app.Preferences;
+import processing.app.contrib.ModeContribution;
+import processing.mode.java.JavaMode;
+import java.io.File;
+import processing.app.Sketch;
+import processing.mode.java.JavaBuild;
+import processing.mode.java.CompletionGenerator;
+import processing.mode.java.PreprocService;
+import org.eclipse.lsp4j.WorkspaceFoldersOptions;
+import org.eclipse.lsp4j.services.LanguageClientAware;
+import org.eclipse.lsp4j.services.LanguageClient;
+import processing.mode.java.ErrorChecker;
+import processing.app.Problem;
+import org.eclipse.lsp4j.PublishDiagnosticsParams;
+import org.eclipse.lsp4j.Diagnostic;
+import org.eclipse.lsp4j.Range;
+import org.eclipse.lsp4j.Position;
+import org.eclipse.lsp4j.DiagnosticSeverity;
+import java.net.URI;
+import java.util.Optional;
+import java.util.HashMap;
+import java.util.Arrays;
+
+class ProcessingLanguageServer implements LanguageServer, LanguageClientAware {
+ static Optional lowerExtension(File file) {
+ String s = file.toString();
+ int dot = s.lastIndexOf('.');
+ if (dot == -1) return Optional.empty();
+ else return Optional.of(s.substring(dot + 1).toLowerCase());
+ }
+
+ HashMap adapters = new HashMap<>();
+ LanguageClient client = null;
+ ProcessingTextDocumentService textDocumentService = new ProcessingTextDocumentService(this);
+ ProcessingWorkspaceService workspaceService = new ProcessingWorkspaceService(this);
+
+ @Override
+ public void exit() {
+ System.out.println("exit");
+ }
+
+ @Override
+ public TextDocumentService getTextDocumentService() {
+ return textDocumentService;
+ }
+
+ @Override
+ public WorkspaceService getWorkspaceService() {
+ return workspaceService;
+ }
+
+ Optional getAdapter(URI uri) {
+ return ProcessingAdapter.uriToPath(uri).filter(file -> {
+ String ext = lowerExtension(file).orElse("");
+ return ext.equals("pde") || ext.equals("java");
+ }).map(file -> {
+ File rootDir = file.getParentFile();
+ return adapters.computeIfAbsent(rootDir, _k -> new ProcessingAdapter(rootDir, client));
+ });
+ }
+
+ @Override
+ public CompletableFuture initialize(InitializeParams params) {
+ ProcessingAdapter.init();
+ System.out.println("initialize");
+ var capabilities = new ServerCapabilities();
+ capabilities.setTextDocumentSync(TextDocumentSyncKind.Full);
+
+ var completionOptions = new CompletionOptions();
+ completionOptions.setResolveProvider(true);
+ completionOptions.setTriggerCharacters(
+ Arrays.asList(
+ "."
+ )
+ );
+ capabilities.setCompletionProvider(completionOptions);
+ capabilities.setDocumentFormattingProvider(true);
+ var result = new InitializeResult(capabilities);
+ return CompletableFuture.completedFuture(result);
+ }
+
+ @Override
+ public CompletableFuture