- Notifications
You must be signed in to change notification settings - Fork 84
KTL-3015 implement Kotlin LSP proxy for completions #878
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: kotlin-community/dev
Are you sure you want to change the base?
KTL-3015 implement Kotlin LSP proxy for completions #878
Conversation
Please, resolve conflicts |
completions/src/test/resources/lsp/lsp-users-projects-root-test/settings.gradle.kts Show resolved Hide resolved
src/main/kotlin/com/compiler/server/configuration/WebSocketConfig.kt Outdated Show resolved Hide resolved
src/main/kotlin/com/compiler/server/configuration/WebSocketConfig.kt Outdated Show resolved Hide resolved
src/main/kotlin/com/compiler/server/configuration/WebSocketConfig.kt Outdated Show resolved Hide resolved
src/main/kotlin/com/compiler/server/controllers/CompilerRestController.kt Outdated Show resolved Hide resolved
Please add kdocs for all public methods and classes |
src/main/kotlin/com/compiler/server/controllers/LspCompletionWebSocketHandler.kt Outdated Show resolved Hide resolved
src/main/kotlin/com/compiler/server/controllers/LspCompletionWebSocketHandler.kt Outdated Show resolved Hide resolved
} | ||
| ||
fun KotlinLspProxy.onClientConnected(clientId: String) { | ||
val project = Project(files = listOf(ProjectFile(name = "$clientId.kt"))) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd say each project should have clientId
, but files can then be called file1-$clientId.kt
, file2-$clientId.kt
, etc, so that we will have a possibility to work with mutliple files
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is it hard to rewrite it to list of files instead of one file now?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It shouldn't introduce big problems, but right now the LSP logic is implemented considering only a single file, meaning that every file is treated in isolation regardless of the same user. Unfortunately it is not trivial to make the user's files see each other with the current isolated documents
LSP mode.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Got it, thanks!
Anyway, let's make multiple files here and pass to lsp only the first one. And leave a comment or todo, with the place where we choose only one file, with the explanation about LSP mode.
Note: if it is a todo, then it should have the number of the task connected to it. If we don't have such a task, then you should create it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've added support for multiple documents, let me know if it's ok.
As a sidenote, my main doubt is how we handle which document among the project's documents has to be picked for providing completions. Right now in the KotlinLspProxy
I've changed completion functions signatures in order to accept a ProjectFile
(which by default is the first file of the project), which now are unused due to the API format, i.e frontend just provide project + line + character. Once multiple files will be supported, the APIs should change in order to include such logic.
fun KotlinLspProxy.onClientConnected(clientId: String) { | ||
val project = Project(files = listOf(ProjectFile(name = "$clientId.kt"))) | ||
.also { clientsProjects[clientId] = it } | ||
val lspProject = LspProject.fromProject(project).also { lspProjects[project] = it } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not clear for me, why do we have project and lspProject, can it be merged into one entity here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
They can be merged, but I needed a way to keep track of files in the file system, retrieve their lsp-compliant URIs and lsp documents versions. I though it was better to keep them separate being Project
a simpler entity and already containing what's needed for most compiler server tasks.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I expect that after extracting completion to a separate subproject, you will not have a Project entity, so that you can only leave a LspProject
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Even after extracting the completion logic to a separate subproject, the dependency on Project
is still present due to the API contract that we have with frontend. Frontend will send a Project
instance to completion endpoints (both REST and WS as of now), so we still need a way to map the incoming project plus the convenient LspProject
. If the subproject will be lsp-based only I can merge the two entities, while I suggest to keep them separated in case it must be shared by other completion logics. In the meantime I'll take care in documenting LspProject
so that its purpose is clear.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You don't need to copy the whole project from the old API, just save fields, that you need
src/main/kotlin/com/compiler/server/controllers/LspCompletionWebSocketHandler.kt Outdated Show resolved Hide resolved
src/main/kotlin/com/compiler/server/controllers/LspCompletionWebSocketHandler.kt Outdated Show resolved Hide resolved
src/main/kotlin/com/compiler/server/controllers/LspCompletionWebSocketHandler.kt Outdated Show resolved Hide resolved
d1fd552
to 7731e29
Compare @dkrasnoff With recent commits I've made some changes that have probably changed the references to the request changes above. In short, I've
I remain available for any eventual question. |
95cc73a
to c4c51ad
Compare completions/src/test/resources/lsp/lsp-users-projects-root-test/settings.gradle.kts Show resolved Hide resolved
completions/src/main/kotlin/completions/controllers/ws/LspCompletionWebSocketHandler.kt Outdated Show resolved Hide resolved
import org.springframework.web.bind.annotation.ExceptionHandler | ||
| ||
@ControllerAdvice | ||
class LspAvailabilityAdvice { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's maybe move the controller package and write a class global exception handler for all exceptions, where we can handle all custom exceptions like this.
* | ||
* @param query the query the user has typed so far | ||
*/ | ||
fun List<CompletionItem>.rankCompletions(query: String): List<CompletionItem> = |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: I used to conventions, where all public methods go on top and private go down
?.jsonObject?.get("prefix") | ||
?.jsonPrimitive?.content | ||
| ||
private fun fuzzyScore(query: String, candidate: String): Int { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Was it taken from VS code plugin?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's inspired and a simplification of how Monaco editor (the actual editor inside VS-Code) performs completion matching. With LSP it's largely up to the editor to determine completion sorting, LSP just provides a filterText: int
field that rarely yields good completion results on top.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Got it than, lets make it util and cover this fuzzyScore
with differnet test scenarios, to show what do we expect from it in the tests
import com.fasterxml.jackson.annotation.JsonValue | ||
| ||
@JsonIgnoreProperties(ignoreUnknown = true) | ||
data class Project( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
lets move it to package completions.dto.api call it CompletionsRequest and remove args
and confType
as we don't need this fields
fun KotlinLspProxy.onClientConnected(clientId: String) { | ||
val project = Project(files = listOf(ProjectFile(name = "$clientId.kt"))) | ||
.also { clientsProjects[clientId] = it } | ||
val lspProject = LspProject.fromProject(project).also { lspProjects[project] = it } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You don't need to copy the whole project from the old API, just save fields, that you need
aa74429
to ff47e3a
Compare import kotlin.test.Ignore | ||
import kotlin.test.assertEquals | ||
| ||
// TODO(Dmitrii Krasnov): this test is disabled until KTL-2807 is fixed |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I thought we could remove this todo and ignore the annotation here and in the concurrency test
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes we can, but I wanted to discuss with you about a little detail: having developed basically two ways of performing completion (REST and WebSocket), we could take inspiration from current's CompletionTest
, i.e. an abstract test that is reified for both implementations. In this way we reuse current tests, testing both completions approaches adding the least amount of code as possible.
I was trying to implement this with ImportTest
, but apparently LSP completions differs a little bit from what is expected in that test (e.g. old completions: Random (kotlin.random.Random)
, LSP-based completions: Random (kotlin.random)
). In these cases, should I change the tests or should I adapt LSP-based completions to make those tests pass? (it is mostly regex and string manipulation).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeap, you can manipulate with test data if it doesn't affect the main logic of the check like in your example.
| ||
plugins { | ||
id("base-kotlin-jvm-conventions") | ||
alias(libs.plugins.spring.dependency.management) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What do you think about extracting base spring related things to Grdle convention plugin like base-kotlin-jvm-conventions
, so we can share the same spring build setting across different modules.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That's a great idea! I'm going to try and see what I can do after fixing requested changes!
completions/src/main/kotlin/completions/controllers/ws/LspCompletionWebSocketHandler.kt Outdated Show resolved Hide resolved
?.jsonObject?.get("prefix") | ||
?.jsonPrimitive?.content | ||
| ||
private fun fuzzyScore(query: String, candidate: String): Int { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Got it than, lets make it util and cover this fuzzyScore
with differnet test scenarios, to show what do we expect from it in the tests
0b4bc80
to 0180d35
Compare @dkrasnoff I don't remember if we've already discussed this, but should I move also |
Now client's requests are modelled as `CompletionRequest` which will not include args or conftype. This simplifies the logic of `LspProject` and the translation between the two.
0180d35
to 69ed0ed
Compare fix: fix relative path when running locally fix: use different client id when running stateful test chore: re-introduce `objectReader` fix: propagate refactor in payload of WS tests
Added tests, and improved the following: - improve fuzzy matches - prefer shorter completions first - ultimately follow `sortText`
c24863c
to 5ea84b9
Compare …line and ch positions
""".trimIndent() | ||
| ||
val foundCompletionsTexts = getCompletions(codeWithCaret = code, isJs = isJs).map { it.text } | ||
val completions = listOf("kotlin.math.sin(") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@dkrasnoff I think I'm not getting what is the expected behaviour here, and also in current version of playground. Even when looking at the HTTP responses of playground if I insert this code snippet, I'm receiving the following completion:
{ "1": { "text": "kotlin.math.sin(", "displayText": "sin(x: Double) (kotlin.math.sin)", "tail": "Double", "import": "kotlin.math.sin", "icon": "method", "hasOtherImports": true } }
hasOtherImports
is marked as true
, so the text
field is replaced from sin(
to the fully qualified name kotlin.math.sin(
. However, I think this logic is wrong, because hasOtherImports
should not be true
due to the fact that the there aren't actually other imports that matches the same function signature, therefore text
should be sin(
.
On the other hand, if this is a design choice (i.e. prefer fully qualified names over auto-import), please let me know.
I think the best way to understand how I am figuring out this logic can be found in how I cleanup completions to Completion
before sending them to clients.
…s already imported fix: fix failing test after completion parsing changes
2cc6d44
to 40ad0bb
Compare
This PR introduces a new way of performing completions requests leveraging Kotlin-lsp. Two approaches are included:
api/compiler/lsp/complete
;Currently completions with this approach supports only the latest Kotlin version, but future developments include the support of such feature by using a dedicated client and workspace for each Kotlin version supported by the kotlin-compiler-server.
Moreover, the support for different platforms than Kotlin/JVM is strictly bound to what Kotlin-lsp supports, namely only Kotlin/JVM projects as of today.