Skip to content

Commit 064854f

Browse files
Adjust LSP Content Object Lifecycle (#2102)
### Description Adjusts how LSP content objects are created and updated. CESE works best when coordinators and highlighters are static for the lifetime of the editor. This adjusts both the content coordinator and semantic highlighter objects to have optional document URIs and server references. That makes it so these objects will be passed into the editor immediately, and will just ignore requests or notifications until the language server tells the object about itself. The change fixes a bug where an editor opened that triggers a language server startup would not have a working semantic highlighter or update its contents with the server when the document was updated. This fixes that race condition. This also makes a small change to language server initialization, this creates the log container before the server and injects it into the data channel so if the channel terminates the log container can receive a message. ### Related Issues * N/A ### Checklist - [x] I read and understood the [contributing guide](https://github.com/CodeEditApp/CodeEdit/blob/main/CONTRIBUTING.md) as well as the [code of conduct](https://github.com/CodeEditApp/CodeEdit/blob/main/CODE_OF_CONDUCT.md) - [x] The issues this PR addresses are related to each other - [x] My changes generate no new warnings - [x] My code builds and runs on my machine - [x] My changes are all related to the related issue above - [x] I documented my code ### Screenshots N/A
1 parent ef9e91b commit 064854f

File tree

12 files changed

+84
-101
lines changed

12 files changed

+84
-101
lines changed

CodeEdit.xcodeproj/project.pbxproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1783,7 +1783,7 @@
17831783
repositoryURL = "https://github.com/CodeEditApp/CodeEditSourceEditor";
17841784
requirement = {
17851785
kind = exactVersion;
1786-
version = 0.14.1;
1786+
version = 0.15.0;
17871787
};
17881788
};
17891789
6C85BB3E2C2105ED00EB5DEF /* XCRemoteSwiftPackageReference "CodeEditKit" */ = {

CodeEdit.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved

Lines changed: 14 additions & 14 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

CodeEdit/Features/Editor/Views/CodeFileView.swift

Lines changed: 3 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,7 @@ struct CodeFileView: View {
2121

2222
/// Any coordinators passed to the view.
2323
private var textViewCoordinators: [TextViewCoordinator]
24-
25-
@State private var highlightProviders: [any HighlightProviding] = []
24+
private var highlightProviders: [any HighlightProviding] = []
2625

2726
@AppSettings(\.textEditing.defaultTabWidth)
2827
var defaultTabWidth
@@ -86,15 +85,15 @@ struct CodeFileView: View {
8685
self.textViewCoordinators = textViewCoordinators
8786
+ [editorInstance.rangeTranslator]
8887
+ [codeFile.contentCoordinator]
89-
+ [codeFile.languageServerObjects.textCoordinator].compactMap({ $0 })
88+
+ [codeFile.languageServerObjects.textCoordinator]
9089
self.isEditable = isEditable
9190

9291
if let openOptions = codeFile.openOptions {
9392
codeFile.openOptions = nil
9493
editorInstance.cursorPositions = openOptions.cursorPositions
9594
}
9695

97-
updateHighlightProviders()
96+
highlightProviders = [codeFile.languageServerObjects.highlightProvider] + [treeSitterClient]
9897

9998
codeFile
10099
.contentCoordinator
@@ -186,10 +185,6 @@ struct CodeFileView: View {
186185
.onChange(of: settingsFont) { newFontSetting in
187186
font = newFontSetting.current
188187
}
189-
.onReceive(codeFile.$languageServerObjects) { languageServerObjects in
190-
// This will not be called in single-file views (for now) but is safe to listen to either way
191-
updateHighlightProviders(lspHighlightProvider: languageServerObjects.highlightProvider)
192-
}
193188
}
194189

195190
/// Determines the style of bracket emphasis based on the `bracketEmphasis` setting and the current theme.
@@ -212,12 +207,6 @@ struct CodeFileView: View {
212207
return .underline(color: color)
213208
}
214209
}
215-
216-
/// Updates the highlight providers array.
217-
/// - Parameter lspHighlightProvider: The language server provider, if available.
218-
private func updateHighlightProviders(lspHighlightProvider: HighlightProviding? = nil) {
219-
highlightProviders = [lspHighlightProvider].compactMap({ $0 }) + [treeSitterClient]
220-
}
221210
}
222211

223212
// This extension is kept here because it should not be used elsewhere in the app and may cause confusion

CodeEdit/Features/LSP/Features/DocumentSync/LSPContentCoordinator.swift

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -32,16 +32,21 @@ class LSPContentCoordinator<DocumentType: LanguageServerDocument>: TextViewCoord
3232
private var task: Task<Void, Never>?
3333

3434
weak var languageServer: LanguageServer<DocumentType>?
35-
var documentURI: String
35+
var documentURI: String?
3636

3737
/// Initializes a content coordinator, and begins an async stream of updates
38-
init(documentURI: String, languageServer: LanguageServer<DocumentType>) {
38+
init(documentURI: String? = nil, languageServer: LanguageServer<DocumentType>? = nil) {
3939
self.documentURI = documentURI
4040
self.languageServer = languageServer
4141

4242
setUpUpdatesTask()
4343
}
4444

45+
func setUp(server: LanguageServer<DocumentType>, document: DocumentType) {
46+
languageServer = server
47+
documentURI = document.languageServerURI
48+
}
49+
4550
func setUpUpdatesTask() {
4651
task?.cancel()
4752
// Create this stream here so it's always set up when the text view is set up, rather than only once on init.
@@ -76,7 +81,7 @@ class LSPContentCoordinator<DocumentType: LanguageServerDocument>: TextViewCoord
7681
}
7782

7883
func textView(_ textView: TextView, didReplaceContentsIn range: NSRange, with string: String) {
79-
guard let lspRange = editedRange else {
84+
guard let lspRange = editedRange, let documentURI else {
8085
return
8186
}
8287
self.editedRange = nil

CodeEdit/Features/LSP/Features/SemanticTokens/SemanticTokenHighlightProvider.swift

Lines changed: 18 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -32,9 +32,9 @@ final class SemanticTokenHighlightProvider<
3232
typealias EditCallback = @MainActor (Result<IndexSet, any Error>) -> Void
3333
typealias HighlightCallback = @MainActor (Result<[HighlightRange], any Error>) -> Void
3434

35-
private let tokenMap: SemanticTokenMap
36-
private let documentURI: String
37-
private weak var languageServer: LanguageServer<DocumentType>?
35+
private var tokenMap: SemanticTokenMap?
36+
private var documentURI: String?
37+
weak var languageServer: LanguageServer<DocumentType>?
3838
private weak var textView: TextView?
3939

4040
private var lastEditCallback: EditCallback?
@@ -45,13 +45,23 @@ final class SemanticTokenHighlightProvider<
4545
textView?.documentRange ?? .zero
4646
}
4747

48-
init(tokenMap: SemanticTokenMap, languageServer: LanguageServer<DocumentType>, documentURI: String) {
48+
init(
49+
tokenMap: SemanticTokenMap? = nil,
50+
languageServer: LanguageServer<DocumentType>? = nil,
51+
documentURI: String? = nil
52+
) {
4953
self.tokenMap = tokenMap
5054
self.languageServer = languageServer
5155
self.documentURI = documentURI
5256
self.storage = Storage()
5357
}
5458

59+
func setUp(server: LanguageServer<DocumentType>, document: DocumentType) {
60+
languageServer = server
61+
documentURI = document.languageServerURI
62+
tokenMap = server.highlightMap
63+
}
64+
5565
// MARK: - Language Server Content Lifecycle
5666

5767
/// Called when the language server finishes sending a document update.
@@ -95,7 +105,8 @@ final class SemanticTokenHighlightProvider<
95105
textView: TextView,
96106
lastResultId: String
97107
) async throws {
98-
guard let response = try await languageServer.requestSemanticTokens(
108+
guard let documentURI,
109+
let response = try await languageServer.requestSemanticTokens(
99110
for: documentURI,
100111
previousResultId: lastResultId
101112
) else {
@@ -112,7 +123,7 @@ final class SemanticTokenHighlightProvider<
112123
/// Requests and applies tokens for an entire document. This does not require a previous response id, and should be
113124
/// used in place of `requestDeltaTokens` when that's the case.
114125
private func requestTokens(languageServer: LanguageServer<DocumentType>, textView: TextView) async throws {
115-
guard let response = try await languageServer.requestSemanticTokens(for: documentURI) else {
126+
guard let documentURI, let response = try await languageServer.requestSemanticTokens(for: documentURI) else {
116127
return
117128
}
118129
await applyEntireResponse(response, callback: lastEditCallback)
@@ -159,7 +170,7 @@ final class SemanticTokenHighlightProvider<
159170
return
160171
}
161172

162-
guard let lspRange = textView.lspRangeFrom(nsRange: range) else {
173+
guard let lspRange = textView.lspRangeFrom(nsRange: range), let tokenMap else {
163174
completion(.failure(HighlightError.lspRangeFailure))
164175
return
165176
}

CodeEdit/Features/LSP/LanguageServer/Capabilities/LanguageServer+DocumentSync.swift

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,7 @@ extension LanguageServer {
8686
switch resolveDocumentSyncKind() {
8787
case .full:
8888
guard let content = await getIsolatedDocumentContent(document) else {
89+
logger.error("Failed to get isolated document content")
8990
return
9091
}
9192
let changeEvent = TextDocumentContentChangeEvent(range: nil, rangeLength: nil, text: content.string)
@@ -107,7 +108,7 @@ extension LanguageServer {
107108

108109
// Let the semantic token provider know about the update.
109110
// Note for future: If a related LSP object need notifying about document changes, do it here.
110-
try await document.languageServerObjects.highlightProvider?.documentDidChange()
111+
try await document.languageServerObjects.highlightProvider.documentDidChange()
111112
} catch {
112113
logger.warning("closeDocument: Error \(error)")
113114
throw error
@@ -128,10 +129,7 @@ extension LanguageServer {
128129

129130
@MainActor
130131
private func updateIsolatedDocument(_ document: DocumentType) {
131-
document.languageServerObjects = LanguageServerDocumentObjects(
132-
textCoordinator: openFiles.contentCoordinator(for: document),
133-
highlightProvider: openFiles.semanticHighlighter(for: document)
134-
)
132+
document.languageServerObjects.setUp(server: self, document: document)
135133
}
136134

137135
@MainActor

CodeEdit/Features/LSP/LanguageServer/LanguageServer.swift

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,8 @@ class LanguageServer<DocumentType: LanguageServerDocument> {
5151
lspInstance: InitializingServer,
5252
lspPid: pid_t,
5353
serverCapabilities: ServerCapabilities,
54-
rootPath: URL
54+
rootPath: URL,
55+
logContainer: LanguageServerLogContainer
5556
) {
5657
self.languageId = languageId
5758
self.binary = binary
@@ -60,7 +61,7 @@ class LanguageServer<DocumentType: LanguageServerDocument> {
6061
self.serverCapabilities = serverCapabilities
6162
self.rootPath = rootPath
6263
self.openFiles = LanguageServerFileMap()
63-
self.logContainer = LanguageServerLogContainer(language: languageId)
64+
self.logContainer = logContainer
6465
self.logger = Logger(
6566
subsystem: Bundle.main.bundleIdentifier ?? "",
6667
category: "LanguageServer.\(languageId.rawValue)"
@@ -89,9 +90,11 @@ class LanguageServer<DocumentType: LanguageServerDocument> {
8990
environment: binary.env
9091
)
9192

93+
let logContainer = LanguageServerLogContainer(language: languageId)
9294
let (connection, process) = try makeLocalServerConnection(
9395
languageId: languageId,
94-
executionParams: executionParams
96+
executionParams: executionParams,
97+
logContainer: logContainer
9598
)
9699
let server = InitializingServer(
97100
server: connection,
@@ -105,7 +108,8 @@ class LanguageServer<DocumentType: LanguageServerDocument> {
105108
lspInstance: server,
106109
lspPid: process.processIdentifier,
107110
serverCapabilities: initializationResponse.capabilities,
108-
rootPath: URL(filePath: workspacePath)
111+
rootPath: URL(filePath: workspacePath),
112+
logContainer: logContainer
109113
)
110114
}
111115

@@ -118,13 +122,17 @@ class LanguageServer<DocumentType: LanguageServerDocument> {
118122
/// - Returns: A new connection to the language server.
119123
static func makeLocalServerConnection(
120124
languageId: LanguageIdentifier,
121-
executionParams: Process.ExecutionParameters
125+
executionParams: Process.ExecutionParameters,
126+
logContainer: LanguageServerLogContainer
122127
) throws -> (connection: JSONRPCServerConnection, process: Process) {
123128
do {
124129
let (channel, process) = try DataChannel.localProcessChannel(
125130
parameters: executionParams,
126-
terminationHandler: {
131+
terminationHandler: { [weak logContainer] in
127132
logger.debug("Terminated data channel for \(languageId.rawValue)")
133+
logContainer?.appendLog(
134+
LogMessageParams(type: .error, message: "Data Channel Terminated Unexpectedly")
135+
)
128136
}
129137
)
130138
return (JSONRPCServerConnection(dataChannel: channel), process)

CodeEdit/Features/LSP/LanguageServer/LanguageServerFileMap.swift

Lines changed: 1 addition & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,6 @@ class LanguageServerFileMap<DocumentType: LanguageServerDocument> {
1616
private struct DocumentObject {
1717
let uri: String
1818
var documentVersion: Int
19-
var contentCoordinator: LSPContentCoordinator<DocumentType>
20-
var semanticHighlighter: HighlightProviderType?
2119
}
2220

2321
private var trackedDocuments: NSMapTable<NSString, DocumentType>
@@ -32,24 +30,7 @@ class LanguageServerFileMap<DocumentType: LanguageServerDocument> {
3230
func addDocument(_ document: DocumentType, for server: LanguageServer<DocumentType>) {
3331
guard let uri = document.languageServerURI else { return }
3432
trackedDocuments.setObject(document, forKey: uri as NSString)
35-
var docData = DocumentObject(
36-
uri: uri,
37-
documentVersion: 0,
38-
contentCoordinator: LSPContentCoordinator(
39-
documentURI: uri,
40-
languageServer: server
41-
),
42-
semanticHighlighter: nil
43-
)
44-
45-
if let tokenMap = server.highlightMap {
46-
docData.semanticHighlighter = HighlightProviderType(
47-
tokenMap: tokenMap,
48-
languageServer: server,
49-
documentURI: uri
50-
)
51-
}
52-
33+
let docData = DocumentObject(uri: uri, documentVersion: 0)
5334
trackedDocumentData[uri] = docData
5435
}
5536

@@ -87,22 +68,4 @@ class LanguageServerFileMap<DocumentType: LanguageServerDocument> {
8768
func documentVersion(for uri: DocumentUri) -> Int? {
8869
return trackedDocumentData[uri]?.documentVersion
8970
}
90-
91-
// MARK: - Content Coordinator
92-
93-
func contentCoordinator(for document: DocumentType) -> LSPContentCoordinator<DocumentType>? {
94-
guard let uri = document.languageServerURI else { return nil }
95-
return contentCoordinator(for: uri)
96-
}
97-
98-
func contentCoordinator(for uri: DocumentUri) -> LSPContentCoordinator<DocumentType>? {
99-
trackedDocumentData[uri]?.contentCoordinator
100-
}
101-
102-
// MARK: - Semantic Highlighter
103-
104-
func semanticHighlighter(for document: DocumentType) -> HighlightProviderType? {
105-
guard let uri = document.languageServerURI else { return nil }
106-
return trackedDocumentData[uri]?.semanticHighlighter
107-
}
10871
}

CodeEdit/Features/LSP/LanguageServerDocument.swift

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,15 @@ import CodeEditLanguages
1010

1111
/// A set of properties a language server sets when a document is registered.
1212
struct LanguageServerDocumentObjects<DocumentType: LanguageServerDocument> {
13-
var textCoordinator: LSPContentCoordinator<DocumentType>?
14-
var highlightProvider: SemanticTokenHighlightProvider<SemanticTokenStorage, DocumentType>?
13+
var textCoordinator: LSPContentCoordinator<DocumentType> = LSPContentCoordinator()
14+
// swiftlint:disable:next line_length
15+
var highlightProvider: SemanticTokenHighlightProvider<SemanticTokenStorage, DocumentType> = SemanticTokenHighlightProvider()
16+
17+
@MainActor
18+
func setUp(server: LanguageServer<DocumentType>, document: DocumentType) {
19+
textCoordinator.setUp(server: server, document: document)
20+
highlightProvider.setUp(server: server, document: document)
21+
}
1522
}
1623

1724
/// A protocol that allows a language server to register objects on a text document.

CodeEdit/Features/StatusBar/Views/StatusBarItems/StatusBarCursorPositionLabel.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -115,7 +115,7 @@ struct StatusBarCursorPositionLabel: View {
115115
}
116116

117117
// When there's a single cursor, display the line and column.
118-
return "Line: \(cursorPositions[0].line) Col: \(cursorPositions[0].column)"
118+
return "Line: \(cursorPositions[0].start.line) Col: \(cursorPositions[0].start.column)"
119119
}
120120
}
121121
}

0 commit comments

Comments
 (0)