From ac062a502dec646dd2f332ea12c32efc01064b1c Mon Sep 17 00:00:00 2001 From: kgtkr Date: Wed, 28 Sep 2022 23:47:44 +0900 Subject: [PATCH] Implementation language server --- app/build.xml | 2 +- app/processing4-app.iml | 2 +- build/build.xml | 5 + java/.gitignore | 1 + java/build.xml | 41 ++- java/ivy.xml | 7 + java/lib/.gitignore | 1 + .../mode/java/CompletionGenerator.java | 2 +- .../processing/mode/java/ErrorChecker.java | 10 +- java/src/processing/mode/java/JavaEditor.java | 4 +- .../processing/mode/java/JavaTextArea.java | 2 +- .../processing/mode/java/PreprocService.java | 15 +- .../mode/java/languageServer/App.java | 26 ++ .../languageServer/ProcessingAdapter.java | 346 ++++++++++++++++++ .../ProcessingLanguageServer.java | 112 ++++++ .../ProcessingTextDocumentService.java | 104 ++++++ .../ProcessingWorkspaceService.java | 53 +++ 17 files changed, 712 insertions(+), 21 deletions(-) create mode 100644 java/ivy.xml create mode 100644 java/lib/.gitignore create mode 100644 java/src/processing/mode/java/languageServer/App.java create mode 100644 java/src/processing/mode/java/languageServer/ProcessingAdapter.java create mode 100644 java/src/processing/mode/java/languageServer/ProcessingLanguageServer.java create mode 100644 java/src/processing/mode/java/languageServer/ProcessingTextDocumentService.java create mode 100644 java/src/processing/mode/java/languageServer/ProcessingWorkspaceService.java diff --git a/app/build.xml b/app/build.xml index d88c13dd15..dcf718f026 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 62e9a8c3a8..e2df2c5b2e 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 2dd0bb2628..639f39b86b 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 0000000000..68bc5a354e --- /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 0000000000..d392f0e82c --- /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 e5c1278724..5a23816e9c 100644 --- a/java/src/processing/mode/java/CompletionGenerator.java +++ b/java/src/processing/mode/java/CompletionGenerator.java @@ -1762,7 +1762,7 @@ public List preparePredictions(final PreprocSketch ps, } - 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 44f9f1944e..f302b50788 100644 --- a/java/src/processing/mode/java/ErrorChecker.java +++ b/java/src/processing/mode/java/ErrorChecker.java @@ -28,7 +28,7 @@ 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 @@ public void preferencesChanged() { pps.registerListener(errorHandlerListener); } else { pps.unregisterListener(errorHandlerListener); - editor.setProblemList(Collections.emptyList()); + editor.accept(Collections.emptyList()); nextUiUpdate = 0; } } @@ -136,7 +136,7 @@ private void handleSketchProblems(PreprocSketch ps) { 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 efb37b1802..cc1feb7400 100644 --- a/java/src/processing/mode/java/JavaEditor.java +++ b/java/src/processing/mode/java/JavaEditor.java @@ -129,7 +129,7 @@ protected JavaEditor(Base base, String path, EditorState state, box.add(textAndError); */ - preprocService = new PreprocService(this); + preprocService = new PreprocService(this.jmode, this.sketch); // long t5 = System.currentTimeMillis(); @@ -141,7 +141,7 @@ protected JavaEditor(Base base, String path, EditorState state, 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 1dedbf3fce..ecab00eed1 100644 --- a/java/src/processing/mode/java/JavaTextArea.java +++ b/java/src/processing/mode/java/JavaTextArea.java @@ -341,7 +341,7 @@ protected void fetchPhrase() { } - 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 4d19f7a64a..8750720f7b 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 @@ private PreprocSketch preprocessSketch(PreprocSketch prevResult) { 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 @@ private PreprocSketch preprocessSketch(PreprocSketch prevResult) { // 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 @@ private PreprocSketch preprocessSketch(PreprocSketch prevResult) { 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 0000000000..bc884c4050 --- /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 0000000000..44cd10a004 --- /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 0000000000..e680ada48e --- /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 shutdown() { + System.out.println("shutdown"); + return CompletableFuture.completedFuture(null); + } + + @Override + public void connect(LanguageClient client) { + this.client = client; + } +} diff --git a/java/src/processing/mode/java/languageServer/ProcessingTextDocumentService.java b/java/src/processing/mode/java/languageServer/ProcessingTextDocumentService.java new file mode 100644 index 0000000000..9709204dac --- /dev/null +++ b/java/src/processing/mode/java/languageServer/ProcessingTextDocumentService.java @@ -0,0 +1,104 @@ +package processing.mode.java.languageServer; + +import org.eclipse.lsp4j.services.TextDocumentService; +import org.eclipse.lsp4j.CompletionParams; +import java.util.concurrent.CompletableFuture; +import org.eclipse.lsp4j.jsonrpc.messages.Either; +import java.util.List; +import org.eclipse.lsp4j.CompletionItem; +import org.eclipse.lsp4j.CompletionList; +import org.eclipse.lsp4j.jsonrpc.CompletableFutures; +import org.eclipse.lsp4j.DidOpenTextDocumentParams; +import org.eclipse.lsp4j.DidSaveTextDocumentParams; +import org.eclipse.lsp4j.DidCloseTextDocumentParams; +import org.eclipse.lsp4j.DidChangeTextDocumentParams; +import org.eclipse.lsp4j.DocumentFormattingParams; +import org.eclipse.lsp4j.TextEdit; +import java.io.File; +import processing.mode.java.AutoFormat; +import org.eclipse.lsp4j.Position; +import org.eclipse.lsp4j.Range; +import java.util.Collections; +import processing.mode.java.CompletionGenerator; +import processing.mode.java.JavaTextArea; +import java.util.Arrays; +import processing.mode.java.CompletionCandidate; +import javax.swing.DefaultListModel; +import org.eclipse.lsp4j.CompletionItemKind; +import org.eclipse.lsp4j.MarkupContent; +import org.eclipse.lsp4j.MarkupKind; +import org.jsoup.Jsoup; +import org.eclipse.lsp4j.InsertTextFormat; +import java.net.URI; + +class ProcessingTextDocumentService implements TextDocumentService { + ProcessingLanguageServer pls; + ProcessingTextDocumentService(ProcessingLanguageServer pls) { + this.pls = pls; + } + + @Override + public void didChange(DidChangeTextDocumentParams params) { + System.out.println("didChange"); + URI uri = URI.create(params.getTextDocument().getUri()); + pls.getAdapter(uri).ifPresent(adapter -> { + var change = params.getContentChanges().get(0); + adapter.onChange(uri, change.getText()); + }); + } + + @Override + public void didClose(DidCloseTextDocumentParams params) { + System.out.println("didClose"); + } + + @Override + public void didOpen(DidOpenTextDocumentParams params) { + System.out.println("didOpen"); + URI uri = URI.create(params.getTextDocument().getUri()); + pls.getAdapter(uri).ifPresent(adapter -> { + adapter.onChange(uri, params.getTextDocument().getText()); + }); + } + + @Override + public void didSave(DidSaveTextDocumentParams params) { + System.out.println("didSave"); + } + + @Override + public CompletableFuture, CompletionList>> completion(CompletionParams params) { + System.out.println("completion"); + URI uri = URI.create(params.getTextDocument().getUri()); + return pls.getAdapter(uri).map(adapter -> { + CompletableFuture, CompletionList>> result = adapter.generateCompletion( + uri, + params.getPosition().getLine(), + params.getPosition().getCharacter() + ).thenApply(Either::forLeft); + return result; + }) + .orElse(CompletableFutures.computeAsync(_x -> Either.forLeft(Collections.emptyList()))); + } + + @Override + public CompletableFuture resolveCompletionItem(CompletionItem params) { + System.out.println("resolveCompletionItem"); + return CompletableFutures.computeAsync(_x -> { + return params; + }); + } + + @Override + public CompletableFuture> formatting(DocumentFormattingParams params) { + System.out.println("formatting"); + URI uri = URI.create(params.getTextDocument().getUri()); + return pls.getAdapter(uri).map(adapter -> { + CompletableFuture> result = CompletableFutures.computeAsync(_x -> { + return adapter.format(uri).map(Collections::singletonList).orElse(Collections.emptyList()); + }); + return result; + }) + .orElse(CompletableFuture.completedFuture(Collections.emptyList())); + } +} diff --git a/java/src/processing/mode/java/languageServer/ProcessingWorkspaceService.java b/java/src/processing/mode/java/languageServer/ProcessingWorkspaceService.java new file mode 100644 index 0000000000..e47b7ea9a7 --- /dev/null +++ b/java/src/processing/mode/java/languageServer/ProcessingWorkspaceService.java @@ -0,0 +1,53 @@ +package processing.mode.java.languageServer; + +import org.eclipse.lsp4j.services.WorkspaceService; +import org.eclipse.lsp4j.DidChangeConfigurationParams; +import org.eclipse.lsp4j.DidChangeWatchedFilesParams; +import org.eclipse.lsp4j.FileChangeType; +import java.net.URI; +import java.io.IOException; + +class ProcessingWorkspaceService implements WorkspaceService { + ProcessingLanguageServer pls; + ProcessingWorkspaceService(ProcessingLanguageServer pls) { + this.pls = pls; + } + + @Override + public void didChangeConfiguration(DidChangeConfigurationParams params) { + } + + @Override + public void didChangeWatchedFiles(DidChangeWatchedFilesParams params) { + System.out.println("didChangeWatchedFiles: " + params); + for (var change : params.getChanges()) { + URI uri = URI.create(change.getUri()); + pls.getAdapter(uri).ifPresent(adapter -> { + switch (change.getType()) { + case Created: + ProcessingAdapter.uriToPath(uri).ifPresent(path -> { + adapter.sketch.loadNewTab(path.getName().toString(), "pde", true); + adapter.notifySketchChanged(); + }); + break; + case Changed: + adapter.findCodeByUri(uri).ifPresent(code -> { + try { + code.load(); + } catch (IOException e) { + throw new RuntimeException(e); + } + adapter.notifySketchChanged(); + }); + break; + case Deleted: + adapter.findCodeByUri(uri).ifPresent(code -> { + adapter.sketch.removeCode(code); + adapter.notifySketchChanged(); + }); + break; + } + }); + } + } +}