Skip to content

Commit a2b68ab

Browse files
authored
Merge pull request #393 from themkat/GH-208
Implement textDocument/documentHighlight
2 parents e62586f + 96b8f8e commit a2b68ab

File tree

9 files changed

+229
-21
lines changed

9 files changed

+229
-21
lines changed

server/src/main/kotlin/org/javacs/kt/CompiledFile.kt

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,18 +4,21 @@ import com.intellij.openapi.util.TextRange
44
import com.intellij.psi.PsiElement
55
import org.javacs.kt.compiler.CompilationKind
66
import org.javacs.kt.position.changedRegion
7+
import org.javacs.kt.position.location
78
import org.javacs.kt.position.position
89
import org.javacs.kt.util.findParent
910
import org.javacs.kt.util.nullResult
1011
import org.javacs.kt.util.toPath
1112
import org.jetbrains.kotlin.descriptors.DeclarationDescriptor
1213
import org.jetbrains.kotlin.descriptors.ModuleDescriptor
1314
import org.jetbrains.kotlin.lexer.KtTokens
15+
import org.jetbrains.kotlin.js.resolve.diagnostics.findPsi
1416
import org.jetbrains.kotlin.psi.*
1517
import org.jetbrains.kotlin.psi.psiUtil.parentsWithSelf
1618
import org.jetbrains.kotlin.resolve.BindingContext
1719
import org.jetbrains.kotlin.resolve.scopes.LexicalScope
1820
import org.jetbrains.kotlin.types.KotlinType
21+
import org.eclipse.lsp4j.Location
1922
import java.nio.file.Paths
2023

