Skip to content

Commit 49782ed

Browse files
committed
Implementation of textDocument/documentHighlight
1 parent 86fd15d commit 49782ed

File tree

8 files changed

+194
-3
lines changed

8 files changed

+194
-3
lines changed

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: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
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.rename.findDeclaration
10+
import org.javacs.kt.util.findParent
11+
import org.jetbrains.kotlin.psi.KtFile
12+
import org.jetbrains.kotlin.psi.KtNamedDeclaration
13+
14+
fun documentHighlightsAt(file: CompiledFile, cursor: Int): List<DocumentHighlight> {
15+
val (declaration, declarationLocation) = findDeclaration(file, cursor)
16+
?: findDeclarationCursorSite(file, cursor)
17+
?: return emptyList()
18+
val references = findReferencesToDeclarationInFile(declaration, file)
19+
20+
return if (declaration.isInFile(file.parse)) {
21+
listOf(DocumentHighlight(declarationLocation.range, DocumentHighlightKind.Text))
22+
} else {
23+
emptyList()
24+
} + references.map { DocumentHighlight(it, DocumentHighlightKind.Text) }
25+
}
26+
27+
private fun findDeclarationCursorSite(
28+
file: CompiledFile,
29+
cursor: Int
30+
): Pair<KtNamedDeclaration, Location>? {
31+
// current symbol might be a declaration. This function is used as a fallback when
32+
// findDeclaration fails
33+
val declaration = file.elementAtPoint(cursor)?.findParent<KtNamedDeclaration>()
34+
35+
return declaration?.let {
36+
// in this scenario we know that the declaration will be at the cursor site, so uri is not
37+
// important
38+
Pair(it,
39+
Location("",
40+
range(file.content, it.nameIdentifier?.textRangeInParent ?: return null)))
41+
}
42+
}
43+
44+
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 & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ fun renameSymbol(file: CompiledFile, cursor: Int, sp: SourcePath, newName: Strin
2828
}
2929
}
3030

31-
private fun findDeclaration(file: CompiledFile, cursor: Int): Pair<KtNamedDeclaration, Location>? {
31+
fun findDeclaration(file: CompiledFile, cursor: Int): Pair<KtNamedDeclaration, Location>? {
3232
val (_, target) = file.referenceAtPoint(cursor) ?: return null
3333
val psi = target.findPsi()
3434

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
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, 14, 13, 23)))
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+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
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 somefunc(globalval: String) {
14+
println(globalval)
15+
}
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)