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) + } +}