mirror of
https://github.com/apple/sourcekit-lsp.git
synced 2026-03-02 18:23:24 +01:00
Rather than asserting, raise an error for the user and abort the operation. This is motivated by the desire to get the test suite mostly working on Windows so that the underlying issues can be worked through more easily.
254 lines
8.9 KiB
Swift
254 lines
8.9 KiB
Swift
//===----------------------------------------------------------------------===//
|
|
//
|
|
// This source file is part of the Swift.org open source project
|
|
//
|
|
// Copyright (c) 2014 - 2020 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 LSPLogging
|
|
import SourceKitD
|
|
import Dispatch
|
|
|
|
/// Represents a code-completion session for a given source location that can be efficiently
|
|
/// re-filtered by calling `update()`.
|
|
///
|
|
/// The first call to `update()` opens the session with sourcekitd, which computes the initial
|
|
/// completions. Subsequent calls to `update()` will re-filter the original completions. Finally,
|
|
/// before creating a new completion session, you must call `close()`. It is an error to create a
|
|
/// new completion session with the same source location before closing the original session.
|
|
///
|
|
/// At the sourcekitd level, this uses `codecomplete.open`, `codecomplete.update` and
|
|
/// `codecomplete.close` requests.
|
|
class CodeCompletionSession {
|
|
unowned let server: SwiftLanguageServer
|
|
let queue: DispatchQueue
|
|
let snapshot: DocumentSnapshot
|
|
let utf8StartOffset: Int
|
|
let position: Position
|
|
let compileCommand: SwiftCompileCommand?
|
|
var state: State = .closed
|
|
|
|
enum State {
|
|
case closed
|
|
// FIXME: we should keep a real queue and cancel previous updates.
|
|
case opening(DispatchGroup)
|
|
case open
|
|
}
|
|
|
|
var uri: DocumentURI { snapshot.document.uri }
|
|
|
|
init(
|
|
server: SwiftLanguageServer,
|
|
snapshot: DocumentSnapshot,
|
|
utf8Offset: Int,
|
|
position: Position,
|
|
compileCommand: SwiftCompileCommand?)
|
|
{
|
|
self.server = server
|
|
self.queue =
|
|
DispatchQueue(label: "\(Self.self)-queue", qos: .userInitiated, target: server.queue)
|
|
self.snapshot = snapshot
|
|
self.utf8StartOffset = utf8Offset
|
|
self.position = position
|
|
self.compileCommand = compileCommand
|
|
}
|
|
|
|
/// Retrieve completions for the given `filterText`, opening or updating the session.
|
|
///
|
|
/// - parameters:
|
|
/// - filterText: The text to use for fuzzy matching the results.
|
|
/// - position: The position at the end of the existing text (typically right after the end of
|
|
/// `filterText`), which determines the end of the `TextEdit` replacement range
|
|
/// in the resulting completions.
|
|
/// - snapshot: The current snapshot that the `TextEdit` replacement in results will be in.
|
|
/// - options: The completion options, such as the maximum number of results.
|
|
/// - completion: Asynchronous callback to receive results or error response.
|
|
func update(
|
|
filterText: String,
|
|
position: Position,
|
|
in snapshot: DocumentSnapshot,
|
|
options: SKCompletionOptions,
|
|
completion: @escaping (LSPResult<CompletionList>) -> Void)
|
|
{
|
|
queue.async {
|
|
switch self.state {
|
|
case .closed:
|
|
self._open(filterText: filterText, position: position, in: snapshot, options: options, completion: completion)
|
|
case .opening(let group):
|
|
group.notify(queue: self.queue) {
|
|
switch self.state {
|
|
case .closed, .opening(_):
|
|
// Don't try again.
|
|
completion(.failure(.cancelled))
|
|
case .open:
|
|
self._update(filterText: filterText, position: position, in: snapshot, options: options, completion: completion)
|
|
}
|
|
}
|
|
case .open:
|
|
self._update(filterText: filterText, position: position, in: snapshot, options: options, completion: completion)
|
|
}
|
|
}
|
|
}
|
|
|
|
func _open(
|
|
filterText: String,
|
|
position: Position,
|
|
in snapshot: DocumentSnapshot,
|
|
options: SKCompletionOptions,
|
|
completion: @escaping (LSPResult<CompletionList>) -> Void)
|
|
{
|
|
log("\(Self.self) Open: \(self) filter=\(filterText)")
|
|
guard snapshot.version == self.snapshot.version else {
|
|
completion(.failure(ResponseError(code: .invalidRequest, message: "open must use the original snapshot")))
|
|
return
|
|
}
|
|
|
|
let req = SKDRequestDictionary(sourcekitd: server.sourcekitd)
|
|
let keys = server.sourcekitd.keys
|
|
req[keys.request] = server.sourcekitd.requests.codecomplete_open
|
|
req[keys.offset] = utf8StartOffset
|
|
req[keys.name] = uri.pseudoPath
|
|
req[keys.sourcefile] = uri.pseudoPath
|
|
req[keys.sourcetext] = snapshot.text
|
|
req[keys.codecomplete_options] = optionsDictionary(filterText: filterText, options: options)
|
|
if let compileCommand = compileCommand {
|
|
req[keys.compilerargs] = compileCommand.compilerArgs
|
|
}
|
|
|
|
let group = DispatchGroup()
|
|
group.enter()
|
|
|
|
state = .opening(group)
|
|
|
|
let handle = server.sourcekitd.send(req, queue) { result in
|
|
defer { group.leave() }
|
|
|
|
guard let dict = result.success else {
|
|
self.state = .closed
|
|
return completion(.failure(ResponseError(result.failure!)))
|
|
}
|
|
if case .closed = self.state {
|
|
return completion(.failure(.cancelled))
|
|
}
|
|
|
|
self.state = .open
|
|
|
|
guard let completions: SKDResponseArray = dict[keys.results] else {
|
|
return completion(.success(CompletionList(isIncomplete: false, items: [])))
|
|
}
|
|
|
|
let results = self.server.completionsFromSKDResponse(completions,
|
|
in: snapshot,
|
|
completionPos: self.position,
|
|
requestPosition: position,
|
|
isIncomplete: true)
|
|
completion(.success(results))
|
|
}
|
|
|
|
// FIXME: cancellation
|
|
_ = handle
|
|
}
|
|
|
|
func _update(
|
|
filterText: String,
|
|
position: Position,
|
|
in snapshot: DocumentSnapshot,
|
|
options: SKCompletionOptions,
|
|
completion: @escaping (LSPResult<CompletionList>) -> Void)
|
|
{
|
|
// FIXME: Assertion for prefix of snapshot matching what we started with.
|
|
|
|
log("\(Self.self) Update: \(self) filter=\(filterText)")
|
|
let req = SKDRequestDictionary(sourcekitd: server.sourcekitd)
|
|
let keys = server.sourcekitd.keys
|
|
req[keys.request] = server.sourcekitd.requests.codecomplete_update
|
|
req[keys.offset] = utf8StartOffset
|
|
req[keys.name] = uri.pseudoPath
|
|
req[keys.codecomplete_options] = optionsDictionary(filterText: filterText, options: options)
|
|
|
|
let handle = server.sourcekitd.send(req, queue) { result in
|
|
guard let dict = result.success else {
|
|
return completion(.failure(ResponseError(result.failure!)))
|
|
}
|
|
guard let completions: SKDResponseArray = dict[keys.results] else {
|
|
return completion(.success(CompletionList(isIncomplete: false, items: [])))
|
|
}
|
|
|
|
completion(.success(self.server.completionsFromSKDResponse(completions, in: snapshot, completionPos: self.position, requestPosition: position, isIncomplete: true)))
|
|
}
|
|
|
|
// FIXME: cancellation
|
|
_ = handle
|
|
}
|
|
|
|
private func optionsDictionary(
|
|
filterText: String,
|
|
options: SKCompletionOptions
|
|
) -> SKDRequestDictionary {
|
|
let dict = SKDRequestDictionary(sourcekitd: server.sourcekitd)
|
|
let keys = server.sourcekitd.keys
|
|
// Sorting and priority options.
|
|
dict[keys.codecomplete_hideunderscores] = 0
|
|
dict[keys.codecomplete_hidelowpriority] = 0
|
|
dict[keys.codecomplete_hidebyname] = 0
|
|
dict[keys.codecomplete_addinneroperators] = 0
|
|
dict[keys.codecomplete_callpatternheuristics] = 0
|
|
dict[keys.codecomplete_showtopnonliteralresults] = 0
|
|
// Filtering options.
|
|
dict[keys.codecomplete_filtertext] = filterText
|
|
if let maxResults = options.maxResults {
|
|
dict[keys.codecomplete_requestlimit] = maxResults
|
|
}
|
|
return dict
|
|
}
|
|
|
|
private func _sendClose(_ server: SwiftLanguageServer) {
|
|
let req = SKDRequestDictionary(sourcekitd: server.sourcekitd)
|
|
let keys = server.sourcekitd.keys
|
|
req[keys.request] = server.sourcekitd.requests.codecomplete_close
|
|
req[keys.offset] = self.utf8StartOffset
|
|
req[keys.name] = self.snapshot.document.uri.pseudoPath
|
|
log("\(Self.self) Closing: \(self)")
|
|
_ = try? server.sourcekitd.sendSync(req)
|
|
}
|
|
|
|
func close() {
|
|
// Temporary back-reference to server to keep it alive during close().
|
|
let server = self.server
|
|
|
|
queue.async {
|
|
switch self.state {
|
|
case .closed:
|
|
// Already closed, nothing to do.
|
|
break
|
|
case .opening(let group):
|
|
group.notify(queue: self.queue) {
|
|
switch self.state {
|
|
case .closed, .opening(_):
|
|
// Don't try again.
|
|
break
|
|
case .open:
|
|
self._sendClose(server)
|
|
self.state = .closed
|
|
}
|
|
}
|
|
case .open:
|
|
self._sendClose(server)
|
|
self.state = .closed
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
extension CodeCompletionSession: CustomStringConvertible {
|
|
var description: String {
|
|
"\(uri.pseudoPath):\(position)"
|
|
}
|
|
}
|