diff --git a/Sources/SourceKitLSP/Swift/CodeCompletion.swift b/Sources/SourceKitLSP/Swift/CodeCompletion.swift index 0aa51d80..d079d0ac 100644 --- a/Sources/SourceKitLSP/Swift/CodeCompletion.swift +++ b/Sources/SourceKitLSP/Swift/CodeCompletion.swift @@ -37,22 +37,6 @@ extension SwiftLanguageServer { let options = req.sourcekitlspOptions ?? serverOptions.completionOptions - return try await completionWithServerFiltering( - offset: offset, - completionPos: completionPos, - snapshot: snapshot, - request: req, - options: options - ) - } - - private func completionWithServerFiltering( - offset: Int, - completionPos: Position, - snapshot: DocumentSnapshot, - request req: CompletionRequest, - options: SKCompletionOptions - ) async throws -> CompletionList { guard let start = snapshot.indexOf(utf8Offset: offset), let end = snapshot.index(of: req.position) else { @@ -62,42 +46,20 @@ extension SwiftLanguageServer { let filterText = String(snapshot.text[start.. CompletionList { + let task = completionQueue.asyncThrowing { + if let session = completionSessions[ObjectIdentifier(sourcekitd)], session.state == .open { + let isCompatible = session.snapshot.uri == snapshot.uri && + session.utf8StartOffset == completionUtf8Offset && + session.position == completionPosition && + session.compileCommand == compileCommand && + session.clientSupportsSnippets == clientSupportsSnippets + + if isCompatible { + return try await session.update(filterText: filterText, position: cursorPosition, in: snapshot, options: options) + } + + if mustReuse { + logger.error( + """ + triggerFromIncompleteCompletions with incompatible completion session; expected \ + \(session.uri.forLogging)@\(session.utf8StartOffset), \ + but got \(snapshot.uri.forLogging)@\(completionUtf8Offset) + """ + ) + throw ResponseError.serverCancelled + } + // The sessions aren't compatible. Close the existing session and open + // a new one below. + session.close() + } + if mustReuse { + logger.error("triggerFromIncompleteCompletions with no existing completion session") + throw ResponseError.serverCancelled + } + let session = CodeCompletionSession( + sourcekitd: sourcekitd, + snapshot: snapshot, + utf8Offset: completionUtf8Offset, + position: completionPosition, + compileCommand: compileCommand, + clientSupportsSnippets: clientSupportsSnippets + ) + completionSessions[ObjectIdentifier(sourcekitd)] = session + return try await session.open(filterText: filterText, position: cursorPosition, in: snapshot, options: options) + } + + // FIXME: (async) Use valuePropagatingCancellation once we support cancellation + return try await task.value + } + + // MARK: - Implementation + private let sourcekitd: any SourceKitD private let snapshot: DocumentSnapshot - let utf8StartOffset: Int + private let utf8StartOffset: Int private let position: Position private let compileCommand: SwiftCompileCommand? private let clientSupportsSnippets: Bool @@ -39,10 +157,10 @@ actor CodeCompletionSession { case open } - nonisolated var uri: DocumentURI { snapshot.uri } - nonisolated var keys: sourcekitd_keys { return sourcekitd.keys } + private nonisolated var uri: DocumentURI { snapshot.uri } + private nonisolated var keys: sourcekitd_keys { return sourcekitd.keys } - init( + private init( sourcekitd: any SourceKitD, snapshot: DocumentSnapshot, utf8Offset: Int, @@ -58,30 +176,6 @@ actor CodeCompletionSession { self.clientSupportsSnippets = clientSupportsSnippets } - /// 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. - func update( - filterText: String, - position: Position, - in snapshot: DocumentSnapshot, - options: SKCompletionOptions - ) async throws -> CompletionList { - switch self.state { - case .closed: - self.state = .open - return try await self.open(filterText: filterText, position: position, in: snapshot, options: options) - case .open: - return try await self.updateImpl(filterText: filterText, position: position, in: snapshot, options: options) - } - } - private func open( filterText: String, position: Position, @@ -105,6 +199,7 @@ actor CodeCompletionSession { } let dict = try await sourcekitd.send(req) + self.state = .open guard let completions: SKDResponseArray = dict[keys.results] else { return CompletionList(isIncomplete: false, items: []) @@ -121,7 +216,7 @@ actor CodeCompletionSession { ) } - private func updateImpl( + private func update( filterText: String, position: Position, in snapshot: DocumentSnapshot, @@ -170,22 +265,18 @@ actor CodeCompletionSession { return dict } - private func sendClose() { - let req = SKDRequestDictionary(sourcekitd: sourcekitd) - req[keys.request] = sourcekitd.requests.codecomplete_close - req[keys.offset] = self.utf8StartOffset - req[keys.name] = self.snapshot.uri.pseudoPath - logger.info("Closing code completion session: \(self, privacy: .private)") - _ = try? sourcekitd.sendSync(req) - } - - func close() async { + private func close() { switch self.state { case .closed: // Already closed, nothing to do. break case .open: - self.sendClose() + let req = SKDRequestDictionary(sourcekitd: sourcekitd) + req[keys.request] = sourcekitd.requests.codecomplete_close + req[keys.offset] = self.utf8StartOffset + req[keys.name] = self.snapshot.uri.pseudoPath + logger.info("Closing code completion session: \(self, privacy: .private)") + _ = try? sourcekitd.sendSync(req) self.state = .closed } } diff --git a/Sources/SourceKitLSP/Swift/SwiftLanguageServer.swift b/Sources/SourceKitLSP/Swift/SwiftLanguageServer.swift index 8e9decc0..8ff07474 100644 --- a/Sources/SourceKitLSP/Swift/SwiftLanguageServer.swift +++ b/Sources/SourceKitLSP/Swift/SwiftLanguageServer.swift @@ -108,8 +108,6 @@ public actor SwiftLanguageServer: ToolchainLanguageServer { var currentDiagnostics: [DocumentURI: [CachedDiagnostic]] = [:] - var currentCompletionSession: CodeCompletionSession? = nil - let syntaxTreeManager = SyntaxTreeManager() let semanticTokensManager = SemanticTokensManager() @@ -393,10 +391,6 @@ extension SwiftLanguageServer { } public func shutdown() async { - if let session = self.currentCompletionSession { - await session.close() - self.currentCompletionSession = nil - } self.sourcekitd.removeNotificationHandler(self) }