[lsi] Document Symbol Support

Change-Id: Idf75084a4debf56c46d49fc8db579d400a8eaef0
Signed-off-by: akosyakov <anton.kosyakov@typefox.io>
This commit is contained in:
akosyakov 2016-05-26 16:14:28 +02:00
parent d5ba9854e1
commit db5608603d
6 changed files with 359 additions and 80 deletions

View file

@ -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
}
}

View file

@ -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<MessageParams> callback) {
// TODO: auto-generated method stub
}
override onShowMessageRequest(NotificationCallback<ShowMessageRequestParams> callback) {
// TODO: auto-generated method stub
}
override onLogMessage(NotificationCallback<MessageParams> 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<MessageParams> 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<ShowMessageRequestParams> callback) {
// TODO: auto-generated method stub
}
override onLogMessage(NotificationCallback<MessageParams> 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")
}

View file

@ -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<? extends SymbolInformation> 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
}
}

View file

@ -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<PublishDiagnosticsParams> {
@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<String, List<? extends Diagnostic>> 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<String, List<? extends Diagnostic>> 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)
}
}
}
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»]'''
}

View file

@ -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 = ''
}

View file

@ -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<? extends SymbolInformation> symbols) '''
«FOR symbol : symbols»
«symbol.toExpectation»
«ENDFOR»
'''
protected def <T extends SymbolInformation> String toExpectation(T it) '''
symbol "«name»" {
kind: «kind»
location: «location.toExpectation»
«IF !container.nullOrEmpty»
container: "«container»"
«ENDIF»
}
'''
}