From db5608603dd3d21f416c4303324d18d669fb80fc Mon Sep 17 00:00:00 2001 From: akosyakov Date: Thu, 26 May 2016 16:14:28 +0200 Subject: [PATCH] [lsi] Document Symbol Support Change-Id: Idf75084a4debf56c46d49fc8db579d400a8eaef0 Signed-off-by: akosyakov --- .../xtext/ide/server/DocumentExtensions.xtend | 66 ++++++++++ .../xtext/ide/server/LanguageServerImpl.xtend | 46 ++++--- .../server/symbol/DocumentSymbolService.xtend | 74 +++++++++++ .../server/AbstractLanguageServerTest.xtend | 120 ++++++++++++------ .../ide/tests/server/CompletionTest.xtend | 33 ++--- .../ide/tests/server/DocumentSymbolTest.xtend | 100 +++++++++++++++ 6 files changed, 359 insertions(+), 80 deletions(-) create mode 100644 plugins/org.eclipse.xtext.ide/src/org/eclipse/xtext/ide/server/DocumentExtensions.xtend create mode 100644 plugins/org.eclipse.xtext.ide/src/org/eclipse/xtext/ide/server/symbol/DocumentSymbolService.xtend create mode 100644 tests/org.eclipse.xtext.ide.tests/src/org/eclipse/xtext/ide/tests/server/DocumentSymbolTest.xtend diff --git a/plugins/org.eclipse.xtext.ide/src/org/eclipse/xtext/ide/server/DocumentExtensions.xtend b/plugins/org.eclipse.xtext.ide/src/org/eclipse/xtext/ide/server/DocumentExtensions.xtend new file mode 100644 index 000000000..59acc85e1 --- /dev/null +++ b/plugins/org.eclipse.xtext.ide/src/org/eclipse/xtext/ide/server/DocumentExtensions.xtend @@ -0,0 +1,66 @@ +/******************************************************************************* + * Copyright (c) 2016 TypeFox GmbH (http://www.typefox.io) and others. + * 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 + *******************************************************************************/ +package org.eclipse.xtext.ide.server + +import com.google.inject.Inject +import com.google.inject.Singleton +import io.typefox.lsapi.LocationImpl +import io.typefox.lsapi.PositionImpl +import io.typefox.lsapi.RangeImpl +import org.eclipse.emf.ecore.EObject +import org.eclipse.emf.ecore.resource.Resource +import org.eclipse.xtext.resource.ILocationInFileProvider +import org.eclipse.xtext.resource.XtextResource +import org.eclipse.xtext.util.ITextRegion + +import static io.typefox.lsapi.util.LsapiFactories.* + +import static extension org.eclipse.xtext.nodemodel.util.NodeModelUtils.* + +/** + * @author kosyakov - Initial contribution and API + */ +@Singleton +class DocumentExtensions { + + @Inject + extension UriExtensions + + @Inject + ILocationInFileProvider locationInFileProvider + + def PositionImpl newPosition(Resource resource, int offset) { + if (resource instanceof XtextResource) { + val rootNode = resource.parseResult.rootNode + val lineAndColumn = rootNode.getLineAndColumn(offset) + return newPosition(lineAndColumn.line - 1, lineAndColumn.column - 1) + } + return null + } + + def RangeImpl newRange(Resource resource, int startOffset, int endOffset) { + val startPosition = resource.newPosition(startOffset) + val endPosition = resource.newPosition(endOffset) + return newRange(startPosition, endPosition) + } + + def RangeImpl newRange(Resource resource, ITextRegion region) { + return resource.newRange(region.offset, region.offset + region.length) + } + + def LocationImpl newLocation(EObject object) { + val resource = object.eResource + val textRegion = locationInFileProvider.getSignificantTextRegion(object) + + val location = new LocationImpl + location.uri = resource.URI.toPath + location.range = resource.newRange(textRegion) + return location + } + +} diff --git a/plugins/org.eclipse.xtext.ide/src/org/eclipse/xtext/ide/server/LanguageServerImpl.xtend b/plugins/org.eclipse.xtext.ide/src/org/eclipse/xtext/ide/server/LanguageServerImpl.xtend index e092b717f..98c91bc74 100644 --- a/plugins/org.eclipse.xtext.ide/src/org/eclipse/xtext/ide/server/LanguageServerImpl.xtend +++ b/plugins/org.eclipse.xtext.ide/src/org/eclipse/xtext/ide/server/LanguageServerImpl.xtend @@ -52,6 +52,7 @@ import org.eclipse.emf.common.util.URI import org.eclipse.xtend.lib.annotations.Accessors import org.eclipse.xtext.ide.editor.contentassist.ContentAssistEntry import org.eclipse.xtext.ide.server.contentassist.ContentAssistService +import org.eclipse.xtext.ide.server.symbol.DocumentSymbolService import org.eclipse.xtext.resource.IResourceServiceProvider import org.eclipse.xtext.validation.Issue @@ -76,6 +77,7 @@ import static io.typefox.lsapi.util.LsapiFactories.* workspaceManager.initialize(rootURI)[this.publishDiagnostics($0, $1)] return new InitializeResultImpl => [ capabilities = new ServerCapabilitiesImpl => [ + documentSymbolProvider = true textDocumentSync = ServerCapabilities.SYNC_INCREMENTAL completionProvider = new CompletionOptionsImpl => [ resolveProvider = false @@ -102,7 +104,23 @@ import static io.typefox.lsapi.util.LsapiFactories.* override getWindowService() { this } + + // notification callbacks + + override onShowMessage(NotificationCallback callback) { + // TODO: auto-generated method stub + } + + override onShowMessageRequest(NotificationCallback callback) { + // TODO: auto-generated method stub + } + override onLogMessage(NotificationCallback callback) { + // TODO: auto-generated method stub + } + + // end notification callbacks + // file/content change events override didOpen(DidOpenTextDocumentParams params) { workspaceManager.didOpen(params.textDocument.uri.toUri, params.textDocument.version, params.textDocument.text) @@ -199,21 +217,21 @@ import static io.typefox.lsapi.util.LsapiFactories.* // end completion stuff - // notification callbacks + // symbols - override onShowMessage(NotificationCallback callback) { - // TODO: auto-generated method stub + override documentSymbol(DocumentSymbolParams params) { + val uri = params.textDocument.uri.toUri + val resourceServiceProvider = uri.resourceServiceProvider + val documentSymbolService = resourceServiceProvider?.get(DocumentSymbolService) + if (documentSymbolService === null) + return emptyList + + return workspaceManager.doRead(uri) [ document, resource | + return documentSymbolService.getSymbols(resource) + ] } - override onShowMessageRequest(NotificationCallback callback) { - // TODO: auto-generated method stub - } - - override onLogMessage(NotificationCallback callback) { - // TODO: auto-generated method stub - } - - // end notification callbacks + // end symbols override symbol(WorkspaceSymbolParams params) { throw new UnsupportedOperationException("TODO: auto-generated method stub") @@ -247,10 +265,6 @@ import static io.typefox.lsapi.util.LsapiFactories.* throw new UnsupportedOperationException("TODO: auto-generated method stub") } - override documentSymbol(DocumentSymbolParams params) { - throw new UnsupportedOperationException("TODO: auto-generated method stub") - } - override codeAction(CodeActionParams params) { throw new UnsupportedOperationException("TODO: auto-generated method stub") } diff --git a/plugins/org.eclipse.xtext.ide/src/org/eclipse/xtext/ide/server/symbol/DocumentSymbolService.xtend b/plugins/org.eclipse.xtext.ide/src/org/eclipse/xtext/ide/server/symbol/DocumentSymbolService.xtend new file mode 100644 index 000000000..ba2609d08 --- /dev/null +++ b/plugins/org.eclipse.xtext.ide/src/org/eclipse/xtext/ide/server/symbol/DocumentSymbolService.xtend @@ -0,0 +1,74 @@ +/******************************************************************************* + * Copyright (c) 2016 TypeFox GmbH (http://www.typefox.io) and others. + * 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 + *******************************************************************************/ +package org.eclipse.xtext.ide.server.symbol + +import com.google.inject.Inject +import com.google.inject.Singleton +import io.typefox.lsapi.SymbolInformation +import io.typefox.lsapi.SymbolInformationImpl +import java.util.List +import org.eclipse.emf.ecore.EObject +import org.eclipse.xtext.ide.server.DocumentExtensions +import org.eclipse.xtext.naming.IQualifiedNameProvider +import org.eclipse.xtext.resource.XtextResource + +import static extension org.eclipse.emf.ecore.util.EcoreUtil.* + +/** + * @author kosyakov - Initial contribution and API + */ +@Singleton +class DocumentSymbolService { + + @Inject + extension DocumentExtensions + + @Inject + extension IQualifiedNameProvider qualifiedNameProvider + + def List getSymbols(XtextResource resource) { + val symbols = newLinkedHashMap + val contents = resource.getAllProperContents(true) + while (contents.hasNext) { + val obj = contents.next + val symbol = obj.createSymbol + if (symbol !== null) { + symbols.put(obj, symbol) + + val container = obj.container + val containerSymbol = symbols.get(container) + symbol.container = containerSymbol?.name + } + } + return symbols.values.toList + } + + protected def EObject getContainer(EObject obj) { + return obj.eContainer + } + + protected def SymbolInformationImpl createSymbol(EObject object) { + val symbolName = object.symbolName + if(symbolName === null) return null + + val symbol = new SymbolInformationImpl + symbol.name = symbolName + symbol.kind = object.symbolKind + symbol.location = object.newLocation + return symbol + } + + protected def String getSymbolName(EObject object) { + return object.fullyQualifiedName?.toString + } + + protected def int getSymbolKind(EObject object) { + return 0 + } + +} diff --git a/tests/org.eclipse.xtext.ide.tests/src/org/eclipse/xtext/ide/tests/server/AbstractLanguageServerTest.xtend b/tests/org.eclipse.xtext.ide.tests/src/org/eclipse/xtext/ide/tests/server/AbstractLanguageServerTest.xtend index 29022b761..2d3a47564 100644 --- a/tests/org.eclipse.xtext.ide.tests/src/org/eclipse/xtext/ide/tests/server/AbstractLanguageServerTest.xtend +++ b/tests/org.eclipse.xtext.ide.tests/src/org/eclipse/xtext/ide/tests/server/AbstractLanguageServerTest.xtend @@ -10,12 +10,21 @@ package org.eclipse.xtext.ide.tests.server import com.google.inject.Guice import com.google.inject.Inject import io.typefox.lsapi.Diagnostic +import io.typefox.lsapi.DidOpenTextDocumentParamsImpl import io.typefox.lsapi.InitializeParamsImpl import io.typefox.lsapi.InitializeResult +import io.typefox.lsapi.Location import io.typefox.lsapi.NotificationCallback +import io.typefox.lsapi.Position import io.typefox.lsapi.PublishDiagnosticsParams +import io.typefox.lsapi.Range +import io.typefox.lsapi.TextDocumentIdentifierImpl +import io.typefox.lsapi.TextDocumentItemImpl import java.io.File import java.io.FileWriter +import java.net.URI +import java.nio.file.Path +import java.nio.file.Paths import java.util.List import java.util.Map import org.eclipse.xtext.ide.server.LanguageServerImpl @@ -28,53 +37,84 @@ import org.junit.Before * @author Sven Efftinge - Initial contribution and API */ class AbstractLanguageServerTest implements NotificationCallback { - - @Before - def void setup() { - val injector = Guice.createInjector(new ServerModule()) - injector.injectMembers(this) - // register notification callbacks - languageServer.getTextDocumentService.onPublishDiagnostics(this) - - // create workingdir - root = new File("./test-data/test-project") - if (!root.mkdirs) { - Files.cleanFolder(root, null, true, false) - } - root.deleteOnExit - } - - @Inject extension UriExtensions - @Inject protected LanguageServerImpl languageServer - protected Map> diagnostics = newHashMap() - - protected File root - + + @Before + def void setup() { + val injector = Guice.createInjector(new ServerModule()) + injector.injectMembers(this) + // register notification callbacks + languageServer.getTextDocumentService.onPublishDiagnostics(this) + + // create workingdir + root = new File("./test-data/test-project") + if (!root.mkdirs) { + Files.cleanFolder(root, null, true, false) + } + root.deleteOnExit + } + + @Inject extension UriExtensions + @Inject protected LanguageServerImpl languageServer + protected Map> diagnostics = newHashMap() + + protected File root + + protected def Path getRootPath() { + root.toPath().toAbsolutePath().normalize() + } + + protected def Path relativize(String uri) { + val path = Paths.get(new URI(uri)) + rootPath.relativize(path) + } + protected def InitializeResult initialize() { return initialize(null) } - + protected def InitializeResult initialize((InitializeParamsImpl)=>void initializer) { val params = new InitializeParamsImpl - params.rootPath = root.toPath.toAbsolutePath.normalize.toString + params.rootPath = rootPath.toString initializer?.apply(params) return languageServer.initialize(params) } - - def String ->(String path, CharSequence contents) { - val file = new File(root, path) - file.parentFile.mkdirs - file.createNewFile - - val writer = new FileWriter(file) - writer.write(contents.toString) - writer.close + + protected def void open(String fileUri, String model) { + languageServer.didOpen(new DidOpenTextDocumentParamsImpl => [ + textDocument = new TextDocumentItemImpl => [ + uri = fileUri + version = 1 + text = model + ] + ]) + } + + def String ->(String path, CharSequence contents) { + val file = new File(root, path) + file.parentFile.mkdirs + file.createNewFile + + val writer = new FileWriter(file) + writer.write(contents.toString) + writer.close return file.toURI.normalize.toPath - } - - override call(PublishDiagnosticsParams t) { - diagnostics.put(t.uri, t.diagnostics) - } - -} \ No newline at end of file + } + + override call(PublishDiagnosticsParams t) { + diagnostics.put(t.uri, t.diagnostics) + } + + protected def TextDocumentIdentifierImpl createIdentifier(String uri) { + val identifier = new TextDocumentIdentifierImpl + identifier.uri = uri + return identifier + } + + protected def String toExpectation(Location it) '''«uri.relativize» «range.toExpectation»''' + + protected def String toExpectation(Range it) '''[«start.toExpectation» .. «end.toExpectation»]''' + + protected def String toExpectation(Position it) '''[«line», «character»]''' + +} diff --git a/tests/org.eclipse.xtext.ide.tests/src/org/eclipse/xtext/ide/tests/server/CompletionTest.xtend b/tests/org.eclipse.xtext.ide.tests/src/org/eclipse/xtext/ide/tests/server/CompletionTest.xtend index 613dd6164..8bd404642 100644 --- a/tests/org.eclipse.xtext.ide.tests/src/org/eclipse/xtext/ide/tests/server/CompletionTest.xtend +++ b/tests/org.eclipse.xtext.ide.tests/src/org/eclipse/xtext/ide/tests/server/CompletionTest.xtend @@ -8,15 +8,12 @@ package org.eclipse.xtext.ide.tests.server import io.typefox.lsapi.CompletionItem -import io.typefox.lsapi.DidOpenTextDocumentParamsImpl -import io.typefox.lsapi.PositionImpl -import io.typefox.lsapi.TextDocumentIdentifierImpl -import io.typefox.lsapi.TextDocumentItemImpl import io.typefox.lsapi.TextDocumentPositionParamsImpl import java.util.List import org.eclipse.xtend.lib.annotations.Accessors import org.junit.Test +import static io.typefox.lsapi.util.LsapiFactories.* import static org.junit.Assert.* /** @@ -37,7 +34,7 @@ class CompletionTest extends AbstractLanguageServerTest { def void testCompletion_02() { testCompletion [ model = 'type ' - caretColumn = 5 + column = 5 expectedCompletionItems = ''' name (ID) ''' @@ -51,8 +48,8 @@ class CompletionTest extends AbstractLanguageServerTest { type Foo {} type Bar {} ''' - caretLine = 1 - caretColumn = 'type Bar {'.length + line = 1 + column = 'type Bar {'.length expectedCompletionItems = ''' Bar (TypeDeclaration) Foo (TypeDeclaration) @@ -71,23 +68,11 @@ class CompletionTest extends AbstractLanguageServerTest { val fileUri = filePath -> model initialize - - languageServer.didOpen(new DidOpenTextDocumentParamsImpl => [ - textDocument = new TextDocumentItemImpl => [ - uri = fileUri - version = 1 - text = model - ] - ]) + open(fileUri, model) val completionItems = languageServer.completion(new TextDocumentPositionParamsImpl => [ - textDocument = new TextDocumentIdentifierImpl => [ - uri = fileUri - ] - position = new PositionImpl => [ - line = caretLine - character = caretColumn - ] + textDocument = fileUri.createIdentifier + position = newPosition(line, column) ]) val actualCompletionItems = toExpectation(completionItems) @@ -108,8 +93,8 @@ class CompletionTest extends AbstractLanguageServerTest { static class TestCompletionConfiguration { String model = '' String filePath = 'MyModel.testlang' - int caretLine = 0 - int caretColumn = 0 + int line = 0 + int column = 0 String expectedCompletionItems = '' } diff --git a/tests/org.eclipse.xtext.ide.tests/src/org/eclipse/xtext/ide/tests/server/DocumentSymbolTest.xtend b/tests/org.eclipse.xtext.ide.tests/src/org/eclipse/xtext/ide/tests/server/DocumentSymbolTest.xtend new file mode 100644 index 000000000..8f0612dfc --- /dev/null +++ b/tests/org.eclipse.xtext.ide.tests/src/org/eclipse/xtext/ide/tests/server/DocumentSymbolTest.xtend @@ -0,0 +1,100 @@ +/******************************************************************************* + * Copyright (c) 2016 TypeFox GmbH (http://www.typefox.io) and others. + * 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 + *******************************************************************************/ +package org.eclipse.xtext.ide.tests.server + +import io.typefox.lsapi.DocumentSymbolParamsImpl +import io.typefox.lsapi.SymbolInformation +import java.util.List +import org.eclipse.xtend.lib.annotations.Accessors +import org.junit.Test + +import static org.junit.Assert.* + +/** + * @author kosyakov - Initial contribution and API + */ +class DocumentSymbolTest extends AbstractLanguageServerTest { + + @Test + def void testDocumentSymbol_01() { + testDocumentSymbol[ + model = ''' + type Foo { + int bar + } + type Bar { + Foo foo + } + ''' + expectedSymbols = ''' + symbol "Foo" { + kind: 0 + location: MyModel.testlang [[0, 5] .. [0, 8]] + } + symbol "Foo.bar" { + kind: 0 + location: MyModel.testlang [[1, 5] .. [1, 8]] + container: "Foo" + } + symbol "Foo.bar.int" { + kind: 0 + location: MyModel.testlang [[1, 1] .. [1, 4]] + container: "Foo.bar" + } + symbol "Bar" { + kind: 0 + location: MyModel.testlang [[3, 5] .. [3, 8]] + } + symbol "Bar.foo" { + kind: 0 + location: MyModel.testlang [[4, 5] .. [4, 8]] + container: "Bar" + } + ''' + ] + } + + protected def void testDocumentSymbol((DocumentSymbolConfiguraiton)=>void configurator) { + val extension configuration = new DocumentSymbolConfiguraiton + configurator.apply(configuration) + val fileUri = filePath -> model + + initialize + open(fileUri, model) + + val symbols = languageServer.documentSymbol(new DocumentSymbolParamsImpl => [ + textDocument = fileUri.createIdentifier + ]) + val String actualSymbols = symbols.toExpectation + assertEquals(expectedSymbols, actualSymbols) + } + + @Accessors + static class DocumentSymbolConfiguraiton { + String model = '' + String filePath = 'MyModel.testlang' + String expectedSymbols = '' + } + + protected def String toExpectation(List symbols) ''' + «FOR symbol : symbols» + «symbol.toExpectation» + «ENDFOR» + ''' + + protected def String toExpectation(T it) ''' + symbol "«name»" { + kind: «kind» + location: «location.toExpectation» + «IF !container.nullOrEmpty» + container: "«container»" + «ENDIF» + } + ''' + +}