From 580bea7fe63b08a6013856dc5e7990302901c062 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dawid=20Paku=C5=82a?= Date: Mon, 18 Sep 2023 16:00:01 +0200 Subject: [PATCH] Fix #1332 - Add autoinsert support #1333 --- org.eclipse.wildwebdeveloper/plugin.xml | 11 +- .../vue/VueLanguageServer.java | 45 ++-- .../vue/VueLanguageServerAPI.java | 42 ++++ .../vue/autoinsert/AutoInsertLastChange.java | 32 +++ .../vue/autoinsert/AutoInsertOptions.java | 27 +++ .../vue/autoinsert/AutoInsertParams.java | 60 +++++ .../vue/autoinsert/AutoInsertResponse.java | 43 ++++ .../autoinsert/VueAutoInsertReconciler.java | 210 ++++++++++++++++++ 8 files changed, 456 insertions(+), 14 deletions(-) create mode 100644 org.eclipse.wildwebdeveloper/src/org/eclipse/wildwebdeveloper/vue/VueLanguageServerAPI.java create mode 100644 org.eclipse.wildwebdeveloper/src/org/eclipse/wildwebdeveloper/vue/autoinsert/AutoInsertLastChange.java create mode 100644 org.eclipse.wildwebdeveloper/src/org/eclipse/wildwebdeveloper/vue/autoinsert/AutoInsertOptions.java create mode 100644 org.eclipse.wildwebdeveloper/src/org/eclipse/wildwebdeveloper/vue/autoinsert/AutoInsertParams.java create mode 100644 org.eclipse.wildwebdeveloper/src/org/eclipse/wildwebdeveloper/vue/autoinsert/AutoInsertResponse.java create mode 100644 org.eclipse.wildwebdeveloper/src/org/eclipse/wildwebdeveloper/vue/autoinsert/VueAutoInsertReconciler.java diff --git a/org.eclipse.wildwebdeveloper/plugin.xml b/org.eclipse.wildwebdeveloper/plugin.xml index a0d4d9bc5b..3889f89b95 100644 --- a/org.eclipse.wildwebdeveloper/plugin.xml +++ b/org.eclipse.wildwebdeveloper/plugin.xml @@ -769,7 +769,7 @@ base-type="org.eclipse.wildwebdeveloper.parent" file-extensions="vue" id="org.eclipse.wildwebdeveloper.vue" - name="VUE module" + name="VUE component" priority="normal"> @@ -777,6 +777,7 @@ @@ -819,6 +820,14 @@ + + + + + commands = new ArrayList<>(); commands.add(NodeJSManager.getNodeJsLocation().getAbsolutePath()); try { - URL url = FileLocator - .toFileURL(getClass().getResource("/node_modules/@vue/language-server/bin/vue-language-server.js")); - commands.add(new java.io.File(url.getPath()).getAbsolutePath()); + if (vuePath == null || tsserverPath == null) { + resolvePaths(); + } + commands.add(vuePath); commands.add("--stdio"); setCommands(commands); setWorkingDirectory(System.getProperty("user.dir")); @@ -47,6 +51,15 @@ public VueLanguageServer() { } } + private void resolvePaths() throws IOException { + URL url = FileLocator + .toFileURL(getClass().getResource("/node_modules/@vue/language-server/bin/vue-language-server.js")); + vuePath = new File(url.getPath()).getAbsolutePath(); + + url = FileLocator.toFileURL(getClass().getResource("/node_modules/typescript/lib")); + tsserverPath = new File(url.getPath()).getAbsolutePath(); + } + @Override protected ProcessBuilder createProcessBuilder() { ProcessBuilder builder = super.createProcessBuilder(); @@ -56,15 +69,21 @@ protected ProcessBuilder createProcessBuilder() { @Override public Object getInitializationOptions(URI rootUri) { - - try { - URL url = FileLocator.toFileURL(getClass().getResource("/node_modules/typescript/lib")); - return Collections.singletonMap("typescript", Collections.singletonMap("tsdk", new File(url.getPath()).getAbsolutePath())); - } catch (IOException e) { - Activator.getDefault().getLog().log( - new Status(IStatus.ERROR, Activator.getDefault().getBundle().getSymbolicName(), e.getMessage(), e)); - } - return null; + Map options = new HashMap<>(); + setWorkingDirectory(rootUri.getRawPath()); + + options.put("typescript", Collections.singletonMap("tsdk", tsserverPath)); + options.put("fullCompletionList", false); + options.put("serverMode", 0); + options.put("diagnosticModel", 1); + options.put("additionalExtensions", new String[] {}); + + Map legend = new HashMap<>(); + legend.put("tokenTypes", new String[] {"compontent"} ); + legend.put("tokenModifiers", new String[] {} ); + options.put("semanticTokensLegend", legend); + + return options; } @Override diff --git a/org.eclipse.wildwebdeveloper/src/org/eclipse/wildwebdeveloper/vue/VueLanguageServerAPI.java b/org.eclipse.wildwebdeveloper/src/org/eclipse/wildwebdeveloper/vue/VueLanguageServerAPI.java new file mode 100644 index 0000000000..1a9f3c3324 --- /dev/null +++ b/org.eclipse.wildwebdeveloper/src/org/eclipse/wildwebdeveloper/vue/VueLanguageServerAPI.java @@ -0,0 +1,42 @@ +/******************************************************************************* + * Copyright (c) 2023 Dawid Pakuła and others. + * + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Dawid Pakuła - initial implementation + *******************************************************************************/ +package org.eclipse.wildwebdeveloper.vue; + +import java.util.concurrent.CompletableFuture; + +import org.eclipse.lsp4j.jsonrpc.messages.Either; +import org.eclipse.lsp4j.jsonrpc.services.JsonRequest; +import org.eclipse.lsp4j.services.LanguageServer; +import org.eclipse.wildwebdeveloper.vue.autoinsert.AutoInsertParams; +import org.eclipse.wildwebdeveloper.vue.autoinsert.AutoInsertResponse; + +/** + * VUE language server API which defines custom LSP commands. + * + */ +public interface VueLanguageServerAPI extends LanguageServer { + + /** + * Auto insert custom LSP command provided by the HTML language server to + * support auto close tag and auto insert of quote for attribute. + * + * @param params auto insert parameters. + * @return the content with the auto close tag / auto insert of quote and null + * otherwise. + * + */ + @JsonRequest("volar/client/autoInsert") + CompletableFuture> autoInsert(AutoInsertParams params); + + +} diff --git a/org.eclipse.wildwebdeveloper/src/org/eclipse/wildwebdeveloper/vue/autoinsert/AutoInsertLastChange.java b/org.eclipse.wildwebdeveloper/src/org/eclipse/wildwebdeveloper/vue/autoinsert/AutoInsertLastChange.java new file mode 100644 index 0000000000..309485749b --- /dev/null +++ b/org.eclipse.wildwebdeveloper/src/org/eclipse/wildwebdeveloper/vue/autoinsert/AutoInsertLastChange.java @@ -0,0 +1,32 @@ +/******************************************************************************* + * Copyright (c) 2023 Dawid Pakuła and others. + * + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Dawid Pakuła - initial implementation + *******************************************************************************/ +package org.eclipse.wildwebdeveloper.vue.autoinsert; + +import org.eclipse.lsp4j.TextDocumentContentChangeEvent; + +/** + * VUE Auto Insert parameters. + * + */ +public class AutoInsertLastChange extends TextDocumentContentChangeEvent { + + private int rangeOffset; + + public int getRangeOffset() { + return rangeOffset; + } + + public void setRangeOffset(int rangeOffset) { + this.rangeOffset = rangeOffset; + } +} diff --git a/org.eclipse.wildwebdeveloper/src/org/eclipse/wildwebdeveloper/vue/autoinsert/AutoInsertOptions.java b/org.eclipse.wildwebdeveloper/src/org/eclipse/wildwebdeveloper/vue/autoinsert/AutoInsertOptions.java new file mode 100644 index 0000000000..98cc064aeb --- /dev/null +++ b/org.eclipse.wildwebdeveloper/src/org/eclipse/wildwebdeveloper/vue/autoinsert/AutoInsertOptions.java @@ -0,0 +1,27 @@ +/******************************************************************************* + * Copyright (c) 2023 Dawid Pakuła and others. + * + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Dawid Pakuła - initial implementation + *******************************************************************************/ +package org.eclipse.wildwebdeveloper.vue.autoinsert; + +public class AutoInsertOptions { + + private AutoInsertLastChange lastChange; + + public AutoInsertLastChange getLastChange() { + return lastChange; + } + + public void setLastChange(AutoInsertLastChange lastChange) { + this.lastChange = lastChange; + } + +} diff --git a/org.eclipse.wildwebdeveloper/src/org/eclipse/wildwebdeveloper/vue/autoinsert/AutoInsertParams.java b/org.eclipse.wildwebdeveloper/src/org/eclipse/wildwebdeveloper/vue/autoinsert/AutoInsertParams.java new file mode 100644 index 0000000000..1831d013f7 --- /dev/null +++ b/org.eclipse.wildwebdeveloper/src/org/eclipse/wildwebdeveloper/vue/autoinsert/AutoInsertParams.java @@ -0,0 +1,60 @@ +/******************************************************************************* + * Copyright (c) 2023 Dawid Pakuła and others. + * + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Dawid Pakuła - initial implementation + *******************************************************************************/ +package org.eclipse.wildwebdeveloper.vue.autoinsert; + +import org.eclipse.lsp4j.Position; +import org.eclipse.lsp4j.TextDocumentIdentifier; + +/** + * VUE Auto Insert parameters. + * + */ +public class AutoInsertParams { + + /** + * The text document. + */ + private TextDocumentIdentifier textDocument; + /** + * The position inside the text document. + */ + private Position position; + + private AutoInsertOptions options; + + + public TextDocumentIdentifier getTextDocument() { + return textDocument; + } + + public void setTextDocument(TextDocumentIdentifier textDocument) { + this.textDocument = textDocument; + } + + public Position getPosition() { + return position; + } + + public void setPosition(Position position) { + this.position = position; + } + + public AutoInsertOptions getOptions() { + return options; + } + + public void setOptions(AutoInsertOptions options) { + this.options = options; + } + +} diff --git a/org.eclipse.wildwebdeveloper/src/org/eclipse/wildwebdeveloper/vue/autoinsert/AutoInsertResponse.java b/org.eclipse.wildwebdeveloper/src/org/eclipse/wildwebdeveloper/vue/autoinsert/AutoInsertResponse.java new file mode 100644 index 0000000000..0670602fd4 --- /dev/null +++ b/org.eclipse.wildwebdeveloper/src/org/eclipse/wildwebdeveloper/vue/autoinsert/AutoInsertResponse.java @@ -0,0 +1,43 @@ +/******************************************************************************* + * Copyright (c) 2023 Dawid Pakuła and others. + * + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Dawid Pakuła - initial implementation + *******************************************************************************/ +package org.eclipse.wildwebdeveloper.vue.autoinsert; + +import org.eclipse.lsp4j.Range; + +/** + * VUE Auto Insert parameters. + * + */ +public class AutoInsertResponse { + + private String newText; + + private Range range; + + public String getNewText() { + return newText; + } + + public void setNewText(String newText) { + this.newText = newText; + } + + public Range getRange() { + return range; + } + + public void setRange(Range range) { + this.range = range; + } + +} diff --git a/org.eclipse.wildwebdeveloper/src/org/eclipse/wildwebdeveloper/vue/autoinsert/VueAutoInsertReconciler.java b/org.eclipse.wildwebdeveloper/src/org/eclipse/wildwebdeveloper/vue/autoinsert/VueAutoInsertReconciler.java new file mode 100644 index 0000000000..790687f635 --- /dev/null +++ b/org.eclipse.wildwebdeveloper/src/org/eclipse/wildwebdeveloper/vue/autoinsert/VueAutoInsertReconciler.java @@ -0,0 +1,210 @@ +/******************************************************************************* + * Copyright (c) 2022, 2023 Red Hat Inc. and others. + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Angelo ZERR (Red Hat Inc.) - initial implementation + * Dawid Pakuła - modified copy for Vue + *******************************************************************************/ +package org.eclipse.wildwebdeveloper.vue.autoinsert; + +import java.net.URI; +import java.util.concurrent.CompletableFuture; +import java.util.function.Function; + +import org.eclipse.jface.text.BadLocationException; +import org.eclipse.jface.text.DocumentEvent; +import org.eclipse.jface.text.IDocument; +import org.eclipse.jface.text.IDocumentListener; +import org.eclipse.jface.text.ITextInputListener; +import org.eclipse.jface.text.ITextViewer; +import org.eclipse.jface.text.reconciler.IReconciler; +import org.eclipse.jface.text.reconciler.IReconcilingStrategy; +import org.eclipse.lsp4e.LSPEclipseUtils; +import org.eclipse.lsp4e.LanguageServers; +import org.eclipse.lsp4j.Range; +import org.eclipse.lsp4j.TextDocumentIdentifier; +import org.eclipse.swt.widgets.Display; +import org.eclipse.wildwebdeveloper.Activator; +import org.eclipse.wildwebdeveloper.vue.VueLanguageServerAPI; + +/** + * {@link IReconciler} implementation used to support auto close tags / auto + * insert quote, features provides by the vscode VUE language server with the + * custom 'volar/client/autoInsert' LSP request. + * + */ +public class VueAutoInsertReconciler implements IReconciler { + + private IDocument document; + + private ITextViewer viewer; + + private Listener listener; + + private void autoInsert(DocumentEvent event) { + if (event == null || viewer == null) { + return; + } + IDocument document = event.getDocument(); + if (document == null || event == null || event.getLength() != 0) { + return; + } + + int offset = event.getOffset(); + URI uri = LSPEclipseUtils.toUri(document); + if (uri == null) { + return; + } + + + TextDocumentIdentifier identifier = new TextDocumentIdentifier(uri.toString()); + + LanguageServers.forDocument(document).collectAll((w, ls) -> CompletableFuture.completedFuture(ls)) + .thenAccept(lss -> lss.stream().filter(VueLanguageServerAPI.class::isInstance) + .map(VueLanguageServerAPI.class::cast).findAny().ifPresent(info -> { + // The document is bound with HTML language server, consumes the html/autoInsert + final Display display = viewer.getTextWidget().getDisplay(); + CompletableFuture.supplyAsync(() -> { + try { + // Wait for textDocument/didChange + Thread.sleep(100); + } catch (InterruptedException ex) { + Thread.interrupted(); + } + try { + + AutoInsertParams params = new AutoInsertParams(); + params.setTextDocument(identifier); + params.setPosition(LSPEclipseUtils.toPosition(offset + event.getText().length(), document)); + + AutoInsertOptions opts = new AutoInsertOptions(); + AutoInsertLastChange changeEvent = new AutoInsertLastChange(); + final var range = new Range(LSPEclipseUtils.toPosition(offset, document), + LSPEclipseUtils.toPosition(offset + event.fLength, document)); + changeEvent.setRange(range); + changeEvent.setText(event.getText()); + changeEvent.setRangeLength(event.fLength); + changeEvent.setRangeOffset(offset); + opts.setLastChange(changeEvent); + params.setOptions(opts); + + // consumes String or AutoInsertResponse from Vue Server + info.autoInsert(params) + .thenAccept(response -> { + if (response != null) { + display.asyncExec(() -> { + try { + // we receive a text like + // $0 + // $0 should be used for set the cursor. + String newText = response.map(Function.identity(), AutoInsertResponse::getNewText); + + String text = newText.replace("$0", "").replace("$1", ""); + + + int index = newText.indexOf("$0"); + + int replaceLength = 0; + int replacePosition = offset + event.getText().length(); + if (response.isRight()) { + replacePosition = LSPEclipseUtils.toOffset(response.getRight().getRange().getStart(), document); + } + document.replace(replacePosition, replaceLength, text); + if (index != -1) { + viewer.setSelectedRange(replacePosition + index, 0); + } + // viewer.setSelectedRange(offset, c) + } catch (BadLocationException e) { + Activator.getDefault().getLog().error(e.getMessage(), e); + } + }); + + } + }); + } catch (BadLocationException e) { + // Do nothing + Activator.getDefault().getLog().error(e.getMessage(), e); + } + return null; + }); + })); + } + + /** + * Internal document listener and text input listener. + */ + class Listener implements IDocumentListener, ITextInputListener { + + /* + * @see IDocumentListener#documentAboutToBeChanged(DocumentEvent) + */ + @Override + public void documentAboutToBeChanged(DocumentEvent e) { + } + + /* + * @see IDocumentListener#documentChanged(DocumentEvent) + */ + @Override + public void documentChanged(DocumentEvent e) { + System.out.println(e.toString()); + autoInsert(e); + } + + /* + * @see ITextInputListener#inputDocumentAboutToBeChanged(IDocument, IDocument) + */ + @Override + public void inputDocumentAboutToBeChanged(IDocument oldInput, IDocument newInput) { + if (oldInput == document) { + if (document != null) { + document.removeDocumentListener(this); + } + document = null; + } + } + + /* + * @see ITextInputListener#inputDocumentChanged(IDocument, IDocument) + */ + @Override + public void inputDocumentChanged(IDocument oldInput, IDocument newInput) { + document = newInput; + if (document == null) { + return; + } + document.addDocumentListener(this); + } + + } + + @Override + public void install(ITextViewer viewer) { + this.viewer = viewer; + listener = new Listener(); + viewer.addTextInputListener(listener); + } + + @Override + public void uninstall() { + if (listener != null) { + viewer.removeTextInputListener(listener); + if (document != null) { + document.removeDocumentListener(listener); + } + listener = null; + } + this.viewer = null; + } + + @Override + public IReconcilingStrategy getReconcilingStrategy(String contentType) { + return null; + } + +}