Merge pull request #1490 from eclipse/jk_gh1488

[lsp] properly rename quoted identifiers
This commit is contained in:
Jan Koehnlein 2020-05-13 13:50:31 +02:00 committed by GitHub
commit 70485e16a4
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 442 additions and 23 deletions

View file

@ -0,0 +1,145 @@
/*******************************************************************************
* Copyright (c) 2020 TypeFox GmbH (http://www.typefox.io) 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
* http://www.eclipse.org/legal/epl-2.0.
*
* SPDX-License-Identifier: EPL-2.0
*******************************************************************************/
package org.eclipse.xtext.ide.tests.server
import org.eclipse.lsp4j.Position
import org.eclipse.lsp4j.RenameParams
import org.eclipse.lsp4j.TextDocumentIdentifier
import org.junit.Test
import org.eclipse.lsp4j.PrepareRenameParams
import org.eclipse.xtext.ide.server.Document
import org.eclipse.lsp4j.ClientCapabilities
import org.eclipse.lsp4j.WorkspaceClientCapabilities
import org.eclipse.lsp4j.WorkspaceEditCapabilities
import org.eclipse.lsp4j.TextDocumentClientCapabilities
import org.eclipse.lsp4j.RenameCapabilities
import org.eclipse.xtext.testing.AbstractLanguageServerTest
/**
* @author koehnlein - Initial contribution and API
*/
class RenameTest3 extends AbstractLanguageServerTest {
new() {
super("renametl")
}
@Test
def void testRenameAutoQuote() {
val model = '''
type Foo {
}
'''
val file = 'foo/Foo.renametl'.writeFile(model)
initialize
val identifier = new TextDocumentIdentifier(file)
val position = new Position(0, 6)
val range = languageServer.prepareRename(new PrepareRenameParams(identifier, position)).get.getLeft
assertEquals('Foo', new Document(0, model).getSubstring(range))
val params = new RenameParams(identifier, position, 'type')
val workspaceEdit = languageServer.rename(params).get
assertEquals('''
changes :
documentChanges :
Foo.renametl <1> : ^type [[0, 5] .. [0, 8]]
'''.toString, toExpectation(workspaceEdit))
}
@Test
def void testRenameQuoted() {
val model = '''
type ^type {
}
'''
val file = 'foo/Foo.renametl'.writeFile(model)
initialize
val identifier = new TextDocumentIdentifier(file)
val position = new Position(0, 6)
val range = languageServer.prepareRename(new PrepareRenameParams(identifier, position)).get.getLeft
assertEquals('^type', new Document(0, model).getSubstring(range))
val params = new RenameParams(identifier, position, 'Foo')
val workspaceEdit = languageServer.rename(params).get
assertEquals('''
changes :
documentChanges :
Foo.renametl <1> : Foo [[0, 5] .. [0, 10]]
'''.toString, toExpectation(workspaceEdit))
}
@Test
def void testRenameAutoQuoteRef() {
val model = '''
type Foo {
}
type Bar extends Foo {
}
'''
val file = 'foo/Foo.renametl'.writeFile(model)
initialize
val identifier = new TextDocumentIdentifier(file)
val position = new Position(3, 18)
val range = languageServer.prepareRename(new PrepareRenameParams(identifier, position)).get.getLeft
assertEquals('Foo', new Document(0, model).getSubstring(range))
val params = new RenameParams(identifier, position, 'type')
val workspaceEdit = languageServer.rename(params).get
assertEquals('''
changes :
documentChanges :
Foo.renametl <1> : ^type [[0, 5] .. [0, 8]]
^type [[3, 17] .. [3, 20]]
'''.toString, toExpectation(workspaceEdit))
}
@Test
def void testRenameQuotedRef() {
val model = '''
type ^type {
}
type Bar extends ^type {
}
'''
val file = 'foo/Foo.renametl'.writeFile(model)
initialize
val identifier = new TextDocumentIdentifier(file)
val position = new Position(3, 19)
val range = languageServer.prepareRename(new PrepareRenameParams(identifier, position)).get.getLeft
assertEquals('^type', new Document(0, model).getSubstring(range))
val params = new RenameParams(identifier, position, 'Foo')
val workspaceEdit = languageServer.rename(params).get
assertEquals('''
changes :
documentChanges :
Foo.renametl <1> : Foo [[0, 5] .. [0, 10]]
Foo [[3, 17] .. [3, 22]]
'''.toString, toExpectation(workspaceEdit))
}
override protected initialize() {
super.initialize([ params |
params.capabilities = new ClientCapabilities => [
workspace = new WorkspaceClientCapabilities => [
workspaceEdit = new WorkspaceEditCapabilities => [
documentChanges = true
]
]
textDocument = new TextDocumentClientCapabilities => [
rename = new RenameCapabilities => [
prepareSupport = true
]
]
]
])
}
}

