Skip to content

Commit

Permalink
SemanticTokenRepresentation + async/await
Browse files Browse the repository at this point in the history
  • Loading branch information
mattmassicotte committed Jun 28, 2022
1 parent 3e9e70b commit 248c474
Show file tree
Hide file tree
Showing 3 changed files with 263 additions and 2 deletions.
4 changes: 2 additions & 2 deletions Sources/LanguageServerProtocol/BasicStructures.swift
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import Foundation
import AnyCodable

public struct Position: Codable, Hashable {
public struct Position: Codable, Hashable, Sendable {
static let zero = Position(line: 0, character: 0)

public let line: Int
Expand Down Expand Up @@ -34,7 +34,7 @@ extension Position: Comparable {
}
}

public struct LSPRange: Codable, Hashable {
public struct LSPRange: Codable, Hashable, Sendable {
static let zero = LSPRange(start: .zero, end: .zero)

public let start: Position
Expand Down
167 changes: 167 additions & 0 deletions Sources/LanguageServerProtocol/SemanticTokenRepresentation.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
import Foundation

public struct Token: Codable, Hashable, Sendable {
public let range: LSPRange
public let tokenType: String
public let modifiers: Set<String>

public init(range: LSPRange, tokenType: String, modifiers: Set<String> = 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..<minLength {
if data[diffStartIndex] != full.data[diffStartIndex] {
break
}

diffStartIndex += 1
}

let diffRange = diffStartIndex..<data.count

if diffRange.isEmpty && data.count > 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..<end, with: newData)
}

return []
}

private nonisolated func lspRange(within dataRange: Range<Int>) -> 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
}
}
94 changes: 94 additions & 0 deletions Sources/LanguageServerProtocol/Server.swift
Original file line number Diff line number Diff line change
Expand Up @@ -81,14 +81,53 @@ public extension Server {
sendNotification(.didOpenTextDocument(params), completionHandler: block)
}

@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *)
func didOpenTextDocument(params: DidOpenTextDocumentParams) async throws {
try await withCheckedThrowingContinuation { (continutation: CheckedContinuation<Void, Error>) in
self.didOpenTextDocument(params: params) { error in
if let error = error {
continutation.resume(throwing: error)
} else {
continutation.resume()
}
}
}
}

func didChangeTextDocument(params: DidChangeTextDocumentParams, block: @escaping (ServerError?) -> Void) {
sendNotification(.didChangeTextDocument(params), completionHandler: block)
}

@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *)
func didChangeTextDocument(params: DidChangeTextDocumentParams) async throws {
try await withCheckedThrowingContinuation { (continutation: CheckedContinuation<Void, Error>) in
self.didChangeTextDocument(params: params) { error in
if let error = error {
continutation.resume(throwing: error)
} else {
continutation.resume()
}
}
}
}

func didCloseTextDocument(params: DidCloseTextDocumentParams, block: @escaping (ServerError?) -> Void) {
sendNotification(.didCloseTextDocument(params), completionHandler: block)
}

@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *)
func didCloseTextDocument(params: DidCloseTextDocumentParams) async throws {
try await withCheckedThrowingContinuation { (continutation: CheckedContinuation<Void, Error>) in
self.didCloseTextDocument(params: params) { error in
if let error = error {
continutation.resume(throwing: error)
} else {
continutation.resume()
}
}
}
}

func willSaveTextDocument(params: WillSaveTextDocumentParams, block: @escaping (ServerError?) -> Void) {
sendNotification(.willSaveTextDocument(params), completionHandler: block)
}
Expand All @@ -109,6 +148,15 @@ public extension Server {
sendRequest(.completion(params), completionHandler: block)
}

@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *)
func completion(params: CompletionParams) async throws -> CompletionResponse {
try await withCheckedThrowingContinuation { continutation in
self.completion(params: params) { result in
continutation.resume(with: result)
}
}
}

func hover(params: TextDocumentPositionParams, block: @escaping (ServerResult<HoverResponse>) -> Void) {
sendRequest(.hover(params), completionHandler: block)
}
Expand All @@ -125,6 +173,15 @@ public extension Server {
sendRequest(.definition(params), completionHandler: block)
}

@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *)
func definition(params: TextDocumentPositionParams) async throws -> DefinitionResponse {
try await withCheckedThrowingContinuation { continutation in
self.definition(params: params) { result in
continutation.resume(with: result)
}
}
}

func typeDefinition(params: TextDocumentPositionParams, block: @escaping (ServerResult<TypeDefinitionResponse>) -> Void) {
sendRequest(.typeDefinition(params), completionHandler: block)
}
Expand Down Expand Up @@ -173,14 +230,41 @@ public extension Server {
sendRequest(.semanticTokensFull(params), completionHandler: block)
}

@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *)
func semanticTokensFull(params: SemanticTokensParams) async throws -> SemanticTokensResponse {
try await withCheckedThrowingContinuation { continutation in
self.semanticTokensFull(params: params) { result in
continutation.resume(with: result)
}
}
}

func semanticTokensFullDelta(params: SemanticTokensDeltaParams, block: @escaping (ServerResult<SemanticTokensDeltaResponse>) -> Void) {
sendRequest(.semanticTokensFullDelta(params), completionHandler: block)
}

@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *)
func semanticTokensFullDelta(params: SemanticTokensDeltaParams) async throws -> SemanticTokensDeltaResponse {
try await withCheckedThrowingContinuation { continutation in
self.semanticTokensFullDelta(params: params) { result in
continutation.resume(with: result)
}
}
}

func semanticTokensRange(params: SemanticTokensRangeParams, block: @escaping (ServerResult<SemanticTokensResponse>) -> Void) {
sendRequest(.semanticTokensRange(params), completionHandler: block)
}

@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *)
func semanticTokensRange(params: SemanticTokensRangeParams) async throws -> SemanticTokensResponse {
try await withCheckedThrowingContinuation { continutation in
self.semanticTokensRange(params: params) { result in
continutation.resume(with: result)
}
}
}

func customRequest<Response: Codable>(method: String, params: AnyCodable, block: @escaping (ServerResult<Response>) -> Void) {
sendRequest(.custom(method, params), completionHandler: block)
}
Expand Down Expand Up @@ -231,6 +315,16 @@ public extension Server {
sendRequest(.workspaceSymbol(params), completionHandler: block)
}

@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *)
func workspaceSymbol(params: WorkspaceSymbolParams) async throws -> WorkspaceSymbolResponse {
try await withCheckedThrowingContinuation { continutation in
self.workspaceSymbol(params: params) { result in
continutation.resume(with: result)
}
}
}


func workspaceSymbolResolve(params: WorkspaceSymbol, block: @escaping (ServerResult<WorkspaceSymbolResponse>) -> Void) {
sendRequest(.workspaceSymbolResolve(params), completionHandler: block)
}
Expand Down

0 comments on commit 248c474

Please sign in to comment.