diff --git a/.gitignore b/.gitignore
index 6ea2fdd..d714723 100644
--- a/.gitignore
+++ b/.gitignore
@@ -4,4 +4,4 @@
/*.xcodeproj
xcuserdata/
DerivedData/
-.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
\ No newline at end of file
+.swiftpm/xcode/xcuserdata
\ No newline at end of file
diff --git a/.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata b/.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
new file mode 100644
index 0000000..919434a
--- /dev/null
+++ b/.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
@@ -0,0 +1,7 @@
+
+
+
+
+
diff --git a/.swiftpm/xcode/xcshareddata/xcschemes/LanguageServerProtocol.xcscheme b/.swiftpm/xcode/xcshareddata/xcschemes/LanguageServerProtocol.xcscheme
new file mode 100644
index 0000000..f8dee15
--- /dev/null
+++ b/.swiftpm/xcode/xcshareddata/xcschemes/LanguageServerProtocol.xcscheme
@@ -0,0 +1,102 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/README.md b/README.md
index 5d6279b..69ebc11 100644
--- a/README.md
+++ b/README.md
@@ -28,13 +28,8 @@ dependencies: [
For the most part, this library strives to be a straightfoward version of the spec in Swift. There are a few places, however, where it just makes sense to pull in some extra functionality.
-### Snippet
-
-This type makes it easier to interpret the contents of completion results.
-
-### SemanticTokenRepresentation
-
-This is an actor that handles requestig and decoding semantic token information.
+- `Snippet`: makes it easier to interpret the contents of completion results
+- `SemanticTokenClient`: helps to consume Semantic Token information
## Supported Features
diff --git a/Sources/LanguageServerProtocol/Additions/SemanticTokensClient.swift b/Sources/LanguageServerProtocol/Additions/SemanticTokensClient.swift
new file mode 100644
index 0000000..59acfd6
--- /dev/null
+++ b/Sources/LanguageServerProtocol/Additions/SemanticTokensClient.swift
@@ -0,0 +1,49 @@
+import Foundation
+
+/// Handles requesting and decoding Semantic Token information.
+@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *)
+public actor SemanticTokensClient {
+ public private(set) var lastResultId: String? = nil
+ private var tokenRepresentation: TokenRepresentation
+ public let textDocument: TextDocumentIdentifier
+ public let server: Server
+
+ public init(legend: SemanticTokensLegend, textDocument: TextDocumentIdentifier, server: Server) {
+ self.tokenRepresentation = TokenRepresentation(legend: legend)
+ self.textDocument = textDocument
+ self.server = server
+ }
+
+ public func tokens(in range: LSPRange, supportsDeltas: Bool = true) async throws -> [Token] {
+ let response = try await requestTokens(supportsDeltas: supportsDeltas)
+
+ switch response {
+ case .optionA(let fullResponse):
+ self.lastResultId = fullResponse.resultId
+
+ _ = tokenRepresentation.applyData(fullResponse.data)
+ case .optionB(let delta):
+ self.lastResultId = delta.resultId
+
+ _ = tokenRepresentation.applyEdits(delta.edits)
+ case nil:
+ return []
+ }
+
+ return tokenRepresentation.decodeTokens(in: range)
+ }
+
+ private func requestTokens(supportsDeltas: Bool) async throws -> SemanticTokensDeltaResponse {
+ if let resultId = self.lastResultId, supportsDeltas {
+ let params = SemanticTokensDeltaParams(textDocument: textDocument, previousResultId: resultId)
+
+ return try await self.server.semanticTokensFullDelta(params: params)
+ }
+
+ let params = SemanticTokensParams(textDocument: textDocument)
+
+ // translate to a delta response first so we can use a uniform handler for both cases
+ return try await server.semanticTokensFull(params: params)
+ .map({ .optionA($0) })
+ }
+}
diff --git a/Sources/LanguageServerProtocol/Additions/TokenRepresentation.swift b/Sources/LanguageServerProtocol/Additions/TokenRepresentation.swift
new file mode 100644
index 0000000..4b51eab
--- /dev/null
+++ b/Sources/LanguageServerProtocol/Additions/TokenRepresentation.swift
@@ -0,0 +1,127 @@
+import Foundation
+
+/// A structure representing a Semantic Token.
+public struct Token: Codable, Hashable, Sendable {
+ public let range: LSPRange
+ public let tokenType: String
+ public let modifiers: Set
+
+ public init(range: LSPRange, tokenType: String, modifiers: Set = Set()) {
+ self.range = range
+ self.tokenType = tokenType
+ self.modifiers = modifiers
+ }
+}
+
+/// Stores and updates raw Semantic Token data and converts it into Tokens.
+public class TokenRepresentation {
+ private var data: [UInt32]
+ public let legend: SemanticTokensLegend
+
+ public init(legend: SemanticTokensLegend) {
+ self.data = []
+ self.legend = legend
+ }
+
+ /// Merge new token data with existing representation
+ ///
+ /// - Returns: Ranges affected by the new data (currently unimplemented).
+ public func applyData(_ newData: [UInt32]) -> [LSPRange] {
+ let minLength = min(data.count, newData.count)
+
+ var diffStartIndex = 0
+
+ for _ in 0.. 0 {
+ return []
+ }
+
+ let diffValues = newData.suffix(from: diffStartIndex)
+
+ self.data.replaceSubrange(diffRange, with: diffValues)
+
+ return []
+ }
+
+ /// Apply edits to the existing representation
+ ///
+ /// - Returns: Ranges affected by the new data (currently unimplemented).
+ public func applyEdits(_ edits: [SemanticTokensEdit]) -> [LSPRange] {
+ // sort high to low
+ let descendingEdits = edits.sorted(by: {a, b in a.start > b.start })
+
+ for edit in descendingEdits {
+ let start = Int(edit.start)
+ let end = Int(edit.start + edit.deleteCount)
+
+ let newData = edit.data ?? []
+
+ data.replaceSubrange(start.. Token? {
+ guard typeIndex < legend.tokenTypes.endIndex else {
+ return nil
+ }
+
+ let tokenType = legend.tokenTypes[typeIndex]
+
+ return Token(range: range, tokenType: tokenType, modifiers: Set())
+ }
+
+ /// Compute `Token` values within a range.
+ public func decodeTokens(in range: LSPRange) -> [Token] {
+ var lastLine: Int?
+ var lastStartChar: Int?
+
+ var tokens = [Token]()
+
+ for i in stride(from: 0, to: data.count, by: 5) {
+ let lineDelta = Int(data[i])
+ let startingLine = lastLine ?? 0
+ let line = startingLine + lineDelta
+
+ lastLine = line
+
+ let charDelta = Int(data[i+1])
+ let startingChar = lastStartChar ?? 0
+ let startChar = (lineDelta == 0 ? startingChar : 0) + charDelta
+
+ lastStartChar = startChar
+
+ let length = Int(data[i+2])
+
+ let start = Position(line: line, character: startChar)
+ if start >= range.end {
+ break
+ }
+
+ let end = Position(line: line, character: startChar + length)
+ if end < range.start {
+ continue
+ }
+
+ let tokenRange = LSPRange(start: start, end: end)
+ let typeIndex = Int(data[i+3])
+
+ if let token = makeToken(range: tokenRange, typeIndex: typeIndex) {
+ tokens.append(token)
+ }
+ }
+
+ return tokens
+ }
+}
+
diff --git a/Sources/LanguageServerProtocol/LanguageFeatures/SemanticTokens.swift b/Sources/LanguageServerProtocol/LanguageFeatures/SemanticTokens.swift
index 5af85ff..98e045e 100644
--- a/Sources/LanguageServerProtocol/LanguageFeatures/SemanticTokens.swift
+++ b/Sources/LanguageServerProtocol/LanguageFeatures/SemanticTokens.swift
@@ -179,3 +179,14 @@ public struct SemanticTokensRangeParams: Codable {
self.range = range
}
}
+
+public extension TwoTypeOption where T == SemanticTokens, U == SemanticTokensDelta {
+ var resultId: String? {
+ switch self {
+ case .optionA(let token):
+ return token.resultId
+ case .optionB(let delta):
+ return delta.resultId
+ }
+ }
+}
diff --git a/Sources/LanguageServerProtocol/SemanticTokenRepresentation.swift b/Sources/LanguageServerProtocol/SemanticTokenRepresentation.swift
deleted file mode 100644
index b2ae1d3..0000000
--- a/Sources/LanguageServerProtocol/SemanticTokenRepresentation.swift
+++ /dev/null
@@ -1,167 +0,0 @@
-import Foundation
-
-public struct Token: Codable, Hashable, Sendable {
- public let range: LSPRange
- public let tokenType: String
- public let modifiers: Set
-
- public init(range: LSPRange, tokenType: String, modifiers: Set = Set()) {
- self.range = range
- self.tokenType = tokenType
- self.modifiers = modifiers
- }
-}
-
-@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *)
-public actor SemanticTokenRepresentation {
- public private(set) var lastResultId: String?
- private var data: [UInt32]
- public let legend: SemanticTokensLegend
- public let textDocument: TextDocumentIdentifier
- public let server: Server
-
- public init(legend: SemanticTokensLegend, textDocument: TextDocumentIdentifier, server: Server) {
- self.lastResultId = nil
- self.data = []
- self.legend = legend
- self.textDocument = textDocument
- self.server = server
- }
-
- public nonisolated func tokens(in range: LSPRange, supportsDeltas: Bool = true) async throws -> [Token] {
- let tokensResponse = try await requestTokens(supportsDeltas: supportsDeltas)
-
- _ = await handleResponse(tokensResponse)
-
- let tokenData = await self.data
-
- return decodeTokens(from: tokenData, in: range)
- }
-
- private func requestTokens(supportsDeltas: Bool) async throws -> SemanticTokensDeltaResponse {
- if let resultId = self.lastResultId, supportsDeltas {
- let params = SemanticTokensDeltaParams(textDocument: textDocument, previousResultId: resultId)
-
- return try await self.server.semanticTokensFullDelta(params: params)
- }
-
- let params = SemanticTokensParams(textDocument: textDocument)
-
- // translate to a delta response first so we can use a uniform handler for both cases
- return try await server.semanticTokensFull(params: params)
- .map({ .optionA($0) })
- }
-
- private func handleResponse(_ response: SemanticTokensDeltaResponse) -> [LSPRange] {
- switch response {
- case .optionA(let fullResponse):
- return applyFull(fullResponse)
- case .optionB(let delta):
- return applyDelta(delta)
- case nil:
- return []
- }
- }
-
- private func applyFull(_ full: SemanticTokens) -> [LSPRange] {
- self.lastResultId = full.resultId
-
- let minLength = min(data.count, full.data.count)
-
- var diffStartIndex = 0
-
- for _ in 0.. 0 {
- return []
- }
-
- let diffValues = full.data.suffix(from: diffStartIndex)
-
- self.data.replaceSubrange(diffRange, with: diffValues)
-
- return []
- }
-
- private func applyDelta(_ delta: SemanticTokensDelta) -> [LSPRange] {
- self.lastResultId = delta.resultId
-
- // sort high to low
- let descendingEdits = delta.edits.sorted(by: {a, b in a.start > b.start })
-
- for edit in descendingEdits {
- let start = Int(edit.start)
- let end = Int(edit.start + edit.deleteCount)
-
- let newData = edit.data ?? []
-
- data.replaceSubrange(start..) -> LSPRange {
- return LSPRange(startPair: (0, 0), endPair: (0, 0))
- }
-
- private nonisolated func makeToken(range: LSPRange, typeIndex: Int) -> Token? {
- guard typeIndex < legend.tokenTypes.endIndex else {
- return nil
- }
-
- let tokenType = legend.tokenTypes[typeIndex]
-
- return Token(range: range, tokenType: tokenType, modifiers: Set())
- }
-
- private nonisolated func decodeTokens(from data: [UInt32], in range: LSPRange) -> [Token] {
- var lastLine: Int?
- var lastStartChar: Int?
-
- var tokens = [Token]()
-
- for i in stride(from: 0, to: data.count, by: 5) {
- let lineDelta = Int(data[i])
- let startingLine = lastLine ?? 0
- let line = startingLine + lineDelta
-
- lastLine = line
-
- let charDelta = Int(data[i+1])
- let startingChar = lastStartChar ?? 0
- let startChar = (lineDelta == 0 ? startingChar : 0) + charDelta
-
- lastStartChar = startChar
-
- let length = Int(data[i+2])
-
- let start = Position(line: line, character: startChar)
- if start >= range.end {
- break
- }
-
- let end = Position(line: line, character: startChar + length)
- if end < range.start {
- continue
- }
-
- let tokenRange = LSPRange(start: start, end: end)
- let typeIndex = Int(data[i+3])
-
- if let token = makeToken(range: tokenRange, typeIndex: typeIndex) {
- tokens.append(token)
- }
- }
-
- return tokens
- }
-}
diff --git a/Tests/LanguageServerProtocolTests/TokenRepresentationTests.swift b/Tests/LanguageServerProtocolTests/TokenRepresentationTests.swift
new file mode 100644
index 0000000..759b184
--- /dev/null
+++ b/Tests/LanguageServerProtocolTests/TokenRepresentationTests.swift
@@ -0,0 +1,45 @@
+import XCTest
+@testable import LanguageServerProtocol
+
+// All based on gopls and this Go source file:
+
+// package main
+//
+// import "fmt"
+// import "log"
+//
+// func main() {
+// fmt.Println("hello world")
+// }
+
+final class TokenRepresentationTests: XCTestCase {
+ func testDataDecode() {
+ let tokenTypes = ["namespace", "type", "class", "enum", "interface", "struct", "typeParameter", "parameter", "variable", "property", "enumMember", "event", "function", "method", "macro", "keyword", "modifier", "comment", "string", "number", "regexp", "operator"]
+ let tokenModifiers = ["declaration", "definition", "readonly", "static", "deprecated", "abstract", "async", "modification", "documentation", "defaultLibrary"]
+ let legend = SemanticTokensLegend(tokenTypes: tokenTypes, tokenModifiers: tokenModifiers)
+ let rep = TokenRepresentation(legend: legend)
+
+ let codedData: [UInt32] = [0,0,7,15,0,0,8,4,0,0,2,0,6,15,0,0,8,3,0,0,1,0,6,15,0,0,8,3,0,0,2,0,4,15,0,0,5,4,12,2,1,4,3,0,0,0,4,7,12,0,0,8,13,18,0]
+
+ _ = rep.applyData(codedData)
+
+ let range = LSPRange(startPair: (0, 0), endPair: (7, 1))
+ let tokens = rep.decodeTokens(in: range)
+
+ let expectedTokens = [
+ Token(range: LSPRange(startPair: (0, 0), endPair: (0, 7)), tokenType: "keyword"),
+ Token(range: LSPRange(startPair: (0, 8), endPair: (0, 12)), tokenType: "namespace"),
+ Token(range: LSPRange(startPair: (2, 0), endPair: (2, 6)), tokenType: "keyword"),
+ Token(range: LSPRange(startPair: (2, 8), endPair: (2, 11)), tokenType: "namespace"),
+ Token(range: LSPRange(startPair: (3, 0), endPair: (3, 6)), tokenType: "keyword"),
+ Token(range: LSPRange(startPair: (3, 8), endPair: (3, 11)), tokenType: "namespace"),
+ Token(range: LSPRange(startPair: (5, 0), endPair: (5, 4)), tokenType: "keyword"),
+ Token(range: LSPRange(startPair: (5, 5), endPair: (5, 9)), tokenType: "function"),
+ Token(range: LSPRange(startPair: (6, 4), endPair: (6, 7)), tokenType: "namespace"),
+ Token(range: LSPRange(startPair: (6, 8), endPair: (6, 15)), tokenType: "function"),
+ Token(range: LSPRange(startPair: (6, 16), endPair: (6, 29)), tokenType: "string"),
+ ]
+
+ XCTAssertEqual(tokens, expectedTokens)
+ }
+}