From afe2878506ea5e8ec2367401ab843130120bf1c6 Mon Sep 17 00:00:00 2001 From: Jan Koehnlein Date: Mon, 11 May 2020 20:38:05 +0200 Subject: [PATCH] [lsp] properly rename quoted identifiers Fixes #1488 --- .../xtext/ide/tests/server/RenameTest3.xtend | 145 ++++++++++++ .../xtext/ide/tests/server/RenameTest3.java | 218 ++++++++++++++++++ .../ide/server/rename/RenameService2.xtend | 15 +- .../ide/server/rename/RenameService2.java | 63 +++-- 4 files changed, 420 insertions(+), 21 deletions(-) create mode 100644 org.eclipse.xtext.ide.tests/src/org/eclipse/xtext/ide/tests/server/RenameTest3.xtend create mode 100644 org.eclipse.xtext.ide.tests/xtend-gen/org/eclipse/xtext/ide/tests/server/RenameTest3.java diff --git a/org.eclipse.xtext.ide.tests/src/org/eclipse/xtext/ide/tests/server/RenameTest3.xtend b/org.eclipse.xtext.ide.tests/src/org/eclipse/xtext/ide/tests/server/RenameTest3.xtend new file mode 100644 index 000000000..2e7a65884 --- /dev/null +++ b/org.eclipse.xtext.ide.tests/src/org/eclipse/xtext/ide/tests/server/RenameTest3.xtend @@ -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 + ] + ] + ] + ]) + } +} diff --git a/org.eclipse.xtext.ide.tests/xtend-gen/org/eclipse/xtext/ide/tests/server/RenameTest3.java b/org.eclipse.xtext.ide.tests/xtend-gen/org/eclipse/xtext/ide/tests/server/RenameTest3.java new file mode 100644 index 000000000..59d45e774 --- /dev/null +++ b/org.eclipse.xtext.ide.tests/xtend-gen/org/eclipse/xtext/ide/tests/server/RenameTest3.java @@ -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 _function = (InitializeParams params) -> { + ClientCapabilities _clientCapabilities = new ClientCapabilities(); + final Procedure1 _function_1 = (ClientCapabilities it) -> { + WorkspaceClientCapabilities _workspaceClientCapabilities = new WorkspaceClientCapabilities(); + final Procedure1 _function_2 = (WorkspaceClientCapabilities it_1) -> { + WorkspaceEditCapabilities _workspaceEditCapabilities = new WorkspaceEditCapabilities(); + final Procedure1 _function_3 = (WorkspaceEditCapabilities it_2) -> { + it_2.setDocumentChanges(Boolean.valueOf(true)); + }; + WorkspaceEditCapabilities _doubleArrow = ObjectExtensions.operator_doubleArrow(_workspaceEditCapabilities, _function_3); + it_1.setWorkspaceEdit(_doubleArrow); + }; + WorkspaceClientCapabilities _doubleArrow = ObjectExtensions.operator_doubleArrow(_workspaceClientCapabilities, _function_2); + it.setWorkspace(_doubleArrow); + TextDocumentClientCapabilities _textDocumentClientCapabilities = new TextDocumentClientCapabilities(); + final Procedure1 _function_3 = (TextDocumentClientCapabilities it_1) -> { + RenameCapabilities _renameCapabilities = new RenameCapabilities(); + final Procedure1 _function_4 = (RenameCapabilities it_2) -> { + it_2.setPrepareSupport(Boolean.valueOf(true)); + }; + RenameCapabilities _doubleArrow_1 = ObjectExtensions.operator_doubleArrow(_renameCapabilities, _function_4); + it_1.setRename(_doubleArrow_1); + }; + TextDocumentClientCapabilities _doubleArrow_1 = ObjectExtensions.operator_doubleArrow(_textDocumentClientCapabilities, _function_3); + it.setTextDocument(_doubleArrow_1); + }; + ClientCapabilities _doubleArrow = ObjectExtensions.operator_doubleArrow(_clientCapabilities, _function_1); + params.setCapabilities(_doubleArrow); + }; + return super.initialize(_function); + } +} diff --git a/org.eclipse.xtext.ide/src/org/eclipse/xtext/ide/server/rename/RenameService2.xtend b/org.eclipse.xtext.ide/src/org/eclipse/xtext/ide/server/rename/RenameService2.xtend index 372b3fca9..a962a930e 100644 --- a/org.eclipse.xtext.ide/src/org/eclipse/xtext/ide/server/rename/RenameService2.xtend +++ b/org.eclipse.xtext.ide/src/org/eclipse/xtext/ide/server/rename/RenameService2.xtend @@ -28,6 +28,7 @@ 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 @@ -65,6 +66,8 @@ class RenameService2 implements IRenameService2 { @Inject TokenUtil tokenUtil + @Inject IValueConverterService valueConverterService + Function attributeResolver = SimpleAttributeResolver.newResolver(String, 'name') override rename(Options options) { @@ -197,11 +200,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 leafText = getConvertedValue(leaf.grammarElement, leaf) val elementName = element.elementName if (!leafText.nullOrEmpty && !elementName.nullOrEmpty && leafText == 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 +222,14 @@ class RenameService2 implements IRenameService2 { return null } + protected def String getConvertedValue(EObject grammarElement, ILeafNode leaf) { + switch (grammarElement) { + RuleCall: valueConverterService.toValue(leaf.text, grammarElement.rule.name, leaf).toString() + CrossReference: getConvertedValue(grammarElement.terminal, leaf) + default: 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}. diff --git a/org.eclipse.xtext.ide/xtend-gen/org/eclipse/xtext/ide/server/rename/RenameService2.java b/org.eclipse.xtext.ide/xtend-gen/org/eclipse/xtext/ide/server/rename/RenameService2.java index a9591dbd0..e371490c6 100644 --- a/org.eclipse.xtext.ide/xtend-gen/org/eclipse/xtext/ide/server/rename/RenameService2.java +++ b/org.eclipse.xtext.ide/xtend-gen/org/eclipse/xtext/ide/server/rename/RenameService2.java @@ -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; @@ -88,6 +89,9 @@ public class RenameService2 implements IRenameService2 { @Inject private TokenUtil tokenUtil; + @Inject + private IValueConverterService valueConverterService; + private Function attributeResolver = SimpleAttributeResolver.newResolver(String.class, "name"); @Override @@ -133,11 +137,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.get(IChangeSerializer.class); @@ -279,14 +283,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 leafText = this.getConvertedValue(leaf.getGrammarElement(), leaf); final String elementName = this.getElementName(element); if ((((!StringExtensions.isNullOrEmpty(leafText)) && (!StringExtensions.isNullOrEmpty(elementName))) && Objects.equal(leafText, 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.forLeft(_range); } @@ -307,21 +308,40 @@ 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) { + String _switchResult = null; + boolean _matched = false; + if (grammarElement instanceof RuleCall) { + _matched=true; + _switchResult = this.valueConverterService.toValue(leaf.getText(), ((RuleCall)grammarElement).getRule().getName(), leaf).toString(); + } + if (!_matched) { + if (grammarElement instanceof CrossReference) { + _matched=true; + _switchResult = this.getConvertedValue(((CrossReference)grammarElement).getTerminal(), leaf); + } + } + if (!_matched) { + _switchResult = leaf.getText(); + } + return _switchResult; + } + /** * 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 +431,11 @@ public class RenameService2 implements IRenameService2 { return this.tokenUtil; } + @Pure + protected IValueConverterService getValueConverterService() { + return this.valueConverterService; + } + @Pure protected Function getAttributeResolver() { return this.attributeResolver;