Skip to content

Commit 9ac1b4e

Browse files
committed
Finally Working
1 parent 342548f commit 9ac1b4e

File tree

10 files changed

+135
-91
lines changed

10 files changed

+135
-91
lines changed

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

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

CodeEdit/Features/LSP/Editor/LSPContentCoordinator.swift

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,6 @@ class LSPContentCoordinator: TextViewCoordinator, TextViewDelegate {
5252
task = Task.detached { [weak self] in
5353
// Send edit events every 250ms
5454
for await events in stream.chunked(by: .repeating(every: .milliseconds(250), clock: .continuous)) {
55-
5655
guard !Task.isCancelled, self != nil else { return }
5756
guard !events.isEmpty, let uri = events.first?.uri else { continue }
5857
// Errors thrown here are already logged, not much else to do right now.

CodeEdit/Features/LSP/Editor/SemanticTokenHighlightProvider.swift

Lines changed: 86 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -27,13 +27,15 @@ final class SemanticTokenHighlightProvider<Storage: SemanticTokenStorage>: Highl
2727
}
2828

2929
typealias EditCallback = @MainActor (Result<IndexSet, any Error>) -> Void
30+
typealias HighlightCallback = @MainActor (Result<[HighlightRange], any Error>) -> Void
3031

3132
private let tokenMap: SemanticTokenMap
3233
private let documentURI: String
3334
private weak var languageServer: LanguageServer?
3435
private weak var textView: TextView?
3536

3637
private var lastEditCallback: EditCallback?
38+
private var pendingHighlightCallbacks: [HighlightCallback] = []
3739
private var storage: Storage
3840

3941
var documentRange: NSRange {
@@ -47,34 +49,92 @@ final class SemanticTokenHighlightProvider<Storage: SemanticTokenStorage>: Highl
4749
self.storage = Storage()
4850
}
4951

52+
// MARK: - Language Server Content Lifecycle
53+
54+
/// Called when the language server finishes sending a document update.
55+
///
56+
/// This method first checks if this object has any semantic tokens. If not, requests new tokens and responds to the
57+
/// `pendingHighlightCallbacks` queue with cancellation errors, causing the highlighter to re-query those indices.
58+
///
59+
/// If this object already has some tokens, it determines whether or not we can request a token delta and
60+
/// performs the request.
5061
func documentDidChange() async throws {
5162
guard let languageServer, let textView else {
5263
return
5364
}
54-
print("Doc did change")
55-
56-
// The document was updated. Update our cache and send the invalidated ranges for the editor to handle.
57-
if let lastRequestId = storage.lastRequestId {
58-
guard let response = try await languageServer.requestSemanticTokens( // Not sure why these are optional...
59-
for: documentURI,
60-
previousResultId: lastRequestId
61-
) else {
62-
return
63-
}
64-
switch response {
65-
case let .optionA(tokenData):
66-
await applyEntireResponse(tokenData, callback: lastEditCallback)
67-
case let .optionB(deltaData):
68-
await applyDeltaResponse(deltaData, callback: lastEditCallback, textView: textView)
69-
}
70-
} else {
71-
guard let response = try await languageServer.requestSemanticTokens(for: documentURI) else {
72-
return
65+
66+
guard storage.hasTokens else {
67+
// We have no semantic token info, request it!
68+
try await requestTokens(languageServer: languageServer, textView: textView)
69+
await MainActor.run {
70+
for callback in pendingHighlightCallbacks {
71+
callback(.failure(HighlightProvidingError.operationCancelled))
72+
}
73+
pendingHighlightCallbacks.removeAll()
7374
}
74-
await applyEntireResponse(response, callback: lastEditCallback)
75+
return
76+
}
77+
78+
// The document was updated. Update our token cache and send the invalidated ranges for the editor to handle.
79+
if let lastResultId = storage.lastResultId {
80+
try await requestDeltaTokens(languageServer: languageServer, textView: textView, lastResultId: lastResultId)
81+
return
82+
}
83+
84+
try await requestTokens(languageServer: languageServer, textView: textView)
85+
}
86+
87+
// MARK: - LSP Token Requests
88+
89+
/// Requests and applies a token delta. Requires a previous response identifier.
90+
private func requestDeltaTokens(
91+
languageServer: LanguageServer,
92+
textView: TextView,
93+
lastResultId: String
94+
) async throws {
95+
guard let response = try await languageServer.requestSemanticTokens(
96+
for: documentURI,
97+
previousResultId: lastResultId
98+
) else {
99+
return
100+
}
101+
switch response {
102+
case let .optionA(tokenData):
103+
await applyEntireResponse(tokenData, callback: lastEditCallback)
104+
case let .optionB(deltaData):
105+
await applyDeltaResponse(deltaData, callback: lastEditCallback, textView: textView)
106+
}
107+
}
108+
109+
/// Requests and applies tokens for an entire document. This does not require a previous response id, and should be
110+
/// used in place of `requestDeltaTokens` when that's the case.
111+
private func requestTokens(languageServer: LanguageServer, textView: TextView) async throws {
112+
guard let response = try await languageServer.requestSemanticTokens(for: documentURI) else {
113+
return
114+
}
115+
await applyEntireResponse(response, callback: lastEditCallback)
116+
}
117+
118+
// MARK: - Apply LSP Response
119+
120+
/// Applies a delta response from the LSP to our storage.
121+
private func applyDeltaResponse(_ data: SemanticTokensDelta, callback: EditCallback?, textView: TextView?) async {
122+
let lspRanges = storage.applyDelta(data)
123+
lastEditCallback = nil // Don't use this callback again.
124+
await MainActor.run {
125+
let ranges = lspRanges.compactMap { textView?.nsRangeFrom($0) }
126+
callback?(.success(IndexSet(ranges: ranges)))
75127
}
76128
}
77129

130+
private func applyEntireResponse(_ data: SemanticTokens, callback: EditCallback?) async {
131+
storage.setData(data)
132+
lastEditCallback = nil // Don't use this callback again.
133+
await callback?(.success(IndexSet(integersIn: documentRange)))
134+
}
135+
136+
// MARK: - Highlight Provider Conformance
137+
78138
func setUp(textView: TextView, codeLanguage: CodeLanguage) {
79139
// Send off a request to get the initial token data
80140
self.textView = textView
@@ -90,12 +150,12 @@ final class SemanticTokenHighlightProvider<Storage: SemanticTokenStorage>: Highl
90150
lastEditCallback = completion
91151
}
92152

93-
func queryHighlightsFor(
94-
textView: TextView,
95-
range: NSRange,
96-
completion: @escaping @MainActor (Result<[HighlightRange], any Error>) -> Void
97-
) {
98-
print("Querying highlights")
153+
func queryHighlightsFor(textView: TextView, range: NSRange, completion: @escaping HighlightCallback) {
154+
guard storage.hasTokens else {
155+
pendingHighlightCallbacks.append(completion)
156+
return
157+
}
158+
99159
guard let lspRange = textView.lspRangeFrom(nsRange: range) else {
100160
completion(.failure(HighlightError.lspRangeFailure))
101161
return
@@ -104,24 +164,4 @@ final class SemanticTokenHighlightProvider<Storage: SemanticTokenStorage>: Highl
104164
let highlights = tokenMap.decode(tokens: rawTokens, using: textView)
105165
completion(.success(highlights))
106166
}
107-
108-
// MARK: - Apply Response
109-
110-
private func applyDeltaResponse(_ data: SemanticTokensDelta, callback: EditCallback?, textView: TextView?) async {
111-
print("Applying delta: \(data)")
112-
let lspRanges = storage.applyDelta(data, requestId: data.resultId)
113-
await MainActor.run {
114-
let ranges = lspRanges.compactMap { textView?.nsRangeFrom($0) }
115-
callback?(.success(IndexSet(ranges: ranges)))
116-
}
117-
lastEditCallback = nil // Don't use this callback again.
118-
}
119-
120-
private func applyEntireResponse(_ data: SemanticTokens, callback: EditCallback?) async {
121-
print("Applying entire: \(data)")
122-
storage.setData(data)
123-
await callback?(.success(IndexSet(integersIn: documentRange)))
124-
lastEditCallback = nil // Don't use this callback again.
125-
}
126-
127167
}

CodeEdit/Features/LSP/Editor/SemanticTokenMap.swift

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -66,12 +66,14 @@ struct SemanticTokenMap: Sendable { // swiftlint:enable line_length
6666
return nil
6767
}
6868

69+
// Only modifiers are bit packed, capture types are given as a simple index into the ``tokenTypeMap``
6970
let modifiers = decodeModifier(token.modifiers)
7071

71-
// Capture types are indicated by the index of the set bit.
72-
let type = token.type > 0 ? Int(token.type.trailingZeroBitCount) : -1 // Don't try to decode 0
72+
let type = Int(token.type)
7373
let capture = tokenTypeMap.indices.contains(type) ? tokenTypeMap[type] : nil
7474

75+
// print(token.line, token.char, token.length, range, capture, modifiers.values)
76+
7577
return HighlightRange(
7678
range: range,
7779
capture: capture,

CodeEdit/Features/LSP/Editor/SemanticTokenStorage/LSPSemanticTokenStorage.swift

Lines changed: 35 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -19,13 +19,17 @@ import CodeEditSourceEditor
1919
final class LSPSemanticTokenStorage: SemanticTokenStorage {
2020
/// Represents compressed semantic token data received from a language server.
2121
struct CurrentState {
22-
let requestId: String?
22+
let resultId: String?
2323
let tokenData: [UInt32]
2424
let tokens: [SemanticToken]
2525
}
2626

27-
var lastRequestId: String? {
28-
state?.requestId
27+
var lastResultId: String? {
28+
state?.resultId
29+
}
30+
31+
var hasTokens: Bool {
32+
state != nil
2933
}
3034

3135
var state: CurrentState?
@@ -42,17 +46,18 @@ final class LSPSemanticTokenStorage: SemanticTokenStorage {
4246
}
4347
var tokens: [SemanticToken] = []
4448

45-
var idx = findIndex(of: range.start, data: state.tokens[...])
46-
while idx < state.tokens.count && state.tokens[idx].startPosition > range.end {
47-
tokens.append(state.tokens[idx])
48-
idx += 1
49-
}
49+
// var idx = findLowerBound(of: range.start, data: state.tokens[...])
50+
// while idx < state.tokens.count && state.tokens[idx].startPosition < range.end {
51+
// tokens.append(state.tokens[idx])
52+
// idx += 1
53+
// }
5054

5155
return tokens
5256
}
5357

5458
func setData(_ data: borrowing SemanticTokens) {
55-
state = CurrentState(requestId: nil, tokenData: data.data, tokens: data.decode())
59+
print(data.decode())
60+
state = CurrentState(resultId: data.resultId, tokenData: data.data, tokens: data.decode())
5661
}
5762

5863
/// Apply a delta object from a language server and returns all token ranges that may need re-drawing.
@@ -64,7 +69,7 @@ final class LSPSemanticTokenStorage: SemanticTokenStorage {
6469
///
6570
/// - Parameter deltas: The deltas to apply.
6671
/// - Returns: All ranges invalidated by the applied deltas.
67-
func applyDelta(_ deltas: SemanticTokensDelta, requestId: String?) -> [SemanticTokenRange] {
72+
func applyDelta(_ deltas: SemanticTokensDelta) -> [SemanticTokenRange] {
6873
assert(state != nil, "State should be set before applying any deltas.")
6974
guard var tokenData = state?.tokenData else { return [] }
7075
var invalidatedSet: [SemanticTokenRange] = []
@@ -104,7 +109,7 @@ final class LSPSemanticTokenStorage: SemanticTokenStorage {
104109
modifiers: tokenData[idx + 4]
105110
))
106111
}
107-
state = CurrentState(requestId: requestId, tokenData: tokenData, tokens: decodedTokens)
112+
state = CurrentState(resultId: deltas.resultId, tokenData: tokenData, tokens: decodedTokens)
108113

109114
return invalidatedSet
110115
}
@@ -131,21 +136,24 @@ final class LSPSemanticTokenStorage: SemanticTokenStorage {
131136

132137
/// Perform a binary search to find the given position
133138
/// - Complexity: O(log n)
134-
func findIndex(of position: Position, data: ArraySlice<SemanticToken>) -> Int {
135-
var lower = 0
136-
var upper = data.count
137-
var idx = 0
138-
while lower <= upper {
139-
idx = lower + upper / 2
140-
if data[idx].startPosition < position {
141-
lower = idx + 1
142-
} else if data[idx].startPosition > position {
143-
upper = idx
144-
} else {
145-
return idx
146-
}
147-
}
148-
149-
return idx
139+
func findLowerBound(in range: LSPRange, data: ArraySlice<SemanticToken>) -> Int? {
140+
// TODO: This needs to find the closest value in a range, there's a good chance there's no result for a
141+
// specific indice
142+
// var lower = 0
143+
// var upper = data.count
144+
// var idx = 0
145+
// while lower < upper {
146+
// idx = lower + upper / 2
147+
// if data[idx].startPosition < position {
148+
// lower = idx + 1
149+
// } else if data[idx].startPosition > position {
150+
// upper = idx
151+
// } else {
152+
// return idx
153+
// }
154+
// }
155+
//
156+
// return (data[idx].startPosition..<data[idx].endPosition).contains(position) ? idx : nil
157+
return nil
150158
}
151159
}

CodeEdit/Features/LSP/Editor/SemanticTokenStorage/SemanticTokenStorage.swift

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,12 @@ import CodeEditSourceEditor
1414
/// There is only one concrete type that conforms to this in CE, but this protocol is useful in testing.
1515
/// See ``LSPSemanticTokenStorage`` for use.
1616
protocol SemanticTokenStorage: AnyObject {
17-
var lastRequestId: String? { get }
17+
var lastResultId: String? { get }
18+
var hasTokens: Bool { get }
1819

1920
init()
2021

2122
func getTokensFor(range: LSPRange) -> [SemanticToken]
2223
func setData(_ data: borrowing SemanticTokens)
23-
func applyDelta(_ deltas: SemanticTokensDelta, requestId: String?) -> [SemanticTokenRange]
24+
func applyDelta(_ deltas: SemanticTokensDelta) -> [SemanticTokenRange]
2425
}

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

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ extension LanguageServer {
2929
)
3030
try await lspInstance.textDocumentDidOpen(DidOpenTextDocumentParams(textDocument: textDocument))
3131

32-
try await updateIsolatedDocument(document)
32+
await updateIsolatedDocument(document)
3333
} catch {
3434
logger.warning("addDocument: Error \(error)")
3535
throw error
@@ -117,18 +117,16 @@ extension LanguageServer {
117117
@MainActor
118118
private func getIsolatedDocumentContent(_ document: CodeFileDocument) -> DocumentContent? {
119119
guard let uri = document.languageServerURI,
120-
let language = document.getLanguage().lspLanguage,
121120
let content = document.content?.string else {
122121
return nil
123122
}
124-
return DocumentContent(uri: uri, language: language, string: content)
123+
return DocumentContent(uri: uri, language: document.getLanguage().id.rawValue, string: content)
125124
}
126125

127126
@MainActor
128-
private func updateIsolatedDocument(_ document: CodeFileDocument) async throws {
127+
private func updateIsolatedDocument(_ document: CodeFileDocument) {
129128
document.lspCoordinator = openFiles.contentCoordinator(for: document)
130129
document.lspHighlightProvider = openFiles.semanticHighlighter(for: document)
131-
try await document.lspHighlightProvider?.documentDidChange()
132130
}
133131

134132
// swiftlint:disable line_length
@@ -163,7 +161,7 @@ extension LanguageServer {
163161
// Used to avoid a lint error (`large_tuple`) for the return type of `getIsolatedDocumentContent`
164162
fileprivate struct DocumentContent {
165163
let uri: String
166-
let language: LanguageIdentifier
164+
let language: String
167165
let string: String
168166
}
169167
}

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

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ import Foundation
99
import LanguageServerProtocol
1010

1111
extension LanguageServer {
12-
/// Setup and test the validity of a rename operation at a given location
1312
func requestSemanticTokens(for documentURI: String) async throws -> SemanticTokensResponse {
1413
do {
1514
logger.log("Requesting all tokens")

CodeEdit/Features/LSP/Service/LSPService+Events.swift

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,6 @@ extension LSPService {
3333

3434
private func handleEvent(_ event: ServerEvent, for key: ClientKey) {
3535
// TODO: Handle Events
36-
print(event)
3736
// switch event {
3837
// case let .request(id, request):
3938
// print("Request ID: \(id) for \(key.languageId.rawValue)")
@@ -47,7 +46,6 @@ extension LSPService {
4746

4847
private func handleRequest(_ request: ServerRequest) {
4948
// TODO: Handle Requests
50-
print(request )
5149
// switch request {
5250
// case let .workspaceConfiguration(params, _):
5351
// print("workspaceConfiguration: \(params)")
@@ -77,7 +75,6 @@ extension LSPService {
7775

7876
private func handleNotification(_ notification: ServerNotification) {
7977
// TODO: Handle Notifications
80-
print(notification)
8178
// switch notification {
8279
// case let .windowLogMessage(params):
8380
// print("windowLogMessage \(params.type)\n```\n\(params.message)\n```\n")

0 commit comments

Comments
 (0)