mirror of
https://github.com/sigmasternchen/xtext-core
synced 2025-03-16 00:38:56 +00:00
[lsi] Document Symbol Support
Change-Id: Idf75084a4debf56c46d49fc8db579d400a8eaef0 Signed-off-by: akosyakov <anton.kosyakov@typefox.io>
This commit is contained in:
parent
d5ba9854e1
commit
db5608603d
6 changed files with 359 additions and 80 deletions
|
@ -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
|
||||
}
|
||||
|
||||
}
|
|
@ -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")
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
}
|
|
@ -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»]'''
|
||||
|
||||
}
|
||||
|
|
|
@ -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 = ''
|
||||
}
|
||||
|
||||
|
|
|
@ -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»
|
||||
}
|
||||
'''
|
||||
|
||||
}
|
Loading…
Reference in a new issue