Skip to content

Commit 466618a

Browse files
darthorimarintellij-monorepo-bot
authored andcommitted
[lsp] implement textDocument/semanticTokens/range request
^LSP-89 fixed GitOrigin-RevId: 9eb57a3200039e92f51fa1fec1cc08626d534896
1 parent 32b929d commit 466618a

File tree

10 files changed

+187
-12
lines changed

10 files changed

+187
-12
lines changed

api.core/BUILD.bazel

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,8 @@ jvm_library(
3939
"@community//fleet/lsp.protocol",
4040
"@lib//:junit5",
4141
"@lib//:junit5Jupiter",
42+
"@community//platform/testFramework",
43+
"@community//platform/testFramework:testFramework_test_lib",
4244
]
4345
)
4446

api.core/language-server.api.core.iml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,5 +32,6 @@
3232
<orderEntry type="module" module-name="fleet.lsp.protocol" />
3333
<orderEntry type="library" scope="TEST" name="JUnit5" level="project" />
3434
<orderEntry type="library" scope="TEST" name="JUnit5Jupiter" level="project" />
35+
<orderEntry type="module" module-name="intellij.platform.testFramework" scope="TEST" />
3536
</component>
3637
</module>

api.core/src/com/jetbrains/ls/api/core/util/LSUtil.kt

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,29 @@ import com.jetbrains.lsp.protocol.*
1010
val VirtualFile.uri: URI
1111
get() = url.intellijUriToLspUri()
1212

13-
fun Document.offsetByPosition(position: Position): Int =
14-
getLineStartOffset(position.line) + position.character
13+
/**
14+
* Calculates the absolute offset in the document text based on the given position (line and character offset).
15+
*
16+
* @param position The position in the document, represented by a line number (zero-based) and
17+
* character offset in that line (zero-based).
18+
* @return The absolute offset in the document, which represents the character index corresponding
19+
* to the given position. If the position line is outside the document bounds, returns the document
20+
* end offset. If the character offset is outside the line bounds, returns the line end offset.
21+
*/
22+
fun Document.offsetByPosition(position: Position): Int {
23+
val textLength = textLength
24+
if (position.line >= lineCount) {
25+
// lsp position may be outside the document, which means the end of the document
26+
return textLength
27+
}
28+
val lineStart = getLineStartOffset(position.line)
29+
val lineEnd = getLineEndOffset(position.line)
30+
if (position.character > lineEnd - lineStart) {
31+
// lsp position may be outside the line range, which means the end of the line
32+
return lineEnd
33+
}
34+
return lineStart + position.character
35+
}
1536

