Skip to content

Commit 0180d35

Browse files
committed
refactor: simplify Project and LspProject logic
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.
1 parent 1b2d546 commit 0180d35

File tree

9 files changed

+95
-123
lines changed

9 files changed

+95
-123
lines changed

completions/src/main/kotlin/completions/controllers/rest/CompletionsRestController.kt

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
package completions.controllers.rest
22

3-
import completions.model.Project
3+
import completions.dto.api.CompletionRequest
44
import completions.service.lsp.LspCompletionProvider
55
import model.Completion
66
import org.springframework.web.bind.annotation.PostMapping
@@ -16,8 +16,8 @@ class CompletionsRestController(
1616
) {
1717
@PostMapping("/lsp")
1818
suspend fun complete(
19-
@RequestBody project: Project,
19+
@RequestBody completionRequest: CompletionRequest,
2020
@RequestParam line: Int,
2121
@RequestParam ch: Int,
22-
): List<Completion> = lspCompletionProvider.complete(project, line, ch)
22+
): List<Completion> = lspCompletionProvider.complete(completionRequest, line, ch)
2323
}

completions/src/main/kotlin/completions/controllers/ws/LspCompletionWebSocketHandler.kt

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,9 @@
22

33
package completions.controllers.ws
44

5-
import completions.model.Project
65
import com.fasterxml.jackson.databind.ObjectMapper
76
import com.fasterxml.jackson.module.kotlin.readValue
7+
import completions.dto.api.CompletionRequest
88
import completions.lsp.KotlinLspProxy
99
import completions.lsp.StatefulKotlinLspProxy.onClientConnected
1010
import completions.lsp.StatefulKotlinLspProxy.onClientDisconnected
@@ -28,7 +28,7 @@ import reactor.core.scheduler.Schedulers
2828
* - **Connection**: Server sends `Init{ sessionId }` immediately after the WebSocket is established. The client could persist
2929
* this identifier for the lifetime of the connection and treat it as the LSP client id.
3030
*
31-
* - **Requests**: Client sends `CompletionRequest{ requestId, project, line, ch }` for each caret position to be completed.
31+
* - **Requests**: Client sends `CompletionRequest{ requestId, completionRequest, line, ch }` for each caret position to be completed.
3232
* `requestId` must be unique per client session and is used exclusively for correlation.
3333
*
3434
* - **Responses**: For a successfully processed request: `Completions{ completions, requestId }` where `completions` are
@@ -105,7 +105,7 @@ class LspCompletionWebSocketHandler(
105105
lspProxy.requireAvailable()
106106
lspCompletionProvider.complete(
107107
clientId = sessionId,
108-
project = request.project,
108+
request = request.completionRequest,
109109
line = request.line,
110110
ch = request.ch,
111111
applyFuzzyRanking = true,
@@ -150,7 +150,7 @@ sealed interface Response {
150150

151151
private data class WebSocketCompletionRequest(
152152
val requestId: String,
153-
val project: Project,
153+
val completionRequest: CompletionRequest,
154154
val line: Int,
155155
val ch: Int,
156156
)
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
package completions.dto.api
2+
3+
import com.fasterxml.jackson.annotation.JsonIgnoreProperties
4+
5+
@JsonIgnoreProperties(ignoreUnknown = true)
6+
data class CompletionRequest(
7+
val files: List<ProjectFile> = emptyList(),
8+
)
9+
10+
@JsonIgnoreProperties(ignoreUnknown = true)
11+
data class ProjectFile(val text: String = "", val name: String = "")

completions/src/main/kotlin/completions/lsp/KotlinLspProxy.kt

Lines changed: 40 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
package completions.lsp
22

3+
import completions.dto.api.CompletionRequest
4+
import completions.dto.api.ProjectFile
35
import completions.exceptions.LspUnavailableException
46
import completions.lsp.client.LspClient
57
import completions.lsp.client.ReconnectingLspClient
68
import completions.lsp.components.LspProject
7-
import completions.model.Project
8-
import completions.model.ProjectFile
99
import kotlinx.coroutines.CompletableDeferred
1010
import kotlinx.coroutines.CoroutineName
1111
import kotlinx.coroutines.CoroutineScope
@@ -21,14 +21,15 @@ import java.io.IOException
2121
import java.net.URI
2222
import java.nio.file.Path
2323
import java.util.concurrent.ConcurrentHashMap
24+
import java.util.concurrent.CopyOnWriteArrayList
2425
import java.util.concurrent.atomic.AtomicBoolean
2526
import kotlin.time.Duration.Companion.seconds
2627

2728
@Component
2829
class KotlinLspProxy {
2930

3031
internal lateinit var lspClient: LspClient
31-
internal val lspProjects = ConcurrentHashMap<Project, LspProject>()
32+
internal val lspProjects = CopyOnWriteArrayList<LspProject>()
3233

3334
private val available = AtomicBoolean(false)
3435
private val isInitializing = AtomicBoolean(false)
@@ -51,24 +52,24 @@ class KotlinLspProxy {
5152
* This modality is aimed for **stateless** scenarios where we don't care about
5253
* the identity of the client and the project.
5354
*
54-
* @param project the project containing the file
55+
* @param request the completion request
5556
* @param line the line number
5657
* @param ch the character position
5758
* @param projectFile the file to be used for completion, defaults to the first file in the project
5859
* @return a list of [CompletionItem]s
5960
*/
6061
suspend fun getOneTimeCompletions(
61-
project: Project,
62+
request: CompletionRequest,
6263
line: Int,
6364
ch: Int,
64-
projectFile: ProjectFile = project.files.first(),
65+
projectFile: ProjectFile = request.files.first(),
6566
): List<CompletionItem> {
6667
ensureLspClientReady() ?: return emptyList()
67-
val lspProject = lspProjects.getOrPut(project) { createNewProject(project) }
68+
val lspProject = createNewProject(request).also(lspProjects::add)
6869
val uri = lspProject.getDocumentUri(projectFile.name) ?: return emptyList()
6970
lspClient.openDocument(uri, projectFile.text, 1)
7071
return getCompletions(lspProject, line, ch, projectFile.name)
71-
.also { closeProject(project) }
72+
.also { closeProject(lspProject) }
7273
}
7374

7475
/**
@@ -149,17 +150,16 @@ class KotlinLspProxy {
149150
else logger.debug("Lsp client not ready: ${it.message}")
150151
}.isSuccess.takeIf { it }
151152

152-
private fun createNewProject(project: Project): LspProject = LspProject.fromProject(project)
153+
private fun createNewProject(project: CompletionRequest): LspProject = LspProject.fromCompletionRequest(project)
153154

154-
internal fun closeProject(project: Project) {
155-
val lspProject = lspProjects[project] ?: return
155+
internal fun closeProject(lspProject: LspProject) {
156156
lspProject.getDocumentsUris().forEach { uri -> lspClient.closeDocument(uri) }
157157
lspProject.tearDown()
158-
lspProjects.remove(project)
158+
lspProjects.remove(lspProject)
159159
}
160160

161161
fun closeAllProjects() {
162-
lspProjects.keys.forEach { closeProject(it) }
162+
lspProjects.forEach { closeProject(it) }
163163
lspProjects.clear()
164164
}
165165

@@ -184,10 +184,10 @@ class KotlinLspProxy {
184184
isInitializing.set(false)
185185
}
186186
lspClient.addOnReconnectListener {
187-
lspProjects.forEach { (project, lspProject) ->
187+
lspProjects.forEach { project ->
188188
val file = project.files.first()
189-
val uri = lspProject.getDocumentUri(file.name) ?: return@forEach
190-
lspProject.resetDocumentVersion(file.name)
189+
val uri = project.getDocumentUri(file.name) ?: return@forEach
190+
project.resetDocumentVersion(file.name)
191191
client.openDocument(uri, file.text, 1)
192192
}
193193
lspClientInitializedDeferred.complete(Unit)
@@ -242,7 +242,7 @@ class KotlinLspProxy {
242242
* Custom WS messages could be designed to handle this.
243243
*/
244244
object StatefulKotlinLspProxy {
245-
private val clientsProjects = ConcurrentHashMap<String, Project>()
245+
private val clientsProjects = ConcurrentHashMap<String, LspProject>()
246246

247247
/**
248248
* Retrieve completions for a given line and character position in a project file.
@@ -254,21 +254,22 @@ object StatefulKotlinLspProxy {
254254
* the document**.
255255
*
256256
* @param clientId the user identifier (or session identifier)
257-
* @param newProject the project containing the file
257+
* @param completionRequest the request containing the files
258258
* @param line the line number
259259
* @param ch the character position
260260
* @return a list of [CompletionItem]s
261261
*/
262262
suspend fun KotlinLspProxy.getCompletionsForClient(
263263
clientId: String,
264-
newProject: Project,
264+
completionRequest: CompletionRequest,
265265
line: Int,
266266
ch: Int,
267-
projectFile: ProjectFile = newProject.files.first(),
267+
projectFile: ProjectFile = completionRequest.files.first(),
268268
): List<CompletionItem> {
269-
val project =
270-
clientsProjects[clientId]?.let { ensureDocumentPresent(it, projectFile, clientId) } ?: return emptyList()
271-
val lspProject = lspProjects[project] ?: return emptyList()
269+
val lspProject = clientsProjects[clientId]?.let {
270+
ensureDocumentPresent(it, projectFile, clientId)
271+
} ?: return emptyList()
272+
272273
val newContent = projectFile.text
273274
val documentToChange = projectFile.name
274275
changeDocumentContent(lspProject, documentToChange, newContent)
@@ -282,9 +283,10 @@ object StatefulKotlinLspProxy {
282283
* @param clientId the unique identifier for the client that has connected
283284
*/
284285
fun KotlinLspProxy.onClientConnected(clientId: String) {
285-
val project = Project()
286-
.also { clientsProjects[clientId] = it }
287-
val lspProject = LspProject.fromProject(project, ownerId = clientId).also { lspProjects[project] = it }
286+
val lspProject = LspProject.empty(clientId).also {
287+
lspProjects.add(it)
288+
clientsProjects[clientId] = it
289+
}
288290
lspProject.getDocumentsUris().forEach { uri -> lspClient.openDocument(uri, "", 1) }
289291
}
290292

@@ -298,6 +300,7 @@ object StatefulKotlinLspProxy {
298300
clientsProjects[clientId]?.let {
299301
closeProject(it)
300302
clientsProjects.remove(clientId)
303+
lspProjects.remove(it)
301304
}
302305
}
303306

@@ -309,17 +312,21 @@ object StatefulKotlinLspProxy {
309312
* @param projectFile the file to be opened if not present in the project
310313
* @return the modified project, with the file added if necessary
311314
*/
312-
private fun KotlinLspProxy.ensureDocumentPresent(project: Project, projectFile: ProjectFile, clientId: String): Project {
313-
if (project.files.contains(projectFile)) return project
315+
private fun KotlinLspProxy.ensureDocumentPresent(
316+
project: LspProject,
317+
projectFile: ProjectFile,
318+
clientId: String
319+
): LspProject {
320+
if (project.containsFile(projectFile)) return project
314321
lspProjects.remove(project)
315-
val newProject = project.copy(files = project.files + projectFile)
316-
val newLspProject = LspProject.fromProject(newProject, ownerId = clientId)
317-
lspProjects[newProject] = newLspProject
318-
clientsProjects[clientId] = newProject
322+
val newLspProject = project.copy(files = project.files + projectFile).also {
323+
lspProjects.add(it)
324+
clientsProjects[clientId] = it
325+
}
319326
newLspProject.getDocumentUri(projectFile.name)?.let { uri ->
320327
lspClient.openDocument(uri, projectFile.text, 1)
321328
}
322-
return newProject
329+
return newLspProject
323330
}
324331

325332
private fun KotlinLspProxy.changeDocumentContent(

completions/src/main/kotlin/completions/lsp/components/LspProject.kt

Lines changed: 17 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
package completions.lsp.components
22

3+
import completions.dto.api.CompletionRequest
4+
import completions.dto.api.ProjectFile
35
import completions.lsp.KotlinLspProxy
4-
import completions.model.Project
5-
import completions.model.ProjectType
66
import java.nio.file.Path
77
import java.util.UUID
88
import java.util.concurrent.ConcurrentHashMap
@@ -13,15 +13,11 @@ import java.util.concurrent.atomic.AtomicInteger
1313
* and the project's filesystem structure. It facilitates tasks such as creating a project workspace,
1414
* managing documents within that workspace, and tearing down the workspace when it's no longer needed.
1515
*
16-
* @property confType The configuration type of the project, defaulting to `ProjectType.JAVA`.
17-
* @property files A list of [LspDocument] objects representing project files.
16+
* @property files A list of [ProjectFile] objects representing project files.
1817
*/
19-
class LspProject(
20-
confType: ProjectType = ProjectType.JAVA,
21-
files: List<LspDocument> = emptyList(),
22-
ownerId: String? = null,
23-
) {
24-
private val projectRoot: Path = baseDir.resolve("$confType-${ownerId ?: UUID.randomUUID().toString()}")
18+
data class LspProject(val files: List<ProjectFile> = emptyList(), val ownerId: String? = null) {
19+
20+
private val projectRoot: Path = baseDir.resolve("${ownerId?.let { "user-$it" } ?: UUID.randomUUID()}")
2521
private val documentsToPaths: MutableMap<String, Path> = mutableMapOf()
2622
private val documentsVersions = ConcurrentHashMap<String, AtomicInteger>()
2723

@@ -40,6 +36,8 @@ class LspProject(
4036
documentsVersions[name]?.incrementAndGet()
4137
}
4238

39+
fun containsFile(file: ProjectFile): Boolean = files.any { it.name == file.name && it.text == file.text}
40+
4341
/**
4442
* Returns the URI of a document in the project compliant with the [completions.lsp.client.LspClient].
4543
*/
@@ -67,39 +65,26 @@ class LspProject(
6765
private val baseDir = Path.of(KotlinLspProxy.lspLocalWorkspaceRoot()).toAbsolutePath()
6866

6967
/**
70-
* Creates a new instance of [LspProject] based on the provided [Project] data.
68+
* Creates a new instance of [LspProject] based on the provided [CompletionRequest] data.
7169
* Please note that currently only JVM-related projects are supported.
7270
*
73-
* @param project the source project containing configuration type and a list of files
71+
* @param completionRequest the [CompletionRequest] data to use for project creation
7472
* @param ownerId optional identifier for the owner of the project
7573
* @return a new [LspProject] instance with the provided project's configuration type and files
7674
*/
77-
fun fromProject(project: Project, ownerId: String? = null): LspProject {
75+
fun fromCompletionRequest(completionRequest: CompletionRequest, ownerId: String? = null): LspProject {
7876
return LspProject(
79-
confType = ensureSupportedConfType(project.confType),
80-
files = project.files.map { LspDocument(it.text, it.name) },
77+
files = completionRequest.files.map { ProjectFile(it.text, it.name) },
8178
ownerId = ownerId,
8279
)
8380
}
8481

8582
/**
86-
* If and when kotlin LSP support other project types, this function can be updated.
83+
* Creates a new empty [LspProject] instance, with a single empty file.
8784
*/
88-
private fun ensureSupportedConfType(projectType: ProjectType): ProjectType {
89-
require(projectType == ProjectType.JAVA) { "Only JVM related projects are supported" }
90-
return projectType
91-
}
85+
fun empty(ownerId: String? = null): LspProject = LspProject(
86+
files = listOf(ProjectFile("", "File.kt")),
87+
ownerId = ownerId
88+
)
9289
}
93-
}
94-
95-
data class LspDocument(
96-
val text: String = "",
97-
val name: String = "File.kt",
98-
val publicId: String? = null,
99-
)
100-
101-
@Suppress("unused")
102-
enum class LspProjectType(val id: String) {
103-
JAVA("java"),
104-
// add here support for JS, WASM, ...
10590
}

completions/src/main/kotlin/completions/model/Project.kt

Lines changed: 0 additions & 26 deletions
This file was deleted.

0 commit comments

Comments
 (0)