|
| 1 | +// |
| 2 | +// SemanticTokenMap.swift |
| 3 | +// CodeEdit |
| 4 | +// |
| 5 | +// Created by Khan Winter on 11/10/24. |
| 6 | +// |
| 7 | + |
| 8 | +import LanguageClient |
| 9 | +import LanguageServerProtocol |
| 10 | +import CodeEditSourceEditor |
| 11 | +import CodeEditTextView |
| 12 | + |
| 13 | +// swiftlint:disable line_length |
| 14 | +/// Creates a mapping from a language server's semantic token options to a format readable by CodeEdit. |
| 15 | +/// Provides a convenience method for mapping tokens received from the server to highlight ranges suitable for |
| 16 | +/// highlighting in the editor. |
| 17 | +/// |
| 18 | +/// Use this type to handle the initially received semantic highlight capabilities structures. This type will figure |
| 19 | +/// out how to read it into a format it can use. |
| 20 | +/// |
| 21 | +/// After initialization, the map is static until the server is reinitialized. Consequently, this type is `Sendable` |
| 22 | +/// and immutable after initialization. |
| 23 | +/// |
| 24 | +/// This type is not coupled to any text system via the use of the ``SemanticTokenMapRangeProvider``. When decoding to |
| 25 | +/// highlight ranges, provide a type that can provide ranges for highlighting. |
| 26 | +/// |
| 27 | +/// [LSP Spec](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#semanticTokensLegend) |
| 28 | +struct SemanticTokenMap: Sendable { // swiftlint:enable line_length |
| 29 | + private let tokenTypeMap: [CaptureName?] |
| 30 | + private let modifierMap: [CaptureModifier?] |
| 31 | + |
| 32 | + init(semanticCapability: TwoTypeOption<SemanticTokensOptions, SemanticTokensRegistrationOptions>) { |
| 33 | + let legend: SemanticTokensLegend |
| 34 | + switch semanticCapability { |
| 35 | + case .optionA(let tokensOptions): |
| 36 | + legend = tokensOptions.legend |
| 37 | + case .optionB(let tokensRegistrationOptions): |
| 38 | + legend = tokensRegistrationOptions.legend |
| 39 | + } |
| 40 | + |
| 41 | + tokenTypeMap = legend.tokenTypes.map { CaptureName.fromString($0) } |
| 42 | + modifierMap = legend.tokenModifiers.map { CaptureModifier.fromString($0) } |
| 43 | + } |
| 44 | + |
| 45 | + /// Decodes the compressed semantic token data into a `HighlightRange` type for use in an editor. |
| 46 | + /// This is marked main actor to prevent runtime errors, due to the use of the actor-isolated `rangeProvider`. |
| 47 | + /// - Parameters: |
| 48 | + /// - tokens: Semantic tokens from a language server. |
| 49 | + /// - rangeProvider: The provider to use to translate token ranges to text view ranges. |
| 50 | + /// - Returns: An array of decoded highlight ranges. |
| 51 | + @MainActor |
| 52 | + func decode(tokens: SemanticTokens, using rangeProvider: SemanticTokenMapRangeProvider) -> [HighlightRange] { |
| 53 | + tokens.decode().compactMap { token in |
| 54 | + guard let range = rangeProvider.nsRangeFrom(line: token.line, char: token.char, length: token.length) else { |
| 55 | + return nil |
| 56 | + } |
| 57 | + |
| 58 | + let modifiers = decodeModifier(token.modifiers) |
| 59 | + |
| 60 | + // Capture types are indicated by the index of the set bit. |
| 61 | + let type = token.type > 0 ? Int(token.type.trailingZeroBitCount) : -1 // Don't try to decode 0 |
| 62 | + let capture = tokenTypeMap.indices.contains(type) ? tokenTypeMap[type] : nil |
| 63 | + |
| 64 | + return HighlightRange( |
| 65 | + range: range, |
| 66 | + capture: capture, |
| 67 | + modifiers: modifiers |
| 68 | + ) |
| 69 | + } |
| 70 | + } |
| 71 | + |
| 72 | + /// Decodes a raw modifier value into a set of capture modifiers. |
| 73 | + /// - Parameter raw: The raw modifier integer to decode. |
| 74 | + /// - Returns: A set of modifiers for highlighting. |
| 75 | + func decodeModifier(_ raw: UInt32) -> CaptureModifierSet { |
| 76 | + var modifiers: CaptureModifierSet = [] |
| 77 | + var raw = raw |
| 78 | + while raw > 0 { |
| 79 | + let idx = raw.trailingZeroBitCount |
| 80 | + raw &= ~(1 << idx) |
| 81 | + // We don't use `[safe:]` because it creates a double optional here. If someone knows how to extend |
| 82 | + // a collection of optionals to make that return only a single optional this could be updated. |
| 83 | + guard let modifier = modifierMap.indices.contains(idx) ? modifierMap[idx] : nil else { |
| 84 | + continue |
| 85 | + } |
| 86 | + modifiers.insert(modifier) |
| 87 | + } |
| 88 | + return modifiers |
| 89 | + } |
| 90 | +} |
0 commit comments