2124
class CompiledFile(
@@ -171,6 +174,25 @@ class CompiledFile(
171174
return psi.findParent<KtElement>()
172175
}
173176

177+
178+
/**
179+
* Find the declaration of the element at the cursor. Only works if the element at the cursor is a reference.
180+
*/
181+
fun findDeclaration(cursor: Int): Pair<KtNamedDeclaration, Location>? {
182+
val (_, target) = referenceAtPoint(cursor) ?: return null
183+
val psi = target.findPsi()
184+
185+
return if (psi is KtNamedDeclaration) {
186+
psi.nameIdentifier?.let {
187+
location(it)?.let { location ->
188+
Pair(psi, location)
189+
}
190+
}
191+
} else {
192+
null
193+
}
194+
}
195+
174196
/**
175197
* Find the lexical-scope surrounding `cursor`.
176198
* This may be out-of-date if the user is typing quickly.

server/src/main/kotlin/org/javacs/kt/KotlinLanguageServer.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,7 @@ class KotlinLanguageServer : LanguageServer, LanguageClientAware, Closeable {
8787
serverCapabilities.documentFormattingProvider = Either.forLeft(true)
8888
serverCapabilities.documentRangeFormattingProvider = Either.forLeft(true)
8989
serverCapabilities.executeCommandProvider = ExecuteCommandOptions(ALL_COMMANDS)
90+
serverCapabilities.documentHighlightProvider = Either.forLeft(true)
9091

9192
val clientCapabilities = params.capabilities
9293
config.completion.snippets.enabled = clientCapabilities?.textDocument?.completion?.completionItem?.snippetSupport ?: false

server/src/main/kotlin/org/javacs/kt/KotlinTextDocumentService.kt

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import org.javacs.kt.util.parseURI
2626
import org.javacs.kt.util.describeURI
2727
import org.javacs.kt.util.describeURIs
2828
import org.javacs.kt.rename.renameSymbol
29+
import org.javacs.kt.highlight.documentHighlightsAt
2930
import org.jetbrains.kotlin.resolve.diagnostics.Diagnostics
3031
import java.net.URI
3132
import java.io.Closeable
@@ -103,8 +104,9 @@ class KotlinTextDocumentService(
103104
}
104105
}
105106

106-
override fun documentHighlight(position: DocumentHighlightParams): CompletableFuture<List<DocumentHighlight>> {
107-
TODO("not implemented")
107+
override fun documentHighlight(position: DocumentHighlightParams): CompletableFuture<List<DocumentHighlight>> = async.compute {
108+
val (file, cursor) = recover(position.textDocument.uri, position.position, Recompile.NEVER)
109+
documentHighlightsAt(file, cursor)
108110
}
109111

110112
override fun onTypeFormatting(params: DocumentOnTypeFormattingParams): CompletableFuture<List<TextEdit>> {
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
package org.javacs.kt.highlight
2+
3+
import org.eclipse.lsp4j.DocumentHighlight
4+
import org.eclipse.lsp4j.DocumentHighlightKind
5+
import org.eclipse.lsp4j.Location
6+
import org.javacs.kt.CompiledFile
7+
import org.javacs.kt.position.range
8+
import org.javacs.kt.references.findReferencesToDeclarationInFile
9+
import org.javacs.kt.util.findParent
10+
import org.jetbrains.kotlin.psi.KtFile
11+
import org.jetbrains.kotlin.psi.KtNamedDeclaration
12+
13+
fun documentHighlightsAt(file: CompiledFile, cursor: Int): List<DocumentHighlight> {
14+
val (declaration, declarationLocation) = file.findDeclaration(cursor)
15+
?: file.findDeclarationCursorSite(cursor)
16+
?: return emptyList()
17+
val references = findReferencesToDeclarationInFile(declaration, file)
18+
19+
return if (declaration.isInFile(file.parse)) {
20+
listOf(DocumentHighlight(declarationLocation.range, DocumentHighlightKind.Text))
21+
} else {
22+
emptyList()
23+
} + references.map { DocumentHighlight(it, DocumentHighlightKind.Text) }
24+
}
25+
26+
private fun CompiledFile.findDeclarationCursorSite(cursor: Int): Pair<KtNamedDeclaration, Location>? {
27+
// current symbol might be a declaration. This function is used as a fallback when
28+
// findDeclaration fails
29+
val declaration = elementAtPoint(cursor)?.findParent<KtNamedDeclaration>()
30+
31+
return declaration?.let {
32+
// in this scenario we know that the declaration will be at the cursor site, so uri is not
33+
// important
34+
Pair(it,
35+
Location("",
36+
range(content, it.nameIdentifier?.textRange ?: return null)))
37+
}
38+
}
39+
40+
private fun KtNamedDeclaration.isInFile(file: KtFile) = this.containingFile == file

server/src/main/kotlin/org/javacs/kt/references/FindReferences.kt

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package org.javacs.kt.references
22

33
import org.eclipse.lsp4j.Location
4+
import org.eclipse.lsp4j.Range
45
import org.javacs.kt.LOG
56
import org.javacs.kt.SourcePath
67
import org.javacs.kt.position.location
@@ -9,6 +10,7 @@ import org.javacs.kt.util.emptyResult
910
import org.javacs.kt.util.findParent
1011
import org.javacs.kt.util.preOrderTraversal
1112
import org.javacs.kt.util.toPath
13+
import org.javacs.kt.CompiledFile
1214
import org.jetbrains.kotlin.descriptors.ClassConstructorDescriptor
1315
import org.jetbrains.kotlin.descriptors.ConstructorDescriptor
1416
import org.jetbrains.kotlin.descriptors.DeclarationDescriptor
@@ -61,6 +63,28 @@ private fun doFindReferences(element: KtNamedDeclaration, sp: SourcePath): Colle
6163
}
6264
}
6365

66+
/**
67+
* Finds references to the named declaration in the given file. The declaration may or may not reside in another file.
68+
*
69+
* @returns ranges of references in the file. Empty list if none are found
70+
*/
71+
fun findReferencesToDeclarationInFile(declaration: KtNamedDeclaration, file: CompiledFile): List<Range> {
72+
val descriptor = file.compile[BindingContext.DECLARATION_TO_DESCRIPTOR, declaration] ?: return emptyResult("Declaration ${declaration.fqName} has no descriptor")
73+
val bindingContext = file.compile
74+
75+
val references = when {
76+
isComponent(descriptor) -> findComponentReferences(declaration, bindingContext) + findNameReferences(declaration, bindingContext)
77+
isIterator(descriptor) -> findIteratorReferences(declaration, bindingContext) + findNameReferences(declaration, bindingContext)
78+
isPropertyDelegate(descriptor) -> findDelegateReferences(declaration, bindingContext) + findNameReferences(declaration, bindingContext)
79+
else -> findNameReferences(declaration, bindingContext)
80+
}
81+
82+
return references.map {
83+
location(it)?.range
84+
}.filterNotNull()
85+
.sortedWith(compareBy({ it.start.line }))
86+
}
87+
6488
private fun findNameReferences(element: KtNamedDeclaration, recompile: BindingContext): List<KtReferenceExpression> {
6589
val references = recompile.getSliceContents(BindingContext.REFERENCE_TARGET)
6690

server/src/main/kotlin/org/javacs/kt/rename/Rename.kt

Lines changed: 1 addition & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,10 @@ import org.eclipse.lsp4j.*
44
import org.eclipse.lsp4j.jsonrpc.messages.Either
55
import org.javacs.kt.CompiledFile
66
import org.javacs.kt.SourcePath
7-
import org.javacs.kt.position.location
87
import org.javacs.kt.references.findReferences
9-
import org.jetbrains.kotlin.js.resolve.diagnostics.findPsi
10-
import org.jetbrains.kotlin.psi.KtNamedDeclaration
118

129
fun renameSymbol(file: CompiledFile, cursor: Int, sp: SourcePath, newName: String): WorkspaceEdit? {
13-
val (declaration, location) = findDeclaration(file, cursor) ?: return null
10+
val (declaration, location) = file.findDeclaration(cursor) ?: return null
1411
return declaration.let {
1512
val declarationEdit = Either.forLeft<TextDocumentEdit, ResourceOperation>(TextDocumentEdit(
1613
VersionedTextDocumentIdentifier().apply { uri = location.uri },
@@ -27,18 +24,3 @@ fun renameSymbol(file: CompiledFile, cursor: Int, sp: SourcePath, newName: Strin
2724
WorkspaceEdit(listOf(declarationEdit) + referenceEdits)
2825
}
2926
}
30-
31-
private fun findDeclaration(file: CompiledFile, cursor: Int): Pair<KtNamedDeclaration, Location>? {
32-
val (_, target) = file.referenceAtPoint(cursor) ?: return null
33-
val psi = target.findPsi()
34-
35-
return if (psi is KtNamedDeclaration) {
36-
psi.nameIdentifier?.let {
37-
location(it)?.let { location ->
38-
Pair(psi, location)
39-
}
40-
}
41-
} else {
42-
null
43-
}
44-
}
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
package org.javacs.kt
2+
3+
import org.eclipse.lsp4j.DocumentHighlight
4+
import org.eclipse.lsp4j.DocumentHighlightKind
5+
import org.eclipse.lsp4j.DocumentHighlightParams
6+
import org.eclipse.lsp4j.TextDocumentIdentifier
7+
import org.eclipse.lsp4j.Position
8+
import org.hamcrest.Matchers.hasSize
9+
import org.hamcrest.Matchers.equalTo
10+
import org.junit.Assert.assertThat
11+
import org.junit.Test
12+
13+
class DocumentHighlightTest : SingleFileTestFixture("highlight", "DocumentHighlight.kt") {
14+
15+
@Test
16+
fun `should highlight input to function`() {
17+
val fileUri = workspaceRoot.resolve(file).toUri().toString()
18+
val input = DocumentHighlightParams(TextDocumentIdentifier(fileUri), Position(4, 20))
19+
val result = languageServer.textDocumentService.documentHighlight(input).get()
20+
21+
assertThat(result, hasSize(2))
22+
val firstHighlight = result[0]
23+
assertThat(firstHighlight.range, equalTo(range(3, 14, 3, 19)))
24+
assertThat(firstHighlight.kind, equalTo(DocumentHighlightKind.Text))
25+
26+
val secondHighlight = result[1]
27+
assertThat(secondHighlight.range, equalTo(range(5, 20, 5, 25)))
28+
assertThat(secondHighlight.kind, equalTo(DocumentHighlightKind.Text))
29+
}
30+
31+
@Test
32+
fun `should highlight global variable`() {
33+
val fileUri = workspaceRoot.resolve(file).toUri().toString()
34+
val input = DocumentHighlightParams(TextDocumentIdentifier(fileUri), Position(3, 23))
35+
val result = languageServer.textDocumentService.documentHighlight(input).get()
36+
37+
assertThat(result, hasSize(3))
38+
val firstHighlight = result[0]
39+
assertThat(firstHighlight.range, equalTo(range(1, 5, 1, 14)))
40+
assertThat(firstHighlight.kind, equalTo(DocumentHighlightKind.Text))
41+
42+
val secondHighlight = result[1]
43+
assertThat(secondHighlight.range, equalTo(range(4, 23, 4, 32)))
44+
assertThat(secondHighlight.kind, equalTo(DocumentHighlightKind.Text))
45+
46+
val thirdHighlight = result[2]
47+
assertThat(thirdHighlight.range, equalTo(range(8, 13, 8, 22)))
48+
assertThat(thirdHighlight.kind, equalTo(DocumentHighlightKind.Text))
49+
}
50+
51+
@Test
52+
fun `should highlight global variable when marked from declaration site`() {
53+
val fileUri = workspaceRoot.resolve(file).toUri().toString()
54+
val input = DocumentHighlightParams(TextDocumentIdentifier(fileUri), Position(0, 6))
55+
val result = languageServer.textDocumentService.documentHighlight(input).get()
56+
57+
assertThat(result, hasSize(3))
58+
val firstHighlight = result[0]
59+
assertThat(firstHighlight.range, equalTo(range(1, 5, 1, 14)))
60+
assertThat(firstHighlight.kind, equalTo(DocumentHighlightKind.Text))
61+
62+
val secondHighlight = result[1]
63+
assertThat(secondHighlight.range, equalTo(range(4, 23, 4, 32)))
64+
assertThat(secondHighlight.kind, equalTo(DocumentHighlightKind.Text))
65+
66+
val thirdHighlight = result[2]
67+
assertThat(thirdHighlight.range, equalTo(range(8, 13, 8, 22)))
68+
assertThat(thirdHighlight.kind, equalTo(DocumentHighlightKind.Text))
69+
}
70+
71+
@Test
72+
fun `should highlight symbols in current file where declaration is in another file`() {
73+
val fileUri = workspaceRoot.resolve(file).toUri().toString()
74+
val input = DocumentHighlightParams(TextDocumentIdentifier(fileUri), Position(4, 48))
75+
val result = languageServer.textDocumentService.documentHighlight(input).get()
76+
77+
assertThat(result, hasSize(2))
78+
val firstHighlight = result[0]
79+
assertThat(firstHighlight.range, equalTo(range(5, 49, 5, 67)))
80+
assertThat(firstHighlight.kind, equalTo(DocumentHighlightKind.Text))
81+
82+
val secondHighlight = result[1]
83+
assertThat(secondHighlight.range, equalTo(range(9, 13, 9, 31)))
84+
assertThat(secondHighlight.kind, equalTo(DocumentHighlightKind.Text))
85+
}
86+
87+
@Test
88+
fun `should highlight shadowed variable correctly, just show the shadowed variable`() {
89+
val fileUri = workspaceRoot.resolve(file).toUri().toString()
90+
val input = DocumentHighlightParams(TextDocumentIdentifier(fileUri), Position(13, 14))
91+
val result = languageServer.textDocumentService.documentHighlight(input).get()
92+
93+
assertThat(result, hasSize(2))
94+
val firstHighlight = result[0]
95+
assertThat(firstHighlight.range, equalTo(range(13, 15, 13, 24)))
96+
assertThat(firstHighlight.kind, equalTo(DocumentHighlightKind.Text))
97+
98+
val secondHighlight = result[1]
99+
assertThat(secondHighlight.range, equalTo(range(14, 13, 14, 22)))
100+
assertThat(secondHighlight.kind, equalTo(DocumentHighlightKind.Text))
101+
}
102+
103+
@Test
104+
fun `should highlight function reference correctly`() {
105+
val fileUri = workspaceRoot.resolve(file).toUri().toString()
106+
val input = DocumentHighlightParams(TextDocumentIdentifier(fileUri), Position(2, 6))
107+
val result = languageServer.textDocumentService.documentHighlight(input).get()
108+
109+
assertThat(result, hasSize(2))
110+
val firstHighlight = result[0]
111+
assertThat(firstHighlight.range, equalTo(range(3, 5, 3, 13)))
112+
assertThat(firstHighlight.kind, equalTo(DocumentHighlightKind.Text))
113+
114+
val secondHighlight = result[1]
115+
assertThat(secondHighlight.range, equalTo(range(15, 5, 15, 13)))
116+
assertThat(secondHighlight.kind, equalTo(DocumentHighlightKind.Text))
117+
}
118+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
val globalval = 23
2+
3+
fun somefunc(input: String) {
4+
val calculation = globalval + 1
5+
val otherval = input.length + calculation + somevalinotherfile
6+
7+
println(otherval)
8+
println(globalval)
9+
println(somevalinotherfile)
10+
}
11+
12+
// test shadowing of the global variable
13+
fun somefunc2(globalval: String) {
14+
println(globalval)
15+
somefunc("")
16+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
val somevalinotherfile = 42
2+
val otherval = somevalinotherfile
3+
println(somevalinotherfile)

0 commit comments

Comments
 (0)