diff --git a/headless-services/commons/commons-language-server/src/main/java/org/springframework/ide/vscode/commons/languageserver/STS4LanguageClient.java b/headless-services/commons/commons-language-server/src/main/java/org/springframework/ide/vscode/commons/languageserver/STS4LanguageClient.java index 5c9bec553..4830dff83 100644 --- a/headless-services/commons/commons-language-server/src/main/java/org/springframework/ide/vscode/commons/languageserver/STS4LanguageClient.java +++ b/headless-services/commons/commons-language-server/src/main/java/org/springframework/ide/vscode/commons/languageserver/STS4LanguageClient.java @@ -11,8 +11,12 @@ package org.springframework.ide.vscode.commons.languageserver; +import java.util.concurrent.CompletableFuture; + import org.eclipse.lsp4j.jsonrpc.services.JsonNotification; +import org.eclipse.lsp4j.jsonrpc.services.JsonRequest; import org.eclipse.lsp4j.services.LanguageClient; +import org.springframework.ide.vscode.commons.languageserver.quickfix.QuickfixEdit.CursorMovement; /** * Some 'custom' extensions to standard LSP {@link LanguageClient}. @@ -24,4 +28,7 @@ public interface STS4LanguageClient extends LanguageClient { @JsonNotification("sts/progress") void progress(ProgressParams progressEvent); + @JsonRequest("sts/moveCursor") + CompletableFuture moveCursor(CursorMovement cursorMovement); + } diff --git a/headless-services/commons/commons-language-server/src/main/java/org/springframework/ide/vscode/commons/languageserver/quickfix/QuickfixEdit.java b/headless-services/commons/commons-language-server/src/main/java/org/springframework/ide/vscode/commons/languageserver/quickfix/QuickfixEdit.java new file mode 100644 index 000000000..85e3ed852 --- /dev/null +++ b/headless-services/commons/commons-language-server/src/main/java/org/springframework/ide/vscode/commons/languageserver/quickfix/QuickfixEdit.java @@ -0,0 +1,55 @@ +/******************************************************************************* + * Copyright (c) 2017 Pivotal, Inc. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Pivotal, Inc. - initial API and implementation + *******************************************************************************/ +package org.springframework.ide.vscode.commons.languageserver.quickfix; + +import org.eclipse.lsp4j.Position; +import org.eclipse.lsp4j.WorkspaceEdit; + +public class QuickfixEdit { + + public static class CursorMovement { + private String uri; + private Position position; + + public CursorMovement() { + } + + public CursorMovement(String uri, Position position) { + this.uri = uri; + this.position = position; + } + + public String getUri() { + return uri; + } + + public void setUri(String uri) { + this.uri = uri; + } + + public Position getPosition() { + return position; + } + + public void setPosition(Position position) { + this.position = position; + } + + } + + public final WorkspaceEdit workspaceEdit; + public final CursorMovement cursorMovement; + + public QuickfixEdit(WorkspaceEdit workspaceEdit, CursorMovement cursorMovement) { + this.workspaceEdit = workspaceEdit; + this.cursorMovement = cursorMovement; + } +} diff --git a/headless-services/commons/commons-language-server/src/main/java/org/springframework/ide/vscode/commons/languageserver/quickfix/QuickfixHandler.java b/headless-services/commons/commons-language-server/src/main/java/org/springframework/ide/vscode/commons/languageserver/quickfix/QuickfixHandler.java index 634df1fc1..f0b534ffe 100644 --- a/headless-services/commons/commons-language-server/src/main/java/org/springframework/ide/vscode/commons/languageserver/quickfix/QuickfixHandler.java +++ b/headless-services/commons/commons-language-server/src/main/java/org/springframework/ide/vscode/commons/languageserver/quickfix/QuickfixHandler.java @@ -10,9 +10,7 @@ *******************************************************************************/ package org.springframework.ide.vscode.commons.languageserver.quickfix; -import org.eclipse.lsp4j.WorkspaceEdit; - @FunctionalInterface public interface QuickfixHandler { - WorkspaceEdit createEdits(Object params); + QuickfixEdit createEdits(Object params); } diff --git a/headless-services/commons/commons-language-server/src/main/java/org/springframework/ide/vscode/commons/languageserver/quickfix/QuickfixRegistry.java b/headless-services/commons/commons-language-server/src/main/java/org/springframework/ide/vscode/commons/languageserver/quickfix/QuickfixRegistry.java index c879ea66a..52bd9a2ad 100644 --- a/headless-services/commons/commons-language-server/src/main/java/org/springframework/ide/vscode/commons/languageserver/quickfix/QuickfixRegistry.java +++ b/headless-services/commons/commons-language-server/src/main/java/org/springframework/ide/vscode/commons/languageserver/quickfix/QuickfixRegistry.java @@ -13,7 +13,6 @@ package org.springframework.ide.vscode.commons.languageserver.quickfix; import java.util.HashMap; import java.util.Map; -import org.eclipse.lsp4j.WorkspaceEdit; import org.springframework.ide.vscode.commons.languageserver.util.SimpleLanguageServer; import org.springframework.ide.vscode.commons.util.Assert; @@ -40,7 +39,7 @@ public class QuickfixRegistry { return new QuickfixType() { @Override - public WorkspaceEdit createEdits(Object params) { + public QuickfixEdit createEdits(Object params) { return handler.createEdits(params); } @@ -51,7 +50,7 @@ public class QuickfixRegistry { }; } - public Mono handle(QuickfixResolveParams params) { + public Mono handle(QuickfixResolveParams params) { QuickfixHandler handler = registry.get(params.getType()); return Mono.fromSupplier(() -> { return handler.createEdits(params.getParams()); diff --git a/headless-services/commons/commons-language-server/src/main/java/org/springframework/ide/vscode/commons/languageserver/util/SimpleLanguageServer.java b/headless-services/commons/commons-language-server/src/main/java/org/springframework/ide/vscode/commons/languageserver/util/SimpleLanguageServer.java index bb0d81781..ec037a7cb 100644 --- a/headless-services/commons/commons-language-server/src/main/java/org/springframework/ide/vscode/commons/languageserver/util/SimpleLanguageServer.java +++ b/headless-services/commons/commons-language-server/src/main/java/org/springframework/ide/vscode/commons/languageserver/util/SimpleLanguageServer.java @@ -18,6 +18,7 @@ import java.util.concurrent.Callable; import java.util.concurrent.CompletableFuture; import org.eclipse.lsp4j.ApplyWorkspaceEditParams; +import org.eclipse.lsp4j.ApplyWorkspaceEditResponse; import org.eclipse.lsp4j.CompletionOptions; import org.eclipse.lsp4j.Diagnostic; import org.eclipse.lsp4j.DiagnosticSeverity; @@ -31,7 +32,6 @@ import org.eclipse.lsp4j.Range; import org.eclipse.lsp4j.ServerCapabilities; import org.eclipse.lsp4j.TextDocumentIdentifier; import org.eclipse.lsp4j.TextDocumentSyncKind; -import org.eclipse.lsp4j.WorkspaceEdit; import org.eclipse.lsp4j.services.LanguageClient; import org.eclipse.lsp4j.services.LanguageClientAware; import org.eclipse.lsp4j.services.LanguageServer; @@ -40,6 +40,7 @@ import org.springframework.ide.vscode.commons.languageserver.ProgressService; import org.springframework.ide.vscode.commons.languageserver.STS4LanguageClient; import org.springframework.ide.vscode.commons.languageserver.quickfix.Quickfix; import org.springframework.ide.vscode.commons.languageserver.quickfix.Quickfix.QuickfixData; +import org.springframework.ide.vscode.commons.languageserver.quickfix.QuickfixEdit; import org.springframework.ide.vscode.commons.languageserver.quickfix.QuickfixRegistry; import org.springframework.ide.vscode.commons.languageserver.quickfix.QuickfixResolveParams; import org.springframework.ide.vscode.commons.languageserver.reconcile.IProblemCollector; @@ -124,15 +125,20 @@ public abstract class SimpleLanguageServer implements LanguageServer, LanguageCl (String)params.getArguments().get(0), params.getArguments().get(1) ); return quickfixResolve(quickfixParams) - .then((WorkspaceEdit edit) -> Mono.fromFuture(client.applyEdit(new ApplyWorkspaceEditParams(edit)))) - .map(r -> (Object)r.getApplied()) - .toFuture(); + .then((QuickfixEdit edit) -> { + Mono applyEdit = Mono.fromFuture(client.applyEdit(new ApplyWorkspaceEditParams(edit.workspaceEdit))); + Mono moveCursor = edit.cursorMovement==null + ? Mono.just(new ApplyWorkspaceEditResponse(true)) + : Mono.fromFuture(client.moveCursor(edit.cursorMovement)); + return applyEdit.then(r -> r.getApplied() ? moveCursor : Mono.just(new ApplyWorkspaceEditResponse(true))); + }) + .toFuture(); } Log.warn("Unknown command ignored: "+params.getCommand()); return CompletableFuture.completedFuture(false); } - public Mono quickfixResolve(QuickfixResolveParams params) { + public Mono quickfixResolve(QuickfixResolveParams params) { QuickfixRegistry quickfixes = getQuickfixRegistry(); return quickfixes.handle(params); } diff --git a/headless-services/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/quickfix/YamlQuickfixes.java b/headless-services/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/quickfix/YamlQuickfixes.java index b4d15c6fc..9fa63297d 100644 --- a/headless-services/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/quickfix/YamlQuickfixes.java +++ b/headless-services/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/quickfix/YamlQuickfixes.java @@ -10,13 +10,16 @@ *******************************************************************************/ package org.springframework.ide.vscode.commons.yaml.quickfix; +import org.eclipse.lsp4j.Position; import org.eclipse.lsp4j.TextEdit; import org.eclipse.lsp4j.WorkspaceEdit; import org.springframework.ide.vscode.commons.languageserver.completion.DocumentEdits.TextReplace; +import org.springframework.ide.vscode.commons.languageserver.quickfix.QuickfixEdit; import org.springframework.ide.vscode.commons.languageserver.quickfix.QuickfixRegistry; import org.springframework.ide.vscode.commons.languageserver.quickfix.QuickfixType; import org.springframework.ide.vscode.commons.languageserver.util.SimpleTextDocumentService; import org.springframework.ide.vscode.commons.util.Log; +import org.springframework.ide.vscode.commons.util.text.IRegion; import org.springframework.ide.vscode.commons.util.text.TextDocument; import org.springframework.ide.vscode.commons.yaml.completion.YamlPathEdits; import org.springframework.ide.vscode.commons.yaml.path.YamlPath; @@ -32,8 +35,14 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; +import org.springframework.ide.vscode.commons.languageserver.quickfix.QuickfixEdit.CursorMovement; + public class YamlQuickfixes { + private static final QuickfixEdit NULL_FIX = new QuickfixEdit( + new WorkspaceEdit(ImmutableMap.of(), null), + null + ); public final QuickfixType MISSING_PROP_FIX; public final QuickfixType SIMPLE_TEXT_EDIT; @@ -53,6 +62,7 @@ public class YamlQuickfixes { SChildBearingNode target = (SChildBearingNode) _target; for (String prop : params.getProps()) { edits.createPath(target, new YamlPath(YamlPathSegment.valueAt(prop)), " "); + edits.freezeCursor(); } TextReplace replaceEdit = edits.asReplacement(_doc); if (replaceEdit!=null) { @@ -61,7 +71,8 @@ public class YamlQuickfixes { params.getUri(), ImmutableList.of(new TextEdit(_doc.toRange(replaceEdit.getRegion()), replaceEdit.newText)) )); - return wsEdits; + Position newCursor = getCursorPostionAfter(_doc, edits); + return new QuickfixEdit(wsEdits, newCursor==null ? null : new CursorMovement(params.getUri(), newCursor)); } } } @@ -70,7 +81,7 @@ public class YamlQuickfixes { Log.log(e); } //Something went wrong. Return empty edit object. - return new WorkspaceEdit(ImmutableMap.of(), null); + return NULL_FIX; }); SIMPLE_TEXT_EDIT = r.register("SIMPLE_TEXT_EDIT", (_params) -> { @@ -78,9 +89,12 @@ public class YamlQuickfixes { ReplaceStringData params = new ObjectMapper().convertValue(_params, ReplaceStringData.class); TextDocument _doc = textDocumentService.getDocument(params.getUri()); if (_doc!=null) { - return new WorkspaceEdit( + return new QuickfixEdit( + new WorkspaceEdit( ImmutableMap.of(params.getUri(), ImmutableList.of(params.getEdit())), null + ), + null //TODO: compute end of the range after applying the edit ); } } catch (Exception e) { @@ -88,8 +102,26 @@ public class YamlQuickfixes { } //Something went wrong. Return empty edit object. //Something went wrong. Return empty edit object. - return new WorkspaceEdit(ImmutableMap.of(), null); + return NULL_FIX; }); } + private Position getCursorPostionAfter(TextDocument _doc, YamlPathEdits edits) { + try { + IRegion newSelection = edits.getSelection(); + if (newSelection!=null) { + //There is probably a more efficient way to compute the new cursor position. But its tricky... + //... because we need to compute line/char coordinate, in terms of lines in the *new* document. + //So we have to take into account how newlines have been inserted or shifted around by the edits. + //Doing that without actually applying the edits is... difficult. + TextDocument doc = _doc.copy(); + edits.apply(doc); + return doc.toPosition(newSelection.getOffset()); + } + } catch (Exception e) { + Log.log(e); + } + return null; + } + } diff --git a/headless-services/commons/language-server-test-harness/src/main/java/org/springframework/ide/vscode/languageserver/testharness/Editor.java b/headless-services/commons/language-server-test-harness/src/main/java/org/springframework/ide/vscode/languageserver/testharness/Editor.java index 3c712d013..37ee1527e 100644 --- a/headless-services/commons/language-server-test-harness/src/main/java/org/springframework/ide/vscode/languageserver/testharness/Editor.java +++ b/headless-services/commons/language-server-test-harness/src/main/java/org/springframework/ide/vscode/languageserver/testharness/Editor.java @@ -741,4 +741,8 @@ public class Editor { return harness.getDocumentSymbols(this.doc); } + public void setCursor(Position position) { + this.selectionStart = this.selectionEnd = doc.toOffset(position); + } + } diff --git a/headless-services/commons/language-server-test-harness/src/main/java/org/springframework/ide/vscode/languageserver/testharness/LanguageServerHarness.java b/headless-services/commons/language-server-test-harness/src/main/java/org/springframework/ide/vscode/languageserver/testharness/LanguageServerHarness.java index 65038befc..1c219cf38 100644 --- a/headless-services/commons/language-server-test-harness/src/main/java/org/springframework/ide/vscode/languageserver/testharness/LanguageServerHarness.java +++ b/headless-services/commons/language-server-test-harness/src/main/java/org/springframework/ide/vscode/languageserver/testharness/LanguageServerHarness.java @@ -75,6 +75,7 @@ import org.eclipse.lsp4j.services.LanguageClientAware; import org.springframework.ide.vscode.commons.languageserver.ProgressParams; import org.springframework.ide.vscode.commons.languageserver.STS4LanguageClient; import org.springframework.ide.vscode.commons.languageserver.completion.DocumentEdits; +import org.springframework.ide.vscode.commons.languageserver.quickfix.QuickfixEdit.CursorMovement; import org.springframework.ide.vscode.commons.languageserver.util.LanguageServerTestListener; import org.springframework.ide.vscode.commons.languageserver.util.SimpleLanguageServer; import org.springframework.ide.vscode.commons.util.Assert; @@ -229,6 +230,17 @@ public class LanguageServerHarness { // TODO Auto-generated method stub } + + @Override + public CompletableFuture moveCursor(CursorMovement cursorMovement) { + for (Editor editor : activeEditors) { + if (editor.getUri().equals(cursorMovement.getUri())) { + editor.setCursor(cursorMovement.getPosition()); + return CompletableFuture.completedFuture(new ApplyWorkspaceEditResponse(true)); + } + } + return CompletableFuture.completedFuture(new ApplyWorkspaceEditResponse(false)); + } }); } diff --git a/headless-services/concourse-language-server/src/test/java/org/springframework/ide/vscode/concourse/ConcourseEditorTest.java b/headless-services/concourse-language-server/src/test/java/org/springframework/ide/vscode/concourse/ConcourseEditorTest.java index ee0967cec..5792ca42c 100644 --- a/headless-services/concourse-language-server/src/test/java/org/springframework/ide/vscode/concourse/ConcourseEditorTest.java +++ b/headless-services/concourse-language-server/src/test/java/org/springframework/ide/vscode/concourse/ConcourseEditorTest.java @@ -58,16 +58,41 @@ public class ConcourseEditorTest { assertEquals("Add property 'type'", quickfix.getLabel()); quickfix.perform(); - editor.assertRawText( + editor.assertText( "resources:\n" + "- name: foo\n" + " source:\n" + " username: someone\n" + - " type: \n" + + " type: <*>\n" + "# Confuse" ); } + @Test public void addMultipleRequiredPropertiesQuickfix() throws Exception { + Editor editor = harness.newEditor( + "resources:\n" + + "- name: foo\n" + + " type: pool\n" + + " source:\n" + + " username: someone\n" + ); + Diagnostic problem = editor.assertProblems("source|[branch, pool, uri] are required").get(0); + CodeAction quickfix = editor.assertCodeAction(problem); + assertEquals("Add properties: [branch, pool, uri]", quickfix.getLabel()); + quickfix.perform(); + + editor.assertText( + "resources:\n" + + "- name: foo\n" + + " type: pool\n" + + " source:\n" + + " username: someone\n" + + " branch: <*>\n" + + " pool: \n" + + " uri: \n" + ); + } + @Test public void reconcileResourceTypeType() throws Exception { Editor editor; editor = harness.newEditor( @@ -89,31 +114,6 @@ public class ConcourseEditorTest { ); } - @Test public void addMultipleRequiredPropertiesQuickfix() throws Exception { - Editor editor = harness.newEditor( - "resources:\n" + - "- name: foo\n" + - " type: pool\n" + - " source:\n" + - " username: someone\n" - ); - Diagnostic problem = editor.assertProblems("source|[branch, pool, uri] are required").get(0); - CodeAction quickfix = editor.assertCodeAction(problem); - assertEquals("Add properties: [branch, pool, uri]", quickfix.getLabel()); - quickfix.perform(); - - editor.assertRawText( - "resources:\n" + - "- name: foo\n" + - " type: pool\n" + - " source:\n" + - " username: someone\n" + - " branch: \n" + - " pool: \n" + - " uri: \n" - ); - } - @Test public void testReconcileCatchesParseError() throws Exception { Editor editor = harness.newEditor( "somemap: val\n"+ diff --git a/vscode-extensions/commons-vscode/src/launch-util.ts b/vscode-extensions/commons-vscode/src/launch-util.ts index 0463ab120..a465ad859 100644 --- a/vscode-extensions/commons-vscode/src/launch-util.ts +++ b/vscode-extensions/commons-vscode/src/launch-util.ts @@ -14,7 +14,7 @@ import { RequestType, LanguageClient, LanguageClientOptions, SettingMonitor, Ser import { TextDocument, OutputChannel, Disposable, window } from 'vscode'; import { Trace, NotificationType } from 'vscode-jsonrpc'; import * as P2C from 'vscode-languageclient/lib/protocolConverter'; -import {WorkspaceEdit} from 'vscode-languageserver-types'; +import {WorkspaceEdit, Position} from 'vscode-languageserver-types'; let p2c = P2C.createConverter(); @@ -145,6 +145,7 @@ function setupLanguageClient(context: VSCode.ExtensionContext, createServer: Ser } let progressNotification = new NotificationType("sts/progress"); + let moveCursorRequest = new RequestType("sts/moveCursor"); let disposable = client.start(); @@ -155,6 +156,17 @@ function setupLanguageClient(context: VSCode.ExtensionContext, createServer: Ser client.onNotification(progressNotification, (params: ProgressParams) => { progressService.handle(params); }); + client.onRequest(moveCursorRequest, (params: MoveCursorParams) => { + let editors = VSCode.window.visibleTextEditors; + for (let editor of editors) { + if (editor.document.uri.toString() == params.uri) { + let cursor = p2c.asPosition(params.position); + let selection : VSCode.Selection = new VSCode.Selection(cursor, cursor); + editor.selections = [ selection ]; + } + } + return { applied: true}; + }); return client; }); } @@ -204,6 +216,15 @@ function correctBinname(binname: string) { return binname; } +interface MoveCursorParams { + uri: string + position: Position +} + +interface MoveCursorResponse { + applied: boolean +} + interface ProgressParams { id: string statusMsg?: string