mirror of
https://github.com/apple/sourcekit-lsp.git
synced 2026-03-02 18:23:24 +01:00
Sourcekitd returns parse diagnostics immediately after edits, but we do not want to clear the semantic diagnostics until we have semantic level diagnostics from after the edit.
1003 lines
34 KiB
Swift
1003 lines
34 KiB
Swift
//===----------------------------------------------------------------------===//
|
|
//
|
|
// This source file is part of the Swift.org open source project
|
|
//
|
|
// Copyright (c) 2014 - 2018 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 LanguageServerProtocol
|
|
import SKCore
|
|
import SKSupport
|
|
import Basic
|
|
import sourcekitd
|
|
import Dispatch
|
|
import struct Foundation.CharacterSet
|
|
|
|
public final class SwiftLanguageServer: LanguageServer {
|
|
|
|
let sourcekitd: SwiftSourceKitFramework
|
|
|
|
let buildSystem: BuildSystem
|
|
|
|
let clientCapabilities: ClientCapabilities
|
|
|
|
// FIXME: ideally we wouldn't need separate management from a parent server in the same process.
|
|
var documentManager: DocumentManager
|
|
|
|
var currentDiagnostics: [CachedDiagnostic] = []
|
|
|
|
let onExit: () -> Void
|
|
|
|
var api: sourcekitd_functions_t { return sourcekitd.api }
|
|
var keys: sourcekitd_keys { return sourcekitd.keys }
|
|
var requests: sourcekitd_requests { return sourcekitd.requests }
|
|
var values: sourcekitd_values { return sourcekitd.values }
|
|
|
|
/// Creates a language server for the given client using the sourcekitd dylib at the specified path.
|
|
public init(client: Connection, sourcekitd: AbsolutePath, buildSystem: BuildSystem, clientCapabilities: ClientCapabilities, onExit: @escaping () -> Void = {}) throws {
|
|
|
|
self.sourcekitd = try SwiftSourceKitFramework(dylib: sourcekitd)
|
|
self.buildSystem = buildSystem
|
|
self.clientCapabilities = clientCapabilities
|
|
self.documentManager = DocumentManager()
|
|
self.onExit = onExit
|
|
super.init(client: client)
|
|
}
|
|
|
|
public override func _registerBuiltinHandlers() {
|
|
|
|
_register(SwiftLanguageServer.initialize)
|
|
_register(SwiftLanguageServer.clientInitialized)
|
|
_register(SwiftLanguageServer.cancelRequest)
|
|
_register(SwiftLanguageServer.shutdown)
|
|
_register(SwiftLanguageServer.exit)
|
|
_register(SwiftLanguageServer.openDocument)
|
|
_register(SwiftLanguageServer.closeDocument)
|
|
_register(SwiftLanguageServer.changeDocument)
|
|
_register(SwiftLanguageServer.willSaveDocument)
|
|
_register(SwiftLanguageServer.didSaveDocument)
|
|
_register(SwiftLanguageServer.completion)
|
|
_register(SwiftLanguageServer.hover)
|
|
_register(SwiftLanguageServer.documentSymbolHighlight)
|
|
_register(SwiftLanguageServer.foldingRange)
|
|
_register(SwiftLanguageServer.symbolInfo)
|
|
_register(SwiftLanguageServer.documentSymbol)
|
|
_register(SwiftLanguageServer.documentColor)
|
|
_register(SwiftLanguageServer.colorPresentation)
|
|
_register(SwiftLanguageServer.codeAction)
|
|
}
|
|
|
|
func publishDiagnostics(
|
|
response: SKResponseDictionary,
|
|
for snapshot: DocumentSnapshot)
|
|
{
|
|
let stageUID: sourcekitd_uid_t? = response[sourcekitd.keys.diagnostic_stage]
|
|
let stage = stageUID.flatMap { DiagnosticStage($0, sourcekitd: sourcekitd) } ?? .sema
|
|
|
|
// Note: we make the notification even if there are no diagnostics to clear the current state.
|
|
var newDiags: [CachedDiagnostic] = []
|
|
response[keys.diagnostics]?.forEach { _, diag in
|
|
if let diag = CachedDiagnostic(diag, in: snapshot) {
|
|
newDiags.append(diag)
|
|
}
|
|
return true
|
|
}
|
|
|
|
let result = mergeDiagnostics(old: currentDiagnostics, new: newDiags, stage: stage)
|
|
currentDiagnostics = result
|
|
|
|
client.send(PublishDiagnostics(
|
|
url: snapshot.document.url,
|
|
diagnostics: result.map { $0.diagnostic }))
|
|
}
|
|
|
|
func handleDocumentUpdate(url: URL) {
|
|
|
|
guard let snapshot = documentManager.latestSnapshot(url) else {
|
|
return
|
|
}
|
|
|
|
// Make the magic 0,0 replacetext request to update diagnostics.
|
|
|
|
let req = SKRequestDictionary(sourcekitd: sourcekitd)
|
|
req[keys.request] = requests.editor_replacetext
|
|
req[keys.name] = url.path
|
|
req[keys.offset] = 0
|
|
req[keys.length] = 0
|
|
req[keys.sourcetext] = ""
|
|
|
|
if let dict = self.sourcekitd.sendSync(req).success {
|
|
publishDiagnostics(response: dict, for: snapshot)
|
|
}
|
|
}
|
|
}
|
|
|
|
extension SwiftLanguageServer {
|
|
|
|
func initialize(_ request: Request<InitializeRequest>) {
|
|
|
|
api.initialize()
|
|
|
|
api.set_notification_handler { [weak self] notification in
|
|
guard let self = self else { return }
|
|
let notification = SKResponse(notification, sourcekitd: self.sourcekitd)
|
|
|
|
guard let dict = notification.value else {
|
|
log(notification.description, level: .error)
|
|
return
|
|
}
|
|
|
|
logAsync(level: .debug) { _ in notification.description }
|
|
|
|
if let kind: sourcekitd_uid_t = dict[self.keys.notification],
|
|
kind == self.values.notification_documentupdate,
|
|
let name: String = dict[self.keys.name] {
|
|
|
|
self.queue.async {
|
|
self.handleDocumentUpdate(url: URL(fileURLWithPath: name))
|
|
}
|
|
}
|
|
}
|
|
|
|
request.reply(InitializeResult(capabilities: ServerCapabilities(
|
|
textDocumentSync: TextDocumentSyncOptions(
|
|
openClose: true,
|
|
change: .incremental,
|
|
willSave: true,
|
|
willSaveWaitUntil: false,
|
|
save: TextDocumentSyncOptions.SaveOptions(includeText: false)),
|
|
completionProvider: CompletionOptions(
|
|
resolveProvider: false,
|
|
triggerCharacters: ["."]),
|
|
hoverProvider: true,
|
|
definitionProvider: nil,
|
|
implementationProvider: true,
|
|
referencesProvider: nil,
|
|
documentHighlightProvider: true,
|
|
foldingRangeProvider: true,
|
|
documentSymbolProvider: true,
|
|
colorProvider: true,
|
|
codeActionProvider: CodeActionServerCapabilities(
|
|
clientCapabilities: request.params.capabilities.textDocument?.codeAction,
|
|
codeActionOptions: CodeActionOptions(codeActionKinds: nil),
|
|
supportsCodeActions: false) // TODO: Turn it on after a provider is implemented.
|
|
)))
|
|
}
|
|
|
|
func clientInitialized(_: Notification<InitializedNotification>) {
|
|
// Nothing to do.
|
|
}
|
|
|
|
func cancelRequest(_ notification: Notification<CancelRequest>) {
|
|
let key = RequestCancelKey(client: notification.clientID, request: notification.params.id)
|
|
requestCancellation[key]?.cancel()
|
|
}
|
|
|
|
func shutdown(_ request: Request<Shutdown>) {
|
|
api.set_notification_handler(nil)
|
|
}
|
|
|
|
func exit(_ notification: Notification<Exit>) {
|
|
api.shutdown()
|
|
onExit()
|
|
}
|
|
|
|
// MARK: - Text synchronization
|
|
|
|
func openDocument(_ note: Notification<DidOpenTextDocument>) {
|
|
guard let snapshot = documentManager.open(note) else {
|
|
// Already logged failure.
|
|
return
|
|
}
|
|
|
|
let req = SKRequestDictionary(sourcekitd: sourcekitd)
|
|
req[keys.request] = requests.editor_open
|
|
req[keys.name] = note.params.textDocument.url.path
|
|
req[keys.sourcetext] = snapshot.text
|
|
|
|
if let settings = buildSystem.settings(for: snapshot.document.url, snapshot.document.language) {
|
|
req[keys.compilerargs] = settings.compilerArguments
|
|
}
|
|
|
|
guard let dict = self.sourcekitd.sendSync(req).success else {
|
|
// Already logged failure.
|
|
return
|
|
}
|
|
|
|
publishDiagnostics(response: dict, for: snapshot)
|
|
}
|
|
|
|
func closeDocument(_ note: Notification<DidCloseTextDocument>) {
|
|
documentManager.close(note)
|
|
|
|
let req = SKRequestDictionary(sourcekitd: sourcekitd)
|
|
req[keys.request] = requests.editor_close
|
|
req[keys.name] = note.params.textDocument.url.path
|
|
|
|
_ = self.sourcekitd.sendSync(req)
|
|
}
|
|
|
|
func changeDocument(_ note: Notification<DidChangeTextDocument>) {
|
|
|
|
var lastResponse: SKResponseDictionary? = nil
|
|
|
|
let snapshot = documentManager.edit(note) { (before: DocumentSnapshot, edit: TextDocumentContentChangeEvent) in
|
|
let req = SKRequestDictionary(sourcekitd: self.sourcekitd)
|
|
req[self.keys.request] = self.requests.editor_replacetext
|
|
req[self.keys.name] = note.params.textDocument.url.path
|
|
|
|
if let range = edit.range {
|
|
|
|
guard let offset = before.utf8Offset(of: range.lowerBound), let end = before.utf8Offset(of: range.upperBound) else {
|
|
fatalError("invalid edit \(range)")
|
|
}
|
|
|
|
req[self.keys.offset] = offset
|
|
req[self.keys.length] = end - offset
|
|
|
|
} else {
|
|
// Full text
|
|
req[self.keys.offset] = 0
|
|
req[self.keys.length] = before.text.utf8.count
|
|
}
|
|
|
|
req[self.keys.sourcetext] = edit.text
|
|
|
|
lastResponse = self.sourcekitd.sendSync(req).success
|
|
}
|
|
|
|
if let dict = lastResponse, let snapshot = snapshot {
|
|
publishDiagnostics(response: dict, for: snapshot)
|
|
}
|
|
}
|
|
|
|
func willSaveDocument(_ note: Notification<WillSaveTextDocument>) {
|
|
|
|
}
|
|
|
|
func didSaveDocument(_ note: Notification<DidSaveTextDocument>) {
|
|
|
|
}
|
|
|
|
// MARK: - Language features
|
|
|
|
func completion(_ req: Request<CompletionRequest>) {
|
|
|
|
guard let snapshot = documentManager.latestSnapshot(req.params.textDocument.url) else {
|
|
log("failed to find snapshot for url \(req.params.textDocument.url)")
|
|
req.reply(CompletionList(isIncomplete: true, items: []))
|
|
return
|
|
}
|
|
|
|
guard let completionPos = adjustCompletionLocation(req.params.position, in: snapshot) else {
|
|
log("invalid completion position \(req.params.position)")
|
|
req.reply(CompletionList(isIncomplete: true, items: []))
|
|
return
|
|
}
|
|
|
|
guard let offset = snapshot.utf8Offset(of: completionPos) else {
|
|
log("invalid completion position \(req.params.position) (adjusted: \(completionPos)")
|
|
req.reply(CompletionList(isIncomplete: true, items: []))
|
|
return
|
|
}
|
|
|
|
let skreq = SKRequestDictionary(sourcekitd: sourcekitd)
|
|
skreq[keys.request] = requests.codecomplete
|
|
skreq[keys.offset] = offset
|
|
skreq[keys.sourcefile] = snapshot.document.url.path
|
|
skreq[keys.sourcetext] = snapshot.text
|
|
|
|
if let settings = buildSystem.settings(for: snapshot.document.url, snapshot.document.language) {
|
|
skreq[keys.compilerargs] = settings.compilerArguments
|
|
}
|
|
|
|
logAsync { _ in skreq.description }
|
|
|
|
let handle = sourcekitd.send(skreq) { [weak self] result in
|
|
guard let self = self else { return }
|
|
guard let dict = result.success else {
|
|
req.reply(.failure(result.failure!))
|
|
return
|
|
}
|
|
|
|
guard let completions: SKResponseArray = dict[self.keys.results] else {
|
|
req.reply(CompletionList(isIncomplete: false, items: []))
|
|
return
|
|
}
|
|
|
|
var result = CompletionList(isIncomplete: false, items: [])
|
|
|
|
let cancelled = !completions.forEach { (i, value) -> Bool in
|
|
guard let name: String = value[self.keys.description] else {
|
|
return true // continue
|
|
}
|
|
|
|
let filterName: String? = value[self.keys.name]
|
|
let insertText: String? = value[self.keys.sourcetext]
|
|
let typeName: String? = value[self.keys.typename]
|
|
|
|
let clientCompletionCapabilities = self.clientCapabilities.textDocument?.completion
|
|
let clientSupportsSnippets = clientCompletionCapabilities?.completionItem?.snippetSupport == true
|
|
let text = insertText.map {
|
|
self.rewriteSourceKitPlaceholders(inString: $0, clientSupportsSnippets: clientSupportsSnippets)
|
|
}
|
|
let isInsertTextSnippet = clientSupportsSnippets && text != insertText
|
|
|
|
let kind: sourcekitd_uid_t? = value[self.keys.kind]
|
|
result.items.append(CompletionItem(
|
|
label: name,
|
|
detail: typeName,
|
|
sortText: nil,
|
|
filterText: filterName,
|
|
textEdit: nil,
|
|
insertText: text,
|
|
insertTextFormat: isInsertTextSnippet ? .snippet : .plain,
|
|
kind: kind?.asCompletionItemKind(self.values) ?? .value,
|
|
deprecated: nil
|
|
))
|
|
|
|
return true
|
|
}
|
|
|
|
if !cancelled {
|
|
req.reply(result)
|
|
}
|
|
}
|
|
|
|
// FIXME: cancellation
|
|
_ = handle
|
|
}
|
|
|
|
func rewriteSourceKitPlaceholders(inString string: String, clientSupportsSnippets: Bool) -> String {
|
|
var result = string
|
|
var index = 1
|
|
while let start = result.range(of: EditorPlaceholder.placeholderPrefix) {
|
|
guard let end = result[start.upperBound...].range(of: EditorPlaceholder.placeholderSuffix) else {
|
|
log("invalid placeholder in \(string)", level: .debug)
|
|
return string
|
|
}
|
|
let rawPlaceholder = String(result[start.lowerBound..<end.upperBound])
|
|
guard let displayName = EditorPlaceholder(rawPlaceholder)?.displayName else {
|
|
log("failed to decode placeholder \(rawPlaceholder) in \(string)", level: .debug)
|
|
return string
|
|
}
|
|
let placeholder = clientSupportsSnippets ? "${\(index):\(displayName)}" : ""
|
|
result.replaceSubrange(start.lowerBound..<end.upperBound, with: placeholder)
|
|
index += 1
|
|
}
|
|
return result
|
|
}
|
|
|
|
/// Adjust completion position to the start of identifier characters.
|
|
func adjustCompletionLocation(_ pos: Position, in snapshot: DocumentSnapshot) -> Position? {
|
|
guard pos.line < snapshot.lineTable.count else {
|
|
// Line out of range.
|
|
return nil
|
|
}
|
|
let lineSlice = snapshot.lineTable[pos.line]
|
|
let startIndex = lineSlice.startIndex
|
|
|
|
let identifierChars = CharacterSet.alphanumerics.union(CharacterSet(charactersIn: "_"))
|
|
|
|
guard var loc = lineSlice.utf16.index(startIndex, offsetBy: pos.utf16index, limitedBy: lineSlice.endIndex) else {
|
|
// Column out of range.
|
|
return nil
|
|
}
|
|
while loc != startIndex {
|
|
let prev = lineSlice.index(before: loc)
|
|
if !identifierChars.contains(lineSlice.unicodeScalars[prev]) {
|
|
break
|
|
}
|
|
loc = prev
|
|
}
|
|
|
|
// ###aabccccccdddddd
|
|
// ^ ^- loc ^-requestedLoc
|
|
// `- startIndex
|
|
|
|
let adjustedOffset = lineSlice.utf16.distance(from: startIndex, to: loc)
|
|
return Position(line: pos.line, utf16index: adjustedOffset)
|
|
}
|
|
|
|
func hover(_ req: Request<HoverRequest>) {
|
|
let url = req.params.textDocument.url
|
|
let position = req.params.position
|
|
cursorInfo(url, position) { result in
|
|
guard let cursorInfo: CursorInfo = result.success ?? nil else {
|
|
if let error = result.failure {
|
|
log("cursor info failed \(url):\(position): \(error)", level: .warning)
|
|
}
|
|
return req.reply(nil)
|
|
}
|
|
|
|
guard let name: String = cursorInfo.symbolInfo.name else {
|
|
// There is a cursor but we don't know how to deal with it.
|
|
req.reply(nil)
|
|
return
|
|
}
|
|
|
|
/// Prepend backslash to `*` and `_`, to prevent them
|
|
/// from being interpreted as markdown.
|
|
func escapeNameMarkdown(_ str: String) -> String {
|
|
return String(str.flatMap({ ($0 == "*" || $0 == "_") ? ["\\", $0] : [$0] }))
|
|
}
|
|
|
|
var result = "# \(escapeNameMarkdown(name))"
|
|
if let doc = cursorInfo.documentationXML {
|
|
result += """
|
|
|
|
\(orLog { try xmlDocumentationToMarkdown(doc) } ?? doc)
|
|
"""
|
|
} else if let annotated: String = cursorInfo.annotatedDeclaration {
|
|
result += """
|
|
|
|
\(orLog { try xmlDocumentationToMarkdown(annotated) } ?? annotated)
|
|
"""
|
|
}
|
|
|
|
req.reply(HoverResponse(contents: MarkupContent(kind: .markdown, value: result), range: nil))
|
|
}
|
|
}
|
|
|
|
func symbolInfo(_ req: Request<SymbolInfoRequest>) {
|
|
let url = req.params.textDocument.url
|
|
let position = req.params.position
|
|
cursorInfo(url, position) { result in
|
|
guard let cursorInfo: CursorInfo = result.success ?? nil else {
|
|
if let error = result.failure {
|
|
log("cursor info failed \(url):\(position): \(error)", level: .warning)
|
|
}
|
|
return req.reply([])
|
|
}
|
|
|
|
req.reply([cursorInfo.symbolInfo])
|
|
}
|
|
}
|
|
|
|
func documentSymbol(_ req: Request<DocumentSymbolRequest>) {
|
|
guard let snapshot = documentManager.latestSnapshot(req.params.textDocument.url) else {
|
|
log("failed to find snapshot for url \(req.params.textDocument.url)")
|
|
req.reply(nil)
|
|
return
|
|
}
|
|
|
|
let skreq = SKRequestDictionary(sourcekitd: sourcekitd)
|
|
skreq[keys.request] = requests.editor_open
|
|
skreq[keys.name] = "DocumentSymbols:" + snapshot.document.url.path
|
|
skreq[keys.sourcetext] = snapshot.text
|
|
skreq[keys.syntactic_only] = 1
|
|
|
|
let handle = sourcekitd.send(skreq) { [weak self] result in
|
|
guard let self = self else { return }
|
|
guard let dict = result.success else {
|
|
req.reply(.failure(result.failure!))
|
|
return
|
|
}
|
|
guard let results: SKResponseArray = dict[self.keys.substructure] else {
|
|
return req.reply([])
|
|
}
|
|
|
|
func documentSymbol(value: SKResponseDictionary) -> DocumentSymbol? {
|
|
guard let name: String = value[self.keys.name],
|
|
let uid: sourcekitd_uid_t = value[self.keys.kind],
|
|
let kind: SymbolKind = uid.asSymbolKind(self.values),
|
|
let offset: Int = value[self.keys.offset],
|
|
let start: Position = snapshot.positionOf(utf8Offset: offset),
|
|
let length: Int = value[self.keys.length],
|
|
let end: Position = snapshot.positionOf(utf8Offset: offset + length) else {
|
|
return nil
|
|
}
|
|
|
|
let range = PositionRange(start..<end)
|
|
let selectionRange: PositionRange
|
|
if let nameOffset: Int = value[self.keys.nameoffset],
|
|
let nameStart: Position = snapshot.positionOf(utf8Offset: nameOffset),
|
|
let nameLength: Int = value[self.keys.namelength],
|
|
let nameEnd: Position = snapshot.positionOf(utf8Offset: nameOffset + nameLength) {
|
|
selectionRange = PositionRange(nameStart..<nameEnd)
|
|
} else {
|
|
selectionRange = range
|
|
}
|
|
|
|
let children: [DocumentSymbol]?
|
|
if let substructure: SKResponseArray = value[self.keys.substructure] {
|
|
children = documentSymbols(array: substructure)
|
|
} else {
|
|
children = nil
|
|
}
|
|
return DocumentSymbol(name: name,
|
|
detail: nil,
|
|
kind: kind,
|
|
deprecated: nil,
|
|
range: range,
|
|
selectionRange: selectionRange,
|
|
children: children)
|
|
}
|
|
|
|
func documentSymbols(array: SKResponseArray) -> [DocumentSymbol] {
|
|
var result: [DocumentSymbol] = []
|
|
array.forEach { (i: Int, value: SKResponseDictionary) in
|
|
if let documentSymbol = documentSymbol(value: value) {
|
|
result.append(documentSymbol)
|
|
} else if let substructure: SKResponseArray = value[self.keys.substructure] {
|
|
result += documentSymbols(array: substructure)
|
|
}
|
|
return true
|
|
}
|
|
return result
|
|
}
|
|
|
|
req.reply(documentSymbols(array: results))
|
|
}
|
|
// FIXME: cancellation
|
|
_ = handle
|
|
return
|
|
}
|
|
|
|
func documentColor(_ req: Request<DocumentColorRequest>) {
|
|
guard let snapshot = documentManager.latestSnapshot(req.params.textDocument.url) else {
|
|
log("failed to find snapshot for url \(req.params.textDocument.url)")
|
|
req.reply(nil)
|
|
return
|
|
}
|
|
|
|
let skreq = SKRequestDictionary(sourcekitd: sourcekitd)
|
|
skreq[keys.request] = requests.editor_open
|
|
skreq[keys.name] = "DocumentColor:" + snapshot.document.url.path
|
|
skreq[keys.sourcetext] = snapshot.text
|
|
skreq[keys.syntactic_only] = 1
|
|
|
|
let handle = sourcekitd.send(skreq) { [weak self] result in
|
|
guard let self = self else { return }
|
|
guard let dict = result.success else {
|
|
req.reply(.failure(result.failure!))
|
|
return
|
|
}
|
|
|
|
guard let results: SKResponseArray = dict[self.keys.substructure] else {
|
|
return req.reply([])
|
|
}
|
|
|
|
func colorInformation(dict: SKResponseDictionary) -> ColorInformation? {
|
|
guard let kind: sourcekitd_uid_t = dict[self.keys.kind],
|
|
kind == self.values.expr_object_literal,
|
|
let name: String = dict[self.keys.name],
|
|
name == "colorLiteral",
|
|
let offset: Int = dict[self.keys.offset],
|
|
let start: Position = snapshot.positionOf(utf8Offset: offset),
|
|
let length: Int = dict[self.keys.length],
|
|
let end: Position = snapshot.positionOf(utf8Offset: offset + length),
|
|
let substructure: SKResponseArray = dict[self.keys.substructure] else {
|
|
return nil
|
|
}
|
|
var red, green, blue, alpha: Double?
|
|
substructure.forEach{ (i: Int, value: SKResponseDictionary) in
|
|
guard let name: String = value[self.keys.name],
|
|
let bodyoffset: Int = value[self.keys.bodyoffset],
|
|
let bodylength: Int = value[self.keys.bodylength] else {
|
|
return true
|
|
}
|
|
let view = snapshot.text.utf8
|
|
let bodyStart = view.index(view.startIndex, offsetBy: bodyoffset)
|
|
let bodyEnd = view.index(view.startIndex, offsetBy: bodyoffset+bodylength)
|
|
let value = String(view[bodyStart..<bodyEnd]).flatMap(Double.init)
|
|
switch name {
|
|
case "red":
|
|
red = value
|
|
case "green":
|
|
green = value
|
|
case "blue":
|
|
blue = value
|
|
case "alpha":
|
|
alpha = value
|
|
default:
|
|
break
|
|
}
|
|
return true
|
|
}
|
|
if let red = red,
|
|
let green = green,
|
|
let blue = blue,
|
|
let alpha = alpha {
|
|
let color = Color(red: red, green: green, blue: blue, alpha: alpha)
|
|
return ColorInformation(range: start..<end, color: color)
|
|
} else {
|
|
return nil
|
|
}
|
|
}
|
|
|
|
func colorInformation(array: SKResponseArray) -> [ColorInformation] {
|
|
var result: [ColorInformation] = []
|
|
array.forEach { (i: Int, value: SKResponseDictionary) in
|
|
if let documentSymbol = colorInformation(dict: value) {
|
|
result.append(documentSymbol)
|
|
} else if let substructure: SKResponseArray = value[self.keys.substructure] {
|
|
result += colorInformation(array: substructure)
|
|
}
|
|
return true
|
|
}
|
|
return result
|
|
}
|
|
|
|
req.reply(colorInformation(array: results))
|
|
}
|
|
// FIXME: cancellation
|
|
_ = handle
|
|
}
|
|
|
|
func colorPresentation(_ req: Request<ColorPresentationRequest>) {
|
|
let color = req.params.color
|
|
// Empty string as a label breaks VSCode color picker
|
|
let label = "Color Literal"
|
|
let newText = "#colorLiteral(red: \(color.red), green: \(color.green), blue: \(color.blue), alpha: \(color.alpha))"
|
|
let textEdit = TextEdit(range: req.params.range.asRange, newText: newText)
|
|
let presentation = ColorPresentation(label: label, textEdit: textEdit, additionalTextEdits: nil)
|
|
req.reply([presentation])
|
|
}
|
|
|
|
func documentSymbolHighlight(_ req: Request<DocumentHighlightRequest>) {
|
|
|
|
guard let snapshot = documentManager.latestSnapshot(req.params.textDocument.url) else {
|
|
log("failed to find snapshot for url \(req.params.textDocument.url)")
|
|
req.reply(nil)
|
|
return
|
|
}
|
|
|
|
guard let offset = snapshot.utf8Offset(of: req.params.position) else {
|
|
log("invalid position \(req.params.position)")
|
|
req.reply(nil)
|
|
return
|
|
}
|
|
|
|
let skreq = SKRequestDictionary(sourcekitd: sourcekitd)
|
|
skreq[keys.request] = requests.relatedidents
|
|
skreq[keys.offset] = offset
|
|
skreq[keys.sourcefile] = snapshot.document.url.path
|
|
|
|
// FIXME: should come from the internal document
|
|
if let settings = buildSystem.settings(for: snapshot.document.url, snapshot.document.language) {
|
|
skreq[keys.compilerargs] = settings.compilerArguments
|
|
}
|
|
|
|
let handle = sourcekitd.send(skreq) { [weak self] result in
|
|
guard let self = self else { return }
|
|
guard let dict = result.success else {
|
|
req.reply(.failure(result.failure!))
|
|
return
|
|
}
|
|
|
|
guard let results: SKResponseArray = dict[self.keys.results] else {
|
|
return req.reply([])
|
|
}
|
|
|
|
var highlights: [DocumentHighlight] = []
|
|
|
|
results.forEach { _, value in
|
|
if let offset: Int = value[self.keys.offset],
|
|
let start: Position = snapshot.positionOf(utf8Offset: offset),
|
|
let length: Int = value[self.keys.length],
|
|
let end: Position = snapshot.positionOf(utf8Offset: offset + length)
|
|
{
|
|
highlights.append(DocumentHighlight(
|
|
range: start..<end,
|
|
kind: .read // unknown
|
|
))
|
|
}
|
|
return true
|
|
}
|
|
|
|
req.reply(highlights)
|
|
}
|
|
|
|
// FIXME: cancellation
|
|
_ = handle
|
|
}
|
|
|
|
func foldingRange(_ req: Request<FoldingRangeRequest>) {
|
|
guard let snapshot = documentManager.latestSnapshot(req.params.textDocument.url) else {
|
|
log("failed to find snapshot for url \(req.params.textDocument.url)")
|
|
req.reply(nil)
|
|
return
|
|
}
|
|
|
|
let skreq = SKRequestDictionary(sourcekitd: sourcekitd)
|
|
skreq[keys.request] = requests.editor_open
|
|
skreq[keys.name] = "FoldingRanges:" + snapshot.document.url.path
|
|
skreq[keys.sourcetext] = snapshot.text
|
|
skreq[keys.syntactic_only] = 1
|
|
|
|
let handle = sourcekitd.send(skreq) { [weak self] result in
|
|
guard let self = self else { return }
|
|
guard let dict = result.success else {
|
|
req.reply(.failure(result.failure!))
|
|
return
|
|
}
|
|
|
|
guard let syntaxMap: SKResponseArray = dict[self.keys.syntaxmap],
|
|
let substructure: SKResponseArray = dict[self.keys.substructure] else {
|
|
return req.reply([])
|
|
}
|
|
|
|
var ranges: [FoldingRange] = []
|
|
|
|
var hasReachedLimit: Bool {
|
|
let capabilities = self.clientCapabilities.textDocument?.foldingRange
|
|
guard let rangeLimit = capabilities?.rangeLimit else {
|
|
return false
|
|
}
|
|
return ranges.count >= rangeLimit
|
|
}
|
|
|
|
// If the limit is less than one, do nothing.
|
|
guard hasReachedLimit == false else {
|
|
req.reply([])
|
|
return
|
|
}
|
|
|
|
// Merge successive comments into one big comment by adding their lengths.
|
|
var currentComment: (offset: Int, length: Int)? = nil
|
|
|
|
syntaxMap.forEach { _, value in
|
|
if let kind: sourcekitd_uid_t = value[self.keys.kind],
|
|
kind.isCommentKind(self.values),
|
|
let offset: Int = value[self.keys.offset],
|
|
let length: Int = value[self.keys.length]
|
|
{
|
|
if let comment = currentComment, comment.offset + comment.length == offset {
|
|
currentComment!.length += length
|
|
return true
|
|
}
|
|
if let comment = currentComment {
|
|
self.addFoldingRange(offset: comment.offset, length: comment.length, kind: .comment, in: snapshot, toArray: &ranges)
|
|
}
|
|
currentComment = (offset: offset, length: length)
|
|
}
|
|
return hasReachedLimit == false
|
|
}
|
|
|
|
// Add the last stored comment.
|
|
if let comment = currentComment, hasReachedLimit == false {
|
|
self.addFoldingRange(offset: comment.offset, length: comment.length, kind: .comment, in: snapshot, toArray: &ranges)
|
|
currentComment = nil
|
|
}
|
|
|
|
var structureStack: [SKResponseArray] = [substructure]
|
|
while !hasReachedLimit, let substructure = structureStack.popLast() {
|
|
substructure.forEach { _, value in
|
|
if let offset: Int = value[self.keys.bodyoffset],
|
|
let length: Int = value[self.keys.bodylength],
|
|
length > 0
|
|
{
|
|
self.addFoldingRange(offset: offset, length: length, in: snapshot, toArray: &ranges)
|
|
if hasReachedLimit {
|
|
return false
|
|
}
|
|
}
|
|
if let substructure: SKResponseArray = value[self.keys.substructure] {
|
|
structureStack.append(substructure)
|
|
}
|
|
return true
|
|
}
|
|
}
|
|
|
|
ranges.sort()
|
|
req.reply(ranges)
|
|
}
|
|
|
|
// FIXME: cancellation
|
|
_ = handle
|
|
}
|
|
|
|
func addFoldingRange(offset: Int, length: Int, kind: FoldingRangeKind? = nil, in snapshot: DocumentSnapshot, toArray ranges: inout [FoldingRange]) {
|
|
guard let start: Position = snapshot.positionOf(utf8Offset: offset),
|
|
let end: Position = snapshot.positionOf(utf8Offset: offset + length) else {
|
|
log("folding range failed to retrieve position of \(snapshot.document.url): \(offset)-\(offset + length)", level: .warning)
|
|
return
|
|
}
|
|
let capabilities = clientCapabilities.textDocument?.foldingRange
|
|
let range: FoldingRange
|
|
// If the client only supports folding full lines, ignore the end character's line.
|
|
if capabilities?.lineFoldingOnly == true {
|
|
let lastLineToFold = end.line - 1
|
|
if lastLineToFold <= start.line {
|
|
return
|
|
} else {
|
|
range = FoldingRange(startLine: start.line,
|
|
startUTF16Index: nil,
|
|
endLine: lastLineToFold,
|
|
endUTF16Index: nil,
|
|
kind: kind)
|
|
}
|
|
} else {
|
|
range = FoldingRange(startLine: start.line,
|
|
startUTF16Index: start.utf16index,
|
|
endLine: end.line,
|
|
endUTF16Index: end.utf16index,
|
|
kind: kind)
|
|
}
|
|
ranges.append(range)
|
|
}
|
|
|
|
func codeAction(_ req: Request<CodeActionRequest>) {
|
|
let providersAndKinds: [(provider: CodeActionProvider, kind: CodeActionKind)] = [
|
|
//TODO: Implement the providers.
|
|
//(retrieveRefactorCodeActions, .refactor),
|
|
//(retrieveQuickFixCodeActions, .quickFix)
|
|
]
|
|
let wantedActionKinds = req.params.context.only
|
|
let providers = providersAndKinds.filter { wantedActionKinds?.contains($0.1) != false }
|
|
retrieveCodeActions(req, providers: providers.map { $0.provider }) { codeActions in
|
|
let capabilities = self.clientCapabilities.textDocument?.codeAction
|
|
let response = CodeActionRequestResponse(codeActions: codeActions,
|
|
clientCapabilities: capabilities)
|
|
req.reply(response)
|
|
}
|
|
}
|
|
|
|
func retrieveCodeActions(_ req: Request<CodeActionRequest>, providers: [CodeActionProvider], completion: @escaping CodeActionProviderCompletion) {
|
|
guard providers.isEmpty == false else {
|
|
completion([])
|
|
return
|
|
}
|
|
var codeActions = [CodeAction]()
|
|
let dispatchGroup = DispatchGroup()
|
|
(0..<providers.count).forEach { _ in dispatchGroup.enter() }
|
|
dispatchGroup.notify(queue: queue) {
|
|
completion(codeActions)
|
|
}
|
|
for i in 0..<providers.count {
|
|
providers[i](req.params) { actions in
|
|
self.queue.sync {
|
|
codeActions += actions
|
|
}
|
|
dispatchGroup.leave()
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
extension DocumentSnapshot {
|
|
|
|
func utf8Offset(of pos: Position) -> Int? {
|
|
return lineTable.utf8OffsetOf(line: pos.line, utf16Column: pos.utf16index)
|
|
}
|
|
|
|
func positionOf(utf8Offset: Int) -> Position? {
|
|
return lineTable.lineAndUTF16ColumnOf(utf8Offset: utf8Offset).map {
|
|
Position(line: $0.line, utf16index: $0.utf16Column)
|
|
}
|
|
}
|
|
|
|
func positionOf(zeroBasedLine: Int, utf8Column: Int) -> Position? {
|
|
return lineTable.utf16ColumnAt(line: zeroBasedLine, utf8Column: utf8Column).map {
|
|
Position(line: zeroBasedLine, utf16index: $0)
|
|
}
|
|
}
|
|
}
|
|
|
|
func makeLocalSwiftServer(client: MessageHandler, sourcekitd: AbsolutePath, buildSettings: BuildSystem?, clientCapabilities: ClientCapabilities?) throws -> Connection {
|
|
|
|
let connectionToSK = LocalConnection()
|
|
let connectionToClient = LocalConnection()
|
|
|
|
let server = try SwiftLanguageServer(
|
|
client: connectionToClient,
|
|
sourcekitd: sourcekitd,
|
|
buildSystem: buildSettings ?? BuildSystemList(),
|
|
clientCapabilities: clientCapabilities ?? ClientCapabilities(workspace: nil, textDocument: nil)
|
|
)
|
|
|
|
connectionToSK.start(handler: server)
|
|
connectionToClient.start(handler: client)
|
|
|
|
return connectionToSK
|
|
}
|
|
|
|
extension sourcekitd_uid_t {
|
|
func isCommentKind(_ vals: sourcekitd_values) -> Bool {
|
|
switch self {
|
|
case vals.syntaxtype_comment, vals.syntaxtype_comment_marker, vals.syntaxtype_comment_url:
|
|
return true
|
|
default:
|
|
return isDocCommentKind(vals)
|
|
}
|
|
}
|
|
|
|
func isDocCommentKind(_ vals: sourcekitd_values) -> Bool {
|
|
return self == vals.syntaxtype_doccomment || self == vals.syntaxtype_doccomment_field
|
|
}
|
|
|
|
func asCompletionItemKind(_ vals: sourcekitd_values) -> CompletionItemKind? {
|
|
switch self {
|
|
case vals.kind_keyword:
|
|
return .keyword
|
|
case vals.decl_module:
|
|
return .module
|
|
case vals.decl_class:
|
|
return .class
|
|
case vals.decl_struct:
|
|
return .struct
|
|
case vals.decl_enum:
|
|
return .enum
|
|
case vals.decl_enumelement:
|
|
return .enumMember
|
|
case vals.decl_protocol:
|
|
return .interface
|
|
case vals.decl_associatedtype:
|
|
return .typeParameter
|
|
case vals.decl_typealias:
|
|
return .typeParameter // FIXME: is there a better choice?
|
|
case vals.decl_generic_type_param:
|
|
return .typeParameter
|
|
case vals.decl_function_constructor:
|
|
return .constructor
|
|
case vals.decl_function_destructor:
|
|
return .value // FIXME: is there a better choice?
|
|
case vals.decl_function_subscript:
|
|
return .method // FIXME: is there a better choice?
|
|
case vals.decl_function_method_static:
|
|
return .method
|
|
case vals.decl_function_method_instance:
|
|
return .method
|
|
case vals.decl_function_operator_prefix,
|
|
vals.decl_function_operator_postfix,
|
|
vals.decl_function_operator_infix:
|
|
return .operator
|
|
case vals.decl_precedencegroup:
|
|
return .value
|
|
case vals.decl_function_free:
|
|
return .function
|
|
case vals.decl_var_static, vals.decl_var_class:
|
|
return .property
|
|
case vals.decl_var_instance:
|
|
return .property
|
|
case vals.decl_var_local,
|
|
vals.decl_var_global,
|
|
vals.decl_var_parameter:
|
|
return .variable
|
|
default:
|
|
return nil
|
|
}
|
|
}
|
|
|
|
func asSymbolKind(_ vals: sourcekitd_values) -> SymbolKind? {
|
|
switch self {
|
|
case vals.decl_class:
|
|
return .class
|
|
case vals.decl_function_method_instance,
|
|
vals.decl_function_method_static,
|
|
vals.decl_function_method_class:
|
|
return .method
|
|
case vals.decl_var_instance,
|
|
vals.decl_var_static,
|
|
vals.decl_var_class:
|
|
return .property
|
|
case vals.decl_enum:
|
|
return .enum
|
|
case vals.decl_enumelement:
|
|
return .enumMember
|
|
case vals.decl_protocol:
|
|
return .interface
|
|
case vals.decl_function_free:
|
|
return .function
|
|
case vals.decl_var_global,
|
|
vals.decl_var_local:
|
|
return .variable
|
|
case vals.decl_struct:
|
|
return .struct
|
|
case vals.decl_generic_type_param:
|
|
return .typeParameter
|
|
case vals.decl_extension:
|
|
// There are no extensions in LSP, so I return something vaguely similar
|
|
return .namespace
|
|
default:
|
|
return nil
|
|
}
|
|
}
|
|
}
|