View file

@ -0,0 +1,218 @@
/**
* Copyright (c) 2020 TypeFox GmbH (http://www.typefox.io) 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
* http://www.eclipse.org/legal/epl-2.0.
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.eclipse.xtext.ide.tests.server;
import org.eclipse.lsp4j.ClientCapabilities;
import org.eclipse.lsp4j.InitializeParams;
import org.eclipse.lsp4j.InitializeResult;
import org.eclipse.lsp4j.Position;
import org.eclipse.lsp4j.PrepareRenameParams;
import org.eclipse.lsp4j.Range;
import org.eclipse.lsp4j.RenameCapabilities;
import org.eclipse.lsp4j.RenameParams;
import org.eclipse.lsp4j.TextDocumentClientCapabilities;
import org.eclipse.lsp4j.TextDocumentIdentifier;
import org.eclipse.lsp4j.WorkspaceClientCapabilities;
import org.eclipse.lsp4j.WorkspaceEdit;
import org.eclipse.lsp4j.WorkspaceEditCapabilities;
import org.eclipse.xtend2.lib.StringConcatenation;
import org.eclipse.xtext.ide.server.Document;
import org.eclipse.xtext.testing.AbstractLanguageServerTest;
import org.eclipse.xtext.xbase.lib.Exceptions;
import org.eclipse.xtext.xbase.lib.ObjectExtensions;
import org.eclipse.xtext.xbase.lib.Procedures.Procedure1;
import org.junit.Test;
/**
* @author koehnlein - Initial contribution and API
*/
@SuppressWarnings("all")
public class RenameTest3 extends AbstractLanguageServerTest {
public RenameTest3() {
super("renametl");
}
@Test
public void testRenameAutoQuote() {
try {
StringConcatenation _builder = new StringConcatenation();
_builder.append("type Foo {");
_builder.newLine();
_builder.append("}");
_builder.newLine();
final String model = _builder.toString();
final String file = this.writeFile("foo/Foo.renametl", model);
this.initialize();
final TextDocumentIdentifier identifier = new TextDocumentIdentifier(file);
final Position position = new Position(0, 6);
PrepareRenameParams _prepareRenameParams = new PrepareRenameParams(identifier, position);
final Range range = this.languageServer.prepareRename(_prepareRenameParams).get().getLeft();
this.assertEquals("Foo", new Document(Integer.valueOf(0), model).getSubstring(range));
final RenameParams params = new RenameParams(identifier, position, "type");
final WorkspaceEdit workspaceEdit = this.languageServer.rename(params).get();
StringConcatenation _builder_1 = new StringConcatenation();
_builder_1.append("changes :");
_builder_1.newLine();
_builder_1.append("documentChanges : ");
_builder_1.newLine();
_builder_1.append(" ");
_builder_1.append("Foo.renametl <1> : ^type [[0, 5] .. [0, 8]]");
_builder_1.newLine();
this.assertEquals(_builder_1.toString(), this.toExpectation(workspaceEdit));
} catch (Throwable _e) {
throw Exceptions.sneakyThrow(_e);
}
}
@Test
public void testRenameQuoted() {
try {
StringConcatenation _builder = new StringConcatenation();
_builder.append("type ^type {");
_builder.newLine();
_builder.append("}");
_builder.newLine();
final String model = _builder.toString();
final String file = this.writeFile("foo/Foo.renametl", model);
this.initialize();
final TextDocumentIdentifier identifier = new TextDocumentIdentifier(file);
final Position position = new Position(0, 6);
PrepareRenameParams _prepareRenameParams = new PrepareRenameParams(identifier, position);
final Range range = this.languageServer.prepareRename(_prepareRenameParams).get().getLeft();
this.assertEquals("^type", new Document(Integer.valueOf(0), model).getSubstring(range));
final RenameParams params = new RenameParams(identifier, position, "Foo");
final WorkspaceEdit workspaceEdit = this.languageServer.rename(params).get();
StringConcatenation _builder_1 = new StringConcatenation();
_builder_1.append("changes :");
_builder_1.newLine();
_builder_1.append("documentChanges : ");
_builder_1.newLine();
_builder_1.append(" ");
_builder_1.append("Foo.renametl <1> : Foo [[0, 5] .. [0, 10]]");
_builder_1.newLine();
this.assertEquals(_builder_1.toString(), this.toExpectation(workspaceEdit));
} catch (Throwable _e) {
throw Exceptions.sneakyThrow(_e);
}
}
@Test
public void testRenameAutoQuoteRef() {
try {
StringConcatenation _builder = new StringConcatenation();
_builder.append("type Foo {");
_builder.newLine();
_builder.append("}");
_builder.newLine();
_builder.newLine();
_builder.append("type Bar extends Foo {");
_builder.newLine();
_builder.append("}");
_builder.newLine();
final String model = _builder.toString();
final String file = this.writeFile("foo/Foo.renametl", model);
this.initialize();
final TextDocumentIdentifier identifier = new TextDocumentIdentifier(file);
final Position position = new Position(3, 18);
PrepareRenameParams _prepareRenameParams = new PrepareRenameParams(identifier, position);
final Range range = this.languageServer.prepareRename(_prepareRenameParams).get().getLeft();
this.assertEquals("Foo", new Document(Integer.valueOf(0), model).getSubstring(range));
final RenameParams params = new RenameParams(identifier, position, "type");
final WorkspaceEdit workspaceEdit = this.languageServer.rename(params).get();
StringConcatenation _builder_1 = new StringConcatenation();
_builder_1.append("changes :");
_builder_1.newLine();
_builder_1.append("documentChanges : ");
_builder_1.newLine();
_builder_1.append(" ");
_builder_1.append("Foo.renametl <1> : ^type [[0, 5] .. [0, 8]]");
_builder_1.newLine();
_builder_1.append(" ");
_builder_1.append("^type [[3, 17] .. [3, 20]]");
_builder_1.newLine();
this.assertEquals(_builder_1.toString(), this.toExpectation(workspaceEdit));
} catch (Throwable _e) {
throw Exceptions.sneakyThrow(_e);
}
}
@Test
public void testRenameQuotedRef() {
try {
StringConcatenation _builder = new StringConcatenation();
_builder.append("type ^type {");
_builder.newLine();
_builder.append("}");
_builder.newLine();
_builder.newLine();
_builder.append("type Bar extends ^type {");
_builder.newLine();
_builder.append("}");
_builder.newLine();
final String model = _builder.toString();
final String file = this.writeFile("foo/Foo.renametl", model);
this.initialize();
final TextDocumentIdentifier identifier = new TextDocumentIdentifier(file);
final Position position = new Position(3, 19);
PrepareRenameParams _prepareRenameParams = new PrepareRenameParams(identifier, position);
final Range range = this.languageServer.prepareRename(_prepareRenameParams).get().getLeft();
this.assertEquals("^type", new Document(Integer.valueOf(0), model).getSubstring(range));
final RenameParams params = new RenameParams(identifier, position, "Foo");
final WorkspaceEdit workspaceEdit = this.languageServer.rename(params).get();
StringConcatenation _builder_1 = new StringConcatenation();
_builder_1.append("changes :");
_builder_1.newLine();
_builder_1.append("documentChanges : ");
_builder_1.newLine();
_builder_1.append(" ");
_builder_1.append("Foo.renametl <1> : Foo [[0, 5] .. [0, 10]]");
_builder_1.newLine();
_builder_1.append(" ");
_builder_1.append("Foo [[3, 17] .. [3, 22]]");
_builder_1.newLine();
this.assertEquals(_builder_1.toString(), this.toExpectation(workspaceEdit));
} catch (Throwable _e) {
throw Exceptions.sneakyThrow(_e);
}
}
@Override
protected InitializeResult initialize() {
final Procedure1<InitializeParams> _function = (InitializeParams params) -> {
ClientCapabilities _clientCapabilities = new ClientCapabilities();
final Procedure1<ClientCapabilities> _function_1 = (ClientCapabilities it) -> {
WorkspaceClientCapabilities _workspaceClientCapabilities = new WorkspaceClientCapabilities();
final Procedure1<WorkspaceClientCapabilities> _function_2 = (WorkspaceClientCapabilities it_1) -> {
WorkspaceEditCapabilities _workspaceEditCapabilities = new WorkspaceEditCapabilities();
final Procedure1<WorkspaceEditCapabilities> _function_3 = (WorkspaceEditCapabilities it_2) -> {
it_2.setDocumentChanges(Boolean.valueOf(true));
};
WorkspaceEditCapabilities _doubleArrow = ObjectExtensions.<WorkspaceEditCapabilities>operator_doubleArrow(_workspaceEditCapabilities, _function_3);
it_1.setWorkspaceEdit(_doubleArrow);
};
WorkspaceClientCapabilities _doubleArrow = ObjectExtensions.<WorkspaceClientCapabilities>operator_doubleArrow(_workspaceClientCapabilities, _function_2);
it.setWorkspace(_doubleArrow);
TextDocumentClientCapabilities _textDocumentClientCapabilities = new TextDocumentClientCapabilities();
final Procedure1<TextDocumentClientCapabilities> _function_3 = (TextDocumentClientCapabilities it_1) -> {
RenameCapabilities _renameCapabilities = new RenameCapabilities();
final Procedure1<RenameCapabilities> _function_4 = (RenameCapabilities it_2) -> {
it_2.setPrepareSupport(Boolean.valueOf(true));
};
RenameCapabilities _doubleArrow_1 = ObjectExtensions.<RenameCapabilities>operator_doubleArrow(_renameCapabilities, _function_4);
it_1.setRename(_doubleArrow_1);
};
TextDocumentClientCapabilities _doubleArrow_1 = ObjectExtensions.<TextDocumentClientCapabilities>operator_doubleArrow(_textDocumentClientCapabilities, _function_3);
it.setTextDocument(_doubleArrow_1);
};
ClientCapabilities _doubleArrow = ObjectExtensions.<ClientCapabilities>operator_doubleArrow(_clientCapabilities, _function_1);
params.setCapabilities(_doubleArrow);
};
return super.initialize(_function);
}
}

