mirror of
https://github.com/apple/sourcekit-lsp.git
synced 2026-03-02 18:23:24 +01:00
180 lines
5.3 KiB
Swift
180 lines
5.3 KiB
Swift
//===----------------------------------------------------------------------===//
|
|
//
|
|
// This source file is part of the Swift.org open source project
|
|
//
|
|
// Copyright (c) 2014 - 2019 Apple Inc. and the Swift project authors
|
|
// Licensed under Apache License v2.0 with Runtime Library Exception
|
|
//
|
|
// See https://swift.org/LICENSE.txt for license information
|
|
// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
|
|
//
|
|
//===----------------------------------------------------------------------===//
|
|
|
|
import Foundation
|
|
@_spi(SourceKitLSP) import SKLogging
|
|
@_spi(RawSyntax) import SwiftSyntax
|
|
|
|
/// Translate SourceKit placeholder syntax — `<#foo#>` — in `input` to LSP
|
|
/// placeholder syntax: `${n:foo}`.
|
|
///
|
|
/// If `clientSupportsSnippets` is `false`, the placeholder is rendered as an
|
|
/// empty string, to prevent the client from inserting special placeholder
|
|
/// characters as if they were literal text.
|
|
@_spi(Testing)
|
|
public func rewriteSourceKitPlaceholders(in input: String, clientSupportsSnippets: Bool) -> String {
|
|
var result = ""
|
|
var nextPlaceholderNumber = 1
|
|
// Current stack of nested placeholders, most nested last. Each element needs
|
|
// to be rendered inside the element before it.
|
|
var placeholders: [(number: Int, contents: String)] = []
|
|
let tokens = tokenize(input)
|
|
for token in tokens {
|
|
switch token {
|
|
case let .text(text):
|
|
if placeholders.isEmpty {
|
|
result += text
|
|
} else {
|
|
placeholders.latest.contents += text
|
|
}
|
|
|
|
case .escapeInsidePlaceholder(let character):
|
|
if placeholders.isEmpty {
|
|
result.append(character)
|
|
} else {
|
|
// A closing brace is only escaped _inside_ a placeholder; otherwise the client would include the backslashes
|
|
// literally.
|
|
placeholders.latest.contents += [#"\"#, character]
|
|
}
|
|
|
|
case .placeholderOpen:
|
|
placeholders.append((number: nextPlaceholderNumber, contents: ""))
|
|
nextPlaceholderNumber += 1
|
|
|
|
case .placeholderClose:
|
|
guard let (number, placeholderBody) = placeholders.popLast() else {
|
|
logger.fault("Invalid placeholder in \(input)")
|
|
return input
|
|
}
|
|
guard let displayName = nameForSnippet(placeholderBody) else {
|
|
logger.fault("Failed to decode placeholder \(placeholderBody) in \(input)")
|
|
return input
|
|
}
|
|
let placeholder =
|
|
clientSupportsSnippets
|
|
? formatLSPPlaceholder(displayName, number: number)
|
|
: ""
|
|
if placeholders.isEmpty {
|
|
result += placeholder
|
|
} else {
|
|
placeholders.latest.contents += placeholder
|
|
}
|
|
}
|
|
}
|
|
|
|
return result
|
|
}
|
|
|
|
/// Scan `input` to identify special elements within: curly braces, which may
|
|
/// need to be escaped; and SourceKit placeholder open/close delimiters.
|
|
private func tokenize(_ input: String) -> [SnippetToken] {
|
|
var index = input.startIndex
|
|
var isAtEnd: Bool { index == input.endIndex }
|
|
func match(_ char: Character) -> Bool {
|
|
if isAtEnd || input[index] != char {
|
|
return false
|
|
} else {
|
|
input.formIndex(after: &index)
|
|
return true
|
|
}
|
|
}
|
|
func next() -> Character? {
|
|
guard !isAtEnd else { return nil }
|
|
defer { input.formIndex(after: &index) }
|
|
return input[index]
|
|
}
|
|
|
|
var tokens: [SnippetToken] = []
|
|
var text = ""
|
|
while let char = next() {
|
|
switch char {
|
|
case "<":
|
|
if match("#") {
|
|
tokens.append(.text(text))
|
|
text.removeAll()
|
|
tokens.append(.placeholderOpen)
|
|
} else {
|
|
text.append(char)
|
|
}
|
|
|
|
case "#":
|
|
if match(">") {
|
|
tokens.append(.text(text))
|
|
text.removeAll()
|
|
tokens.append(.placeholderClose)
|
|
} else {
|
|
text.append(char)
|
|
}
|
|
|
|
case "$", "}", "\\":
|
|
tokens.append(.text(text))
|
|
text.removeAll()
|
|
tokens.append(.escapeInsidePlaceholder(char))
|
|
|
|
case let c:
|
|
text.append(c)
|
|
}
|
|
}
|
|
|
|
tokens.append(.text(text))
|
|
|
|
return tokens
|
|
}
|
|
|
|
/// A syntactical element inside a SourceKit snippet.
|
|
private enum SnippetToken {
|
|
/// A placeholder delimiter.
|
|
case placeholderOpen, placeholderClose
|
|
/// '$', '}' or '\', which need to be escaped when used inside a placeholder.
|
|
case escapeInsidePlaceholder(Character)
|
|
/// Any other consecutive run of characters from the input, which needs no
|
|
/// special treatment.
|
|
case text(String)
|
|
}
|
|
|
|
/// Given the interior text of a SourceKit placeholder, extract a display name
|
|
/// suitable for a LSP snippet.
|
|
private func nameForSnippet(_ body: String) -> String? {
|
|
var text = rewrappedAsPlaceholder(body)
|
|
return text.withSyntaxText {
|
|
guard let data = RawEditorPlaceholderData(syntaxText: $0) else {
|
|
return nil
|
|
}
|
|
return String(syntaxText: data.typeForExpansionText ?? data.displayText)
|
|
}
|
|
}
|
|
|
|
private let placeholderStart = "<#"
|
|
private let placeholderEnd = "#>"
|
|
private func rewrappedAsPlaceholder(_ body: String) -> String {
|
|
return placeholderStart + body + placeholderEnd
|
|
}
|
|
|
|
/// Wrap `body` in LSP snippet placeholder syntax, using `number` as the
|
|
/// placeholder's index in the snippet.
|
|
private func formatLSPPlaceholder(_ body: String, number: Int) -> String {
|
|
"${\(number):\(body)}"
|
|
}
|
|
|
|
private extension Array {
|
|
/// Mutable access to the final element of an array.
|
|
///
|
|
/// - precondition: The array must not be empty.
|
|
var latest: Element {
|
|
get { self.last! }
|
|
_modify {
|
|
let index = self.index(before: self.endIndex)
|
|
yield &self[index]
|
|
}
|
|
}
|
|
}
|