1637
fun Document.positionByOffset(offset: Int): Position {
1738
val line = getLineNumber(offset)
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
2+
package com.jetbrains.ls.api.core.util
3+
4+
import com.intellij.mock.MockMultiLineImmutableDocument
5+
import com.intellij.openapi.editor.Document
6+
import com.jetbrains.lsp.protocol.Position
7+
import org.junit.jupiter.api.Assertions.assertEquals
8+
import org.junit.jupiter.api.Test
9+
10+
class PositionsTest {
11+
@Test
12+
fun `should convert offsets on empty document`() {
13+
val document = createDocument()
14+
15+
assertEquals(0, document.offsetByPosition(Position(0, 0)))
16+
assertEquals(0, document.offsetByPosition(Position(10, 20)))
17+
}
18+
19+
@Test
20+
fun `should convert offsets outside the range of the document`() {
21+
val document = createDocument(5, 6)
22+
23+
assertEquals(document.textLength, document.offsetByPosition(Position(1, 6)))
24+
assertEquals(document.textLength, document.offsetByPosition(Position(1, 7)))
25+
assertEquals(document.textLength, document.offsetByPosition(Position(2, 0)))
26+
assertEquals(document.textLength, document.offsetByPosition(Position(2, 10)))
27+
}
28+
29+
30+
@Test
31+
fun `should convert offsets outside line ranges`() {
32+
val document = createDocument(5, 6)
33+
34+
assertEquals(5, document.offsetByPosition(Position(0, 6)))
35+
assertEquals(5, document.offsetByPosition(Position(0, 7)))
36+
assertEquals(5, document.offsetByPosition(Position(0, 100)))
37+
assertEquals(12, document.offsetByPosition(Position(1, 7)))
38+
assertEquals(12, document.offsetByPosition(Position(1, 8)))
39+
assertEquals(12, document.offsetByPosition(Position(1, 100)))
40+
}
41+
42+
43+
@Test
44+
fun `should convert offsets inside the range of the document`() {
45+
val document = createDocument(5, 6)
46+
47+
testInsideOffset(0, Position(0, 0), document)
48+
testInsideOffset(1, Position(0, 1), document)
49+
testInsideOffset(4, Position(0, 4), document)
50+
testInsideOffset(6, Position(1, 0), document)
51+
testInsideOffset(10, Position(1, 4), document)
52+
testInsideOffset(11, Position(1, 5), document)
53+
}
54+
55+
private fun testInsideOffset(
56+
expectedOffset: Int,
57+
position: Position,
58+
document: Document,
59+
) {
60+
val actualOffset = document.offsetByPosition(position)
61+
assertEquals(expectedOffset, actualOffset)
62+
63+
val positionByOffset = document.positionByOffset(actualOffset)
64+
assertEquals(position, positionByOffset)
65+
}
66+
67+
68+
private fun createDocument(
69+
vararg lineLengths: Int
70+
): Document {
71+
return createDocument(lineLengths.toList())
72+
}
73+
74+
private fun createDocument(
75+
lineLengths: List<Int>
76+
): Document {
77+
val char = 'a'
78+
val text = buildString {
79+
for (length in lineLengths) {
80+
repeat(length) {
81+
append(char)
82+
}
83+
if (lineLengths.last() != length) {
84+
append('\n')
85+
}
86+
}
87+
}
88+
return MockMultiLineImmutableDocument(text)
89+
}
90+
}

api.features/src/com/jetbrains/ls/api/features/semanticTokens/LSSemanticTokens.kt

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,18 +6,28 @@ import com.jetbrains.ls.api.features.LSConfiguration
66
import com.jetbrains.ls.api.features.semanticTokens.encoding.SemanticTokensEncoder
77
import com.jetbrains.lsp.protocol.SemanticTokens
88
import com.jetbrains.lsp.protocol.SemanticTokensParams
9+
import com.jetbrains.lsp.protocol.SemanticTokensRangeParams
910

11+
// todo send partial results here
1012
object LSSemanticTokens {
1113
context(LSServer, LSConfiguration)
1214
suspend fun semanticTokensFull(params: SemanticTokensParams): SemanticTokens {
13-
// todo send partial results here
1415
val providers = entriesFor<LSSemanticTokensProvider>(params.textDocument)
1516
val result = providers.flatMap { it.full(params) }
1617
val registry = createRegistry()
1718
val encoded = SemanticTokensEncoder.encode(result, registry)
1819
return SemanticTokens(resultId = null, data = encoded)
1920
}
2021

22+
context(LSServer, LSConfiguration)
23+
suspend fun semanticTokensRange(params: SemanticTokensRangeParams): SemanticTokens {
24+
val providers = entriesFor<LSSemanticTokensProvider>(params.textDocument)
25+
val result = providers.flatMap { it.range(params) }
26+
val registry = createRegistry()
27+
val encoded = SemanticTokensEncoder.encode(result, registry)
28+
return SemanticTokens(resultId = null, data = encoded)
29+
}
30+
2131
context(LSConfiguration)
2232
fun createRegistry(): LSSemanticTokenRegistry {
2333
val registries = entries<LSSemanticTokensProvider>().map { it.createRegistry() }

api.features/src/com/jetbrains/ls/api/features/semanticTokens/LSSemanticTokensProvider.kt

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,9 @@
22
package com.jetbrains.ls.api.features.semanticTokens
33

44
import com.jetbrains.ls.api.core.LSServer
5-
import com.jetbrains.ls.api.features.LSConfigurationEntry
65
import com.jetbrains.ls.api.features.LSLanguageSpecificConfigurationEntry
76
import com.jetbrains.lsp.protocol.SemanticTokensParams
7+
import com.jetbrains.lsp.protocol.SemanticTokensRangeParams
88

99
interface LSSemanticTokensProvider : LSLanguageSpecificConfigurationEntry {
1010
fun createRegistry(): LSSemanticTokenRegistry
@@ -14,4 +14,10 @@ interface LSSemanticTokensProvider : LSLanguageSpecificConfigurationEntry {
1414
*/
1515
context(LSServer)
1616
suspend fun full(params: SemanticTokensParams): List<LSSemanticTokenWithRange>
17+
18+
/**
19+
* LSP method `textDocument/semanticTokens/range`
20+
*/
21+
context(LSServer)
22+
suspend fun range(params: SemanticTokensRangeParams): List<LSSemanticTokenWithRange>
1723
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
2+
package com.jetbrains.ls.api.features.utils
3+
4+
import com.intellij.openapi.editor.Document
5+
import com.intellij.psi.PsiElement
6+
import com.intellij.psi.PsiFile
7+
import com.intellij.psi.PsiWhiteSpace
8+
import com.intellij.psi.util.descendantsOfType
9+
import com.jetbrains.ls.api.core.util.toTextRange
10+
import com.jetbrains.lsp.protocol.Range
11+
12+
/**
13+
* Retrieves all non-whitespace child elements from the given `PsiFile`.
14+
* Optionally, filters these elements based on the provided range.
15+
*
16+
* @param range an optional range that defines a subset of the document; only elements within this range are included
17+
* @return a list of non-whitespace child elements, filtered by the specified range if provided
18+
*/
19+
fun PsiFile.allNonWhitespaceChildren(
20+
document: Document,
21+
range: Range?,
22+
): List<PsiElement> {
23+
val all = descendantsOfType<PsiElement>().filter { it !is PsiWhiteSpace }
24+
if (range == null) return all.toList()
25+
val textRange = range.toTextRange(document)
26+
return all.filter { textRange.intersects(it.textRange) }.toList()
27+
}

features-impl/kotlin/src/com/jetbrains/ls/api/features/impl/common/kotlin/semanticTokens/LSSemanticTokensProviderKotlinImpl.kt

Lines changed: 23 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,20 @@ import com.intellij.openapi.vfs.findDocument
88
import com.intellij.openapi.vfs.findPsiFile
99
import com.intellij.psi.PsiElement
1010
import com.intellij.psi.PsiWhiteSpace
11+
import com.intellij.psi.util.descendantsOfType
1112
import com.jetbrains.ls.api.core.LSServer
1213
import com.jetbrains.ls.api.core.util.findVirtualFile
1314
import com.jetbrains.ls.api.core.util.toLspRange
15+
import com.jetbrains.ls.api.core.util.toTextRange
1416
import com.jetbrains.ls.api.features.impl.common.kotlin.language.LSKotlinLanguage
1517
import com.jetbrains.ls.api.features.language.LSLanguage
1618
import com.jetbrains.ls.api.features.semanticTokens.*
19+
import com.jetbrains.ls.api.features.utils.allNonWhitespaceChildren
20+
import com.jetbrains.lsp.protocol.Range
1721
import com.jetbrains.lsp.protocol.SemanticTokensParams
22+
import com.jetbrains.lsp.protocol.SemanticTokensRangeParams
23+
import com.jetbrains.lsp.protocol.TextDocumentIdentifier
24+
import com.jetbrains.lsp.protocol.intersects
1825
import kotlinx.coroutines.CancellationException
1926
import org.jetbrains.annotations.ApiStatus
2027
import org.jetbrains.kotlin.analysis.api.KaSession
@@ -41,12 +48,26 @@ object LSSemanticTokensProviderKotlinImpl : LSSemanticTokensProvider {
4148

4249
context(LSServer)
4350
override suspend fun full(params: SemanticTokensParams): List<LSSemanticTokenWithRange> {
51+
return getTokens(params.textDocument, range = null)
52+
53+
}
54+
55+
context(LSServer)
56+
override suspend fun range(params: SemanticTokensRangeParams): List<LSSemanticTokenWithRange> {
57+
return getTokens(params.textDocument, params.range)
58+
}
59+
60+
/**
61+
* @param range `null` means tokens from the whole file`
62+
*/
63+
context(LSServer)
64+
private suspend fun getTokens(textDocument: TextDocumentIdentifier, range:Range?): List<LSSemanticTokenWithRange> {
4465
return withAnalysisContext {
4566
runReadAction {
46-
val file = params.textDocument.findVirtualFile() ?: return@runReadAction emptyList()
67+
val file = textDocument.findVirtualFile() ?: return@runReadAction emptyList()
4768
val ktFile = file.findPsiFile(project) as? KtFile ?: return@runReadAction emptyList()
4869
val document = file.findDocument() ?: return@runReadAction emptyList()
49-
val leafs = ktFile.allNonWhitespaceChildren()
70+
val leafs = ktFile.allNonWhitespaceChildren(document, range)
5071
if (leafs.isEmpty()) return@runReadAction emptyList()
5172
analyze(ktFile) {
5273
leafs.mapNotNull { element ->
@@ -181,11 +202,6 @@ object LSSemanticTokensProviderKotlinImpl : LSSemanticTokensProvider {
181202
}
182203
}
183204

184-
185-
private fun KtFile.allNonWhitespaceChildren(): List<PsiElement> {
186-
return collectDescendantsOfType<PsiElement>().filter { it !is PsiWhiteSpace }
187-
}
188-
189205
private fun FqName.isFromKotlinStdlib(): Boolean {
190206
return startsWith(StandardNames.BUILT_INS_PACKAGE_NAME)
191207
}

kotlin-lsp/src/com/jetbrains/ls/kotlinLsp/requests/core/initialize.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ internal fun LspHandlersBuilder.initializeRequest() {
7474
tokenModifiers = registry.modifiers.map { it.name },
7575
)
7676
},
77-
range = false,
77+
range = true,
7878
full = true,
7979
),
8080
completionProvider = CompletionRegistrationOptionsImpl(

kotlin-lsp/src/com/jetbrains/ls/kotlinLsp/requests/features.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import com.jetbrains.lsp.protocol.CodeActions.CodeActionRequest
1919
import com.jetbrains.lsp.protocol.Commands.ExecuteCommand
2020
import com.jetbrains.lsp.protocol.Diagnostics.DocumentDiagnosticRequestType
2121
import com.jetbrains.lsp.protocol.SemanticTokensRequests.SemanticTokensFullRequest
22+
import com.jetbrains.lsp.protocol.SemanticTokensRequests.SemanticTokensRangeRequest
2223
import com.jetbrains.lsp.protocol.WorkspaceSymbolRequests.WorkspaceSymbolRequest
2324

2425
context(LSServer, LSConfiguration)
@@ -32,6 +33,7 @@ internal fun LspHandlersBuilder.features() {
3233
request(HoverRequestType) { LSHover.getHover(it) }
3334
request(ReferenceRequestType) { LSReferences.getReferences(it) }
3435
request(SemanticTokensFullRequest) { LSSemanticTokens.semanticTokensFull(it) }
36+
request(SemanticTokensRangeRequest) { LSSemanticTokens.semanticTokensRange(it) }
3537
request(WorkspaceSymbolRequest) { LSWorkspaceSymbols.getSymbols(it) }
3638
request(DocumentSymbolRequest) { LSDocumentSymbols.getSymbols(it) }
3739
}

0 commit comments

Comments
 (0)