View file

@ -28,12 +28,14 @@ import org.eclipse.lsp4j.jsonrpc.messages.Either
import org.eclipse.xtend.lib.annotations.Accessors
import org.eclipse.xtext.CrossReference
import org.eclipse.xtext.RuleCall
import org.eclipse.xtext.conversion.IValueConverterService
import org.eclipse.xtext.ide.refactoring.IRenameStrategy2
import org.eclipse.xtext.ide.refactoring.RenameChange
import org.eclipse.xtext.ide.refactoring.RenameContext
import org.eclipse.xtext.ide.serializer.IChangeSerializer
import org.eclipse.xtext.ide.server.Document
import org.eclipse.xtext.ide.server.ILanguageServerAccess
import org.eclipse.xtext.linking.impl.LinkingHelper
import org.eclipse.xtext.nodemodel.ILeafNode
import org.eclipse.xtext.nodemodel.util.NodeModelUtils
import org.eclipse.xtext.parsetree.reconstr.impl.TokenUtil
@ -65,6 +67,10 @@ class RenameService2 implements IRenameService2 {
@Inject TokenUtil tokenUtil
@Inject IValueConverterService valueConverterService
@Inject LinkingHelper linkingHelper
Function<EObject, String> attributeResolver = SimpleAttributeResolver.newResolver(String, 'name')
override rename(Options options) {
@ -197,11 +203,11 @@ class RenameService2 implements IRenameService2 {
if (element !== null && !element.eIsProxy) {
val leaf = NodeModelUtils.findLeafNodeAtOffset(rootNode, candidateOffset)
if (leaf !== null && leaf.isIdentifier) {
val leafText = NodeModelUtils.getTokenText(leaf)
val convertedNameValue = getConvertedValue(leaf.grammarElement, leaf)
val elementName = element.elementName
if (!leafText.nullOrEmpty && !elementName.nullOrEmpty && leafText == elementName) {
if (!convertedNameValue.nullOrEmpty && !elementName.nullOrEmpty && convertedNameValue == elementName) {
val start = document.getPosition(leaf.offset)
val end = document.getPosition(leaf.offset + elementName.length)
val end = document.getPosition(leaf.endOffset)
return Either.forLeft(new Range(start, end))
}
}
@ -219,6 +225,19 @@ class RenameService2 implements IRenameService2 {
return null
}
protected def String getConvertedValue(EObject grammarElement, ILeafNode leaf) {
try {
switch (grammarElement) {
RuleCall: return valueConverterService.toValue(leaf.text, grammarElement.rule.name, leaf).toString()
CrossReference: return linkingHelper.getCrossRefNodeAsString(leaf, true)
}
} catch (Exception exc) {
// The current name text cannot be converted to a value.
// Rename could be used to fix this.
}
return leaf.text
}
/**
* If this method returns {@code false}, it is sure, that the rename operation will fail.
* There is no guarantee that it will succeed even if it returns {@code true}.

View file

@ -39,6 +39,7 @@ import org.eclipse.xtend.lib.annotations.Accessors;
import org.eclipse.xtend2.lib.StringConcatenation;
import org.eclipse.xtext.CrossReference;
import org.eclipse.xtext.RuleCall;
import org.eclipse.xtext.conversion.IValueConverterService;
import org.eclipse.xtext.ide.refactoring.IRenameStrategy2;
import org.eclipse.xtext.ide.refactoring.RefactoringIssueAcceptor;
import org.eclipse.xtext.ide.refactoring.RenameChange;
@ -49,6 +50,7 @@ import org.eclipse.xtext.ide.server.ILanguageServerAccess;
import org.eclipse.xtext.ide.server.rename.ChangeConverter2;
import org.eclipse.xtext.ide.server.rename.IRenameService2;
import org.eclipse.xtext.ide.server.rename.ServerRefactoringIssueAcceptor;
import org.eclipse.xtext.linking.impl.LinkingHelper;
import org.eclipse.xtext.nodemodel.ICompositeNode;
import org.eclipse.xtext.nodemodel.ILeafNode;
import org.eclipse.xtext.nodemodel.util.NodeModelUtils;
@ -88,6 +90,12 @@ public class RenameService2 implements IRenameService2 {
@Inject
private TokenUtil tokenUtil;
@Inject
private IValueConverterService valueConverterService;
@Inject
private LinkingHelper linkingHelper;
private Function<EObject, String> attributeResolver = SimpleAttributeResolver.<EObject, String>newResolver(String.class, "name");
@Override
@ -133,11 +141,11 @@ public class RenameService2 implements IRenameService2 {
}
}
if (((element == null) || element.eIsProxy())) {
StringConcatenation _builder = new StringConcatenation();
_builder.append("No element found at ");
String _positionFragment = this.toPositionFragment(position_1, uri);
_builder.append(_positionFragment);
issueAcceptor.add(RefactoringIssueAcceptor.Severity.FATAL, _builder.toString());
StringConcatenation _builder_1 = new StringConcatenation();
_builder_1.append("No element found at ");
String _positionFragment_1 = this.toPositionFragment(position_1, uri);
_builder_1.append(_positionFragment_1);
issueAcceptor.add(RefactoringIssueAcceptor.Severity.FATAL, _builder_1.toString());
} else {
final IResourceServiceProvider services = this.serviceProviderRegistry.getResourceServiceProvider(element.eResource().getURI());
final IChangeSerializer changeSerializer = services.<IChangeSerializer>get(IChangeSerializer.class);
@ -279,14 +287,11 @@ public class RenameService2 implements IRenameService2 {
if (((element != null) && (!element.eIsProxy()))) {
final ILeafNode leaf = NodeModelUtils.findLeafNodeAtOffset(rootNode, candidateOffset);
if (((leaf != null) && this.isIdentifier(leaf))) {
final String leafText = NodeModelUtils.getTokenText(leaf);
final String convertedNameValue = this.getConvertedValue(leaf.getGrammarElement(), leaf);
final String elementName = this.getElementName(element);
if ((((!StringExtensions.isNullOrEmpty(leafText)) && (!StringExtensions.isNullOrEmpty(elementName))) && Objects.equal(leafText, elementName))) {
if ((((!StringExtensions.isNullOrEmpty(convertedNameValue)) && (!StringExtensions.isNullOrEmpty(elementName))) && Objects.equal(convertedNameValue, elementName))) {
final Position start = document.getPosition(leaf.getOffset());
int _offset = leaf.getOffset();
int _length = elementName.length();
int _plus = (_offset + _length);
final Position end = document.getPosition(_plus);
final Position end = document.getPosition(leaf.getEndOffset());
Range _range = new Range(start, end);
return Either.<Range, PrepareRenameResult>forLeft(_range);
}
@ -307,21 +312,43 @@ public class RenameService2 implements IRenameService2 {
throw Exceptions.sneakyThrow(_t);
}
}
StringConcatenation _builder_1 = new StringConcatenation();
_builder_1.append("No element found at ");
String _positionFragment = this.toPositionFragment(caretPosition, uri);
_builder_1.append(_positionFragment);
RenameService2.LOG.trace(_builder_1);
} else {
StringConcatenation _builder_2 = new StringConcatenation();
_builder_2.append("Loaded resource is not an XtextResource. URI: ");
URI _uRI = resource.getURI();
_builder_2.append(_uRI);
_builder_2.append("No element found at ");
String _positionFragment_1 = this.toPositionFragment(caretPosition, uri);
_builder_2.append(_positionFragment_1);
RenameService2.LOG.trace(_builder_2);
} else {
StringConcatenation _builder_3 = new StringConcatenation();
_builder_3.append("Loaded resource is not an XtextResource. URI: ");
URI _uRI = resource.getURI();
_builder_3.append(_uRI);
RenameService2.LOG.trace(_builder_3);
}
return null;
}
protected String getConvertedValue(final EObject grammarElement, final ILeafNode leaf) {
try {
boolean _matched = false;
if (grammarElement instanceof RuleCall) {
_matched=true;
return this.valueConverterService.toValue(leaf.getText(), ((RuleCall)grammarElement).getRule().getName(), leaf).toString();
}
if (!_matched) {
if (grammarElement instanceof CrossReference) {
_matched=true;
return this.linkingHelper.getCrossRefNodeAsString(leaf, true);
}
}
} catch (final Throwable _t) {
if (_t instanceof Exception) {
} else {
throw Exceptions.sneakyThrow(_t);
}
}
return leaf.getText();
}
/**
* If this method returns {@code false}, it is sure, that the rename operation will fail.
* There is no guarantee that it will succeed even if it returns {@code true}.
@ -411,6 +438,16 @@ public class RenameService2 implements IRenameService2 {
return this.tokenUtil;
}
@Pure
protected IValueConverterService getValueConverterService() {
return this.valueConverterService;
}
@Pure
protected LinkingHelper getLinkingHelper() {
return this.linkingHelper;
}
@Pure
protected Function<EObject, String> getAttributeResolver() {
return this.attributeResolver;