From 84e89761e4b4b5fb8d02a1139682715fbc236c01 Mon Sep 17 00:00:00 2001 From: Khan Winter <35942988+thecoolwinter@users.noreply.github.com> Date: Sun, 15 Dec 2024 17:05:48 -0600 Subject: [PATCH 01/16] Introduce LSP Token Map, Decouple CodeFileDocument From LSP Service --- CodeEdit.xcodeproj/project.pbxproj | 34 +++++-- .../CodeFileDocument/CodeFileDocument.swift | 14 ++- .../CodeEditDocumentController.swift | 7 -- .../Features/Editor/Views/CodeFileView.swift | 29 +++++- .../LSPContentCoordinator.swift | 13 +-- .../LSP/Editor/SemanticTokenMap.swift | 69 ++++++++++++++ .../LanguageServer+DocumentSync.swift | 9 +- .../LanguageServer+Notifications.swift | 34 +++++++ .../LSP/LanguageServer/LanguageServer.swift | 35 ++++++-- ...Map.swift => LanguageServerFileData.swift} | 39 ++++++-- .../Features/LSP/Service/LSPService.swift | 2 +- .../TextView/TextView+LSPRange.swift | 7 ++ .../LSP/BufferingServerConnection.swift | 11 +++ .../LSP/LanguageServer+DocumentTests.swift | 89 +++++++++++-------- .../Features/LSP/SemanticTokenMapTests.swift | 13 +++ 15 files changed, 313 insertions(+), 92 deletions(-) rename CodeEdit/Features/LSP/{LanguageServer => Editor}/LSPContentCoordinator.swift (87%) create mode 100644 CodeEdit/Features/LSP/Editor/SemanticTokenMap.swift create mode 100644 CodeEdit/Features/LSP/LanguageServer/LanguageServer+Notifications.swift rename CodeEdit/Features/LSP/LanguageServer/{LanguageServerFileMap.swift => LanguageServerFileData.swift} (54%) create mode 100644 CodeEditTests/Features/LSP/SemanticTokenMapTests.swift diff --git a/CodeEdit.xcodeproj/project.pbxproj b/CodeEdit.xcodeproj/project.pbxproj index cfd3741805..b6aa04e855 100644 --- a/CodeEdit.xcodeproj/project.pbxproj +++ b/CodeEdit.xcodeproj/project.pbxproj @@ -382,6 +382,9 @@ 6C2C155A29B4F4CC00EA60A5 /* Variadic.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C2C155929B4F4CC00EA60A5 /* Variadic.swift */; }; 6C2C155D29B4F4E500EA60A5 /* SplitViewReader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C2C155C29B4F4E500EA60A5 /* SplitViewReader.swift */; }; 6C2C156129B4F52F00EA60A5 /* SplitViewModifiers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C2C156029B4F52F00EA60A5 /* SplitViewModifiers.swift */; }; + 6C3B4CD12D0E2C2900C6759E /* SemanticTokenMap.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C3B4CD02D0E2C2900C6759E /* SemanticTokenMap.swift */; }; + 6C3B4CD42D0E2CB000C6759E /* SemanticTokenMapTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C3B4CD32D0E2CB000C6759E /* SemanticTokenMapTests.swift */; }; + 6C3B4CD82D0E39A900C6759E /* LanguageServer+Notifications.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C3B4CD72D0E39A900C6759E /* LanguageServer+Notifications.swift */; }; 6C3E12D32CC830D700DD12F1 /* RecentProjectsStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C3E12D22CC830D700DD12F1 /* RecentProjectsStore.swift */; }; 6C3E12D62CC8388000DD12F1 /* URL+componentCompare.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C3E12D52CC8388000DD12F1 /* URL+componentCompare.swift */; }; 6C3E12D82CC83CB600DD12F1 /* RecentProjectsMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C3E12D72CC83CB600DD12F1 /* RecentProjectsMenu.swift */; }; @@ -461,7 +464,7 @@ 6CC9E4B229B5669900C97388 /* Environment+ActiveEditor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6CC9E4B129B5669900C97388 /* Environment+ActiveEditor.swift */; }; 6CD0358A2C3461160091E1F4 /* KeyWindowControllerObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6CD035892C3461160091E1F4 /* KeyWindowControllerObserver.swift */; }; 6CD03B6A29FC773F001BD1D0 /* SettingsInjector.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6CD03B6929FC773F001BD1D0 /* SettingsInjector.swift */; }; - 6CD26C6E2C8EA1E600ADBA38 /* LanguageServerFileMap.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6CD26C6D2C8EA1E600ADBA38 /* LanguageServerFileMap.swift */; }; + 6CD26C6E2C8EA1E600ADBA38 /* LanguageServerFileData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6CD26C6D2C8EA1E600ADBA38 /* LanguageServerFileData.swift */; }; 6CD26C772C8EA83900ADBA38 /* URL+LanguageServer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6CD26C762C8EA83900ADBA38 /* URL+LanguageServer.swift */; }; 6CD26C7A2C8EA8A500ADBA38 /* LSPCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6CD26C782C8EA8A500ADBA38 /* LSPCache.swift */; }; 6CD26C7B2C8EA8A500ADBA38 /* LSPCache+Data.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6CD26C792C8EA8A500ADBA38 /* LSPCache+Data.swift */; }; @@ -1064,11 +1067,14 @@ 6C1CC99A2B1E7CBC0002349B /* FindNavigatorIndexBar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FindNavigatorIndexBar.swift; sourceTree = ""; }; 6C1F3DA12C18C55800F6DEF6 /* ShellIntegrationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShellIntegrationTests.swift; sourceTree = ""; }; 6C23842E2C796B4C003FBDD4 /* GitChangedFileLabel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GitChangedFileLabel.swift; sourceTree = ""; }; - 6C278CC62C93971F0066F6D9 /* LSPContentCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LSPContentCoordinator.swift; sourceTree = ""; }; + 6C278CC62C93971F0066F6D9 /* LSPContentCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LSPContentCoordinator.swift; sourceTree = ""; wrapsLines = 0; }; 6C2C155729B4F49100EA60A5 /* SplitViewItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SplitViewItem.swift; sourceTree = ""; }; 6C2C155929B4F4CC00EA60A5 /* Variadic.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Variadic.swift; sourceTree = ""; }; 6C2C155C29B4F4E500EA60A5 /* SplitViewReader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SplitViewReader.swift; sourceTree = ""; }; 6C2C156029B4F52F00EA60A5 /* SplitViewModifiers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SplitViewModifiers.swift; sourceTree = ""; }; + 6C3B4CD02D0E2C2900C6759E /* SemanticTokenMap.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SemanticTokenMap.swift; sourceTree = ""; }; + 6C3B4CD32D0E2CB000C6759E /* SemanticTokenMapTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SemanticTokenMapTests.swift; sourceTree = ""; }; + 6C3B4CD72D0E39A900C6759E /* LanguageServer+Notifications.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "LanguageServer+Notifications.swift"; sourceTree = ""; }; 6C3E12D22CC830D700DD12F1 /* RecentProjectsStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecentProjectsStore.swift; sourceTree = ""; }; 6C3E12D52CC8388000DD12F1 /* URL+componentCompare.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "URL+componentCompare.swift"; sourceTree = ""; }; 6C3E12D72CC83CB600DD12F1 /* RecentProjectsMenu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecentProjectsMenu.swift; sourceTree = ""; }; @@ -1133,7 +1139,7 @@ 6CC9E4B129B5669900C97388 /* Environment+ActiveEditor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Environment+ActiveEditor.swift"; sourceTree = ""; }; 6CD035892C3461160091E1F4 /* KeyWindowControllerObserver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyWindowControllerObserver.swift; sourceTree = ""; }; 6CD03B6929FC773F001BD1D0 /* SettingsInjector.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsInjector.swift; sourceTree = ""; }; - 6CD26C6D2C8EA1E600ADBA38 /* LanguageServerFileMap.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LanguageServerFileMap.swift; sourceTree = ""; }; + 6CD26C6D2C8EA1E600ADBA38 /* LanguageServerFileData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LanguageServerFileData.swift; sourceTree = ""; }; 6CD26C762C8EA83900ADBA38 /* URL+LanguageServer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "URL+LanguageServer.swift"; sourceTree = ""; }; 6CD26C782C8EA8A500ADBA38 /* LSPCache.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LSPCache.swift; sourceTree = ""; }; 6CD26C792C8EA8A500ADBA38 /* LSPCache+Data.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "LSPCache+Data.swift"; sourceTree = ""; }; @@ -1587,6 +1593,7 @@ 30B087FB2C0D53080063A882 /* LSP */ = { isa = PBXGroup; children = ( + 6C3B4CD22D0E2C5400C6759E /* Editor */, 6CD26C732C8EA71F00ADBA38 /* LanguageServer */, 6CD26C742C8EA79100ADBA38 /* Service */, 30B087FA2C0D53080063A882 /* LSPUtil.swift */, @@ -2889,6 +2896,15 @@ path = ChangedFile; sourceTree = ""; }; + 6C3B4CD22D0E2C5400C6759E /* Editor */ = { + isa = PBXGroup; + children = ( + 6C278CC62C93971F0066F6D9 /* LSPContentCoordinator.swift */, + 6C3B4CD02D0E2C2900C6759E /* SemanticTokenMap.swift */, + ); + path = Editor; + sourceTree = ""; + }; 6C3E12D42CC830DE00DD12F1 /* Model */ = { isa = PBXGroup; children = ( @@ -3069,10 +3085,10 @@ isa = PBXGroup; children = ( 30B087F72C0D53080063A882 /* LanguageServer.swift */, - 6CD26C6D2C8EA1E600ADBA38 /* LanguageServerFileMap.swift */, + 6C3B4CD72D0E39A900C6759E /* LanguageServer+Notifications.swift */, + 6CD26C6D2C8EA1E600ADBA38 /* LanguageServerFileData.swift */, 6CD26C782C8EA8A500ADBA38 /* LSPCache.swift */, 6CD26C792C8EA8A500ADBA38 /* LSPCache+Data.swift */, - 6C278CC62C93971F0066F6D9 /* LSPContentCoordinator.swift */, 30B0881E2C12626B0063A882 /* Capabilities */, ); path = LanguageServer; @@ -3100,8 +3116,9 @@ 6CD26C882C8F91B600ADBA38 /* LSP */ = { isa = PBXGroup; children = ( - 6CD26C892C8F91ED00ADBA38 /* LanguageServer+DocumentTests.swift */, 6C7D6D452C9092EC00B69EE0 /* BufferingServerConnection.swift */, + 6CD26C892C8F91ED00ADBA38 /* LanguageServer+DocumentTests.swift */, + 6C3B4CD32D0E2CB000C6759E /* SemanticTokenMapTests.swift */, ); path = LSP; sourceTree = ""; @@ -3989,9 +4006,10 @@ 3000516C2BBD3A9500A98562 /* ServiceWrapper.swift in Sources */, 587B9DA029300ABD00AC7927 /* PanelDivider.swift in Sources */, 201169E52837B40300F92B46 /* SourceControlNavigatorRepositoryView.swift in Sources */, - 6CD26C6E2C8EA1E600ADBA38 /* LanguageServerFileMap.swift in Sources */, + 6CD26C6E2C8EA1E600ADBA38 /* LanguageServerFileData.swift in Sources */, 587B9E6A29301D8F00AC7927 /* GitLabPermissions.swift in Sources */, B6EA1FF529DA380E001BF195 /* TextEditingSettingsView.swift in Sources */, + 6C3B4CD82D0E39A900C6759E /* LanguageServer+Notifications.swift in Sources */, D7DC4B76298FFBE900D6C83D /* ProjectNavigatorViewController+OutlineTableViewCellDelegate.swift in Sources */, 587B9E9229301D8F00AC7927 /* BitBucketAccount.swift in Sources */, DE513F52281B672D002260B9 /* EditorTabBarAccessory.swift in Sources */, @@ -4339,6 +4357,7 @@ 6C6BD6EF29CD12E900235D17 /* ExtensionManagerWindow.swift in Sources */, 30B087FF2C0D53080063A882 /* LanguageServer+Completion.swift in Sources */, 61C7E82F2C6CDBA500845336 /* Theme+FuzzySearchable.swift in Sources */, + 6C3B4CD12D0E2C2900C6759E /* SemanticTokenMap.swift in Sources */, 6CFF967629BEBCD900182D6F /* FileCommands.swift in Sources */, B60718462B17DC15009CDAB4 /* RepoOutlineGroupItem.swift in Sources */, 613899B32B6E6FEE00A5CAF6 /* FuzzySearchable.swift in Sources */, @@ -4539,6 +4558,7 @@ 6195E3112B640485007261CA /* WorkspaceDocument+SearchState+IndexTests.swift in Sources */, 6130536B2B24722C00D767E3 /* AsyncIndexingTests.swift in Sources */, 613899C02B6E70FE00A5CAF6 /* FuzzySearchTests.swift in Sources */, + 6C3B4CD42D0E2CB000C6759E /* SemanticTokenMapTests.swift in Sources */, 6195E30D2B64044F007261CA /* WorkspaceDocument+SearchState+FindTests.swift in Sources */, 6195E30F2B640474007261CA /* WorkspaceDocument+SearchState+FindAndReplaceTests.swift in Sources */, 587B612E293419B700D5CD8F /* CodeFileTests.swift in Sources */, diff --git a/CodeEdit/Features/Documents/CodeFileDocument/CodeFileDocument.swift b/CodeEdit/Features/Documents/CodeFileDocument/CodeFileDocument.swift index e3afda72c1..e2e015c3dd 100644 --- a/CodeEdit/Features/Documents/CodeFileDocument/CodeFileDocument.swift +++ b/CodeEdit/Features/Documents/CodeFileDocument/CodeFileDocument.swift @@ -29,7 +29,10 @@ final class CodeFileDocument: NSDocument, ObservableObject { static let logger = Logger(subsystem: Bundle.main.bundleIdentifier ?? "", category: "CodeFileDocument") - @Service var lspService: LSPService + /// Sent when the document is opened. The document will be sent in the notification's object. + static let didOpenNotification = Notification.Name(rawValue: "CodeFileDocument.didOpen") + /// Sent when the document is closed. The document's `fileURL` will be sent in the notification's object. + static let didCloseNotification = Notification.Name(rawValue: "CodeFileDocument.didClose") /// The text content of the document, stored as a text storage /// @@ -47,12 +50,6 @@ final class CodeFileDocument: NSDocument, ObservableObject { /// See ``CodeEditSourceEditor/CombineCoordinator``. @Published var contentCoordinator: CombineCoordinator = CombineCoordinator() - lazy var languageServerCoordinator: LSPContentCoordinator = { - let coordinator = LSPContentCoordinator() - coordinator.uri = self.languageServerURI - return coordinator - }() - /// Used to override detected languages. @Published var language: CodeLanguage? @@ -161,6 +158,7 @@ final class CodeFileDocument: NSDocument, ObservableObject { } else { Self.logger.error("Failed to read file from data using encoding: \(rawEncoding)") } + NotificationCenter.default.post(name: Self.didOpenNotification, object: self) } /// Triggered when change occurred @@ -187,7 +185,7 @@ final class CodeFileDocument: NSDocument, ObservableObject { override func close() { super.close() - lspService.closeDocument(self) + NotificationCenter.default.post(name: Self.didCloseNotification, object: self.fileURL) } func getLanguage() -> CodeLanguage { diff --git a/CodeEdit/Features/Documents/Controllers/CodeEditDocumentController.swift b/CodeEdit/Features/Documents/Controllers/CodeEditDocumentController.swift index 778f707345..5f58b5d3a8 100644 --- a/CodeEdit/Features/Documents/Controllers/CodeEditDocumentController.swift +++ b/CodeEdit/Features/Documents/Controllers/CodeEditDocumentController.swift @@ -92,13 +92,6 @@ final class CodeEditDocumentController: NSDocumentController { } } } - - override func addDocument(_ document: NSDocument) { - super.addDocument(document) - if let document = document as? CodeFileDocument { - lspService.openDocument(document) - } - } } extension NSDocumentController { diff --git a/CodeEdit/Features/Editor/Views/CodeFileView.swift b/CodeEdit/Features/Editor/Views/CodeFileView.swift index 7f80a16ceb..3d199ba157 100644 --- a/CodeEdit/Features/Editor/Views/CodeFileView.swift +++ b/CodeEdit/Features/Editor/Views/CodeFileView.swift @@ -56,12 +56,18 @@ struct CodeFileView: View { init(codeFile: CodeFileDocument, textViewCoordinators: [TextViewCoordinator] = [], isEditable: Bool = true) { self._codeFile = .init(wrappedValue: codeFile) - self.textViewCoordinators = textViewCoordinators + [ - codeFile.contentCoordinator, - codeFile.languageServerCoordinator - ] + self.textViewCoordinators = [] // Temporary value, so we can use ``getLSPAddons`` self.isEditable = isEditable + let lspCoordinator: [TextViewCoordinator] + if let coordinator = getLSPAddons(codeFile: codeFile) { + lspCoordinator = [coordinator] + } else { + lspCoordinator = [] + } + + self.textViewCoordinators = textViewCoordinators + [codeFile.contentCoordinator] + lspCoordinator + if let openOptions = codeFile.openOptions { codeFile.openOptions = nil self.cursorPositions = openOptions.cursorPositions @@ -176,6 +182,21 @@ struct CodeFileView: View { return .underline(color: color) } } + + /// Use this method to find and return any LSP-related objects that should be injected into the editor view. + /// - Returns: The content coordinator for language server content synchronization. + private func getLSPAddons(codeFile: CodeFileDocument) -> LSPContentCoordinator? { + guard let lspService = ServiceContainer.resolve(.singleton, LSPService.self), + let workspaceURL = codeFile.findWorkspace()?.fileURL, + let language = codeFile.language?.lspLanguage, + let coordinator = lspService.languageClient( + for: language, + workspacePath: workspaceURL.absoluteURL.path() + )?.openFiles.contentCoordinator(for: codeFile) else { + return nil + } + return coordinator + } } // This extension is kept here because it should not be used elsewhere in the app and may cause confusion diff --git a/CodeEdit/Features/LSP/LanguageServer/LSPContentCoordinator.swift b/CodeEdit/Features/LSP/Editor/LSPContentCoordinator.swift similarity index 87% rename from CodeEdit/Features/LSP/LanguageServer/LSPContentCoordinator.swift rename to CodeEdit/Features/LSP/Editor/LSPContentCoordinator.swift index 9c405f64ee..2c87596fcc 100644 --- a/CodeEdit/Features/LSP/LanguageServer/LSPContentCoordinator.swift +++ b/CodeEdit/Features/LSP/Editor/LSPContentCoordinator.swift @@ -33,9 +33,12 @@ class LSPContentCoordinator: TextViewCoordinator, TextViewDelegate { private var task: Task? weak var languageServer: LanguageServer? - var uri: String? + var documentURI: String - init() { + /// Initializes a content coordinator, and begins an async stream of updates + init(documentURI: String, languageServer: LanguageServer) { + self.documentURI = documentURI + self.languageServer = languageServer self.stream = AsyncStream { continuation in self.sequenceContinuation = continuation } @@ -71,12 +74,12 @@ class LSPContentCoordinator: TextViewCoordinator, TextViewDelegate { } func textView(_ textView: TextView, didReplaceContentsIn range: NSRange, with string: String) { - guard let uri, - let lspRange = editedRange else { + print("didReplaceContents") + guard let lspRange = editedRange else { return } self.editedRange = nil - self.sequenceContinuation?.yield(SequenceElement(uri: uri, range: lspRange, string: string)) + self.sequenceContinuation?.yield(SequenceElement(uri: documentURI, range: lspRange, string: string)) } func destroy() { diff --git a/CodeEdit/Features/LSP/Editor/SemanticTokenMap.swift b/CodeEdit/Features/LSP/Editor/SemanticTokenMap.swift new file mode 100644 index 0000000000..cc20a485ea --- /dev/null +++ b/CodeEdit/Features/LSP/Editor/SemanticTokenMap.swift @@ -0,0 +1,69 @@ +// +// SemanticTokenMap.swift +// CodeEdit +// +// Created by Khan Winter on 11/10/24. +// + +import LanguageClient +import LanguageServerProtocol +import CodeEditSourceEditor +import CodeEditTextView + +/// Creates a mapping from a language server's semantic token options to a format readable by CodeEdit +/// Provides a convenience method for mapping tokens received from the server to highlight ranges suitable for +/// highlighting in the editor +/// +/// Use this type to handle the initially received semantic highlight capabilities structures. This type will figure +/// out how to read it into a format it can use. +/// +/// After initialization, the map it static (until the server is reinitialized). Similarly, this type is `Sendable` +/// and immutable after initialization. +struct SemanticTokenMap: Sendable { + private let tokenTypeMap: [CaptureName?] + private let modifierMap: [CaptureModifier?] + + init(semanticCapability: TwoTypeOption) { + let legend: SemanticTokensLegend + switch semanticCapability { + case .optionA(let tokensOptions): + legend = tokensOptions.legend + case .optionB(let tokensRegistrationOptions): + legend = tokensRegistrationOptions.legend + } + + tokenTypeMap = legend.tokenTypes.map { CaptureName.fromString($0) } + modifierMap = legend.tokenModifiers.map { CaptureModifier.fromString($0) } + } + + @MainActor + func convert(tokens: SemanticTokens, using textView: TextView) -> [HighlightRange] { + tokens.decode().compactMap { token in + guard let range = textView.nsRangeFrom(line: token.line, char: token.char, length: token.length) else { + return nil + } + + var modifiers: CaptureModifierSet = [] + var raw = token.modifiers + while raw > 0 { + let idx = raw.trailingZeroBitCount + // We don't use `[safe:]` because it creates a double optional here. If someone knows how to extend + // a collection of optionals to make that return only a single optional this could be updated. + guard let modifier = modifierMap.indices.contains(idx) ? modifierMap[idx] : nil else { + raw &= ~(1 << raw.trailingZeroBitCount) + continue + } + modifiers.insert(modifier) + } + + let type = Int(token.type) + let capture = tokenTypeMap.indices.contains(type) ? tokenTypeMap[type] : nil + + return HighlightRange( + range: range, + capture: capture, + modifiers: modifiers + ) + } + } +} diff --git a/CodeEdit/Features/LSP/LanguageServer/Capabilities/LanguageServer+DocumentSync.swift b/CodeEdit/Features/LSP/LanguageServer/Capabilities/LanguageServer+DocumentSync.swift index dcf12fa834..c06831d334 100644 --- a/CodeEdit/Features/LSP/LanguageServer/Capabilities/LanguageServer+DocumentSync.swift +++ b/CodeEdit/Features/LSP/LanguageServer/Capabilities/LanguageServer+DocumentSync.swift @@ -19,7 +19,7 @@ extension LanguageServer { } logger.debug("Opening Document \(content.uri, privacy: .private)") - self.openFiles.addDocument(document) + self.openFiles.addDocument(document, for: self) let textDocument = TextDocumentItem( uri: content.uri, @@ -28,7 +28,6 @@ extension LanguageServer { text: content.string ) try await lspInstance.textDocumentDidOpen(DidOpenTextDocumentParams(textDocument: textDocument)) - await updateIsolatedTextCoordinator(for: document) } catch { logger.warning("addDocument: Error \(error)") throw error @@ -118,12 +117,6 @@ extension LanguageServer { return DocumentContent(uri: uri, language: language, string: content) } - /// Updates the actor-isolated document's text coordinator to map to this server. - @MainActor - fileprivate func updateIsolatedTextCoordinator(for document: CodeFileDocument) { - document.languageServerCoordinator.languageServer = self - } - // swiftlint:disable line_length /// Determines the type of document sync the server supports. /// https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_synchronization_sc diff --git a/CodeEdit/Features/LSP/LanguageServer/LanguageServer+Notifications.swift b/CodeEdit/Features/LSP/LanguageServer/LanguageServer+Notifications.swift new file mode 100644 index 0000000000..c56c4da3fe --- /dev/null +++ b/CodeEdit/Features/LSP/LanguageServer/LanguageServer+Notifications.swift @@ -0,0 +1,34 @@ +// +// LanguageServer+Notifications.swift +// CodeEdit +// +// Created by Khan Winter on 12/14/24. +// + +import Foundation + +extension LanguageServer { + func setUpNotifications() { + NotificationCenter.default.addObserver( + forName: CodeFileDocument.didOpenNotification, + object: nil, + queue: .main + ) { notification in + guard let document = notification.object as? CodeFileDocument else { return } + Task { @MainActor in + try await self.openDocument(document) + } + } + + NotificationCenter.default.addObserver( + forName: CodeFileDocument.didCloseNotification, + object: nil, + queue: .main + ) { notification in + guard let uri = notification.object as? URL else { return } + Task { @MainActor in + try await self.closeDocument(uri.languageServerURI) + } + } + } +} diff --git a/CodeEdit/Features/LSP/LanguageServer/LanguageServer.swift b/CodeEdit/Features/LSP/LanguageServer/LanguageServer.swift index 9f00b4f4c3..0ef8023009 100644 --- a/CodeEdit/Features/LSP/LanguageServer/LanguageServer.swift +++ b/CodeEdit/Features/LSP/LanguageServer/LanguageServer.swift @@ -22,7 +22,13 @@ class LanguageServer { /// A cache to hold responses from the server, to minimize duplicate server requests let lspCache = LSPCache() - let openFiles: LanguageServerFileMap + /// Tracks documents and their associated objects. + /// Use this property when adding new objects that need to track file data, or have a state associated with the + /// language server and a document. For example, the content coordinator. + let openFiles: LanguageServerFileData + + /// Maps the language server's highlight config to one CodeEdit can read. See ``SemanticTokenMap``. + let highlightMap: SemanticTokenMap? /// The configuration options this server supports. var serverCapabilities: ServerCapabilities @@ -44,11 +50,18 @@ class LanguageServer { self.lspInstance = lspInstance self.serverCapabilities = serverCapabilities self.rootPath = rootPath - self.openFiles = LanguageServerFileMap() + self.openFiles = LanguageServerFileData() self.logger = Logger( subsystem: Bundle.main.bundleIdentifier ?? "", category: "LanguageServer.\(languageId.rawValue)" ) + if let semanticTokensProvider = serverCapabilities.semanticTokensProvider { + self.highlightMap = SemanticTokenMap(semanticCapability: semanticTokensProvider) + } else { + self.highlightMap = nil // Server doesn't support semantic highlights + } + + setUpNotifications() } /// Creates and initializes a language server. @@ -82,6 +95,8 @@ class LanguageServer { ) } + // MARK: - Make Local Server Connection + /// Creates a data channel for sending and receiving data with an LSP. /// - Parameters: /// - languageId: The ID of the language to create the channel for. @@ -105,6 +120,8 @@ class LanguageServer { } } + // MARK: - Get Init Params + // swiftlint:disable function_body_length static func getInitParams(workspacePath: String) -> InitializingServer.InitializeParamsProvider { let provider: InitializingServer.InitializeParamsProvider = { @@ -136,15 +153,15 @@ class LanguageServer { // swiftlint:disable:next line_length // https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#semanticTokensClientCapabilities semanticTokens: SemanticTokensClientCapabilities( - dynamicRegistration: true, - requests: .init(range: true, delta: false), - tokenTypes: [], - tokenModifiers: [], + dynamicRegistration: false, + requests: .init(range: true, delta: true), + tokenTypes: SemanticTokenTypes.allStrings, + tokenModifiers: SemanticTokenModifiers.allStrings, formats: [.relative], overlappingTokenSupport: true, multilineTokenSupport: true, serverCancelSupport: true, - augmentsSyntaxTokens: false + augmentsSyntaxTokens: true ) ) @@ -219,6 +236,10 @@ class LanguageServer { self.logger.info("Shutting down language server") try await lspInstance.shutdownAndExit() } + + deinit { + NotificationCenter.default.removeObserver(self) + } } /// Represents a language server binary. diff --git a/CodeEdit/Features/LSP/LanguageServer/LanguageServerFileMap.swift b/CodeEdit/Features/LSP/LanguageServer/LanguageServerFileData.swift similarity index 54% rename from CodeEdit/Features/LSP/LanguageServer/LanguageServerFileMap.swift rename to CodeEdit/Features/LSP/LanguageServer/LanguageServerFileData.swift index 0f3d4469f7..b4e452c898 100644 --- a/CodeEdit/Features/LSP/LanguageServer/LanguageServerFileMap.swift +++ b/CodeEdit/Features/LSP/LanguageServer/LanguageServerFileData.swift @@ -8,9 +8,17 @@ import Foundation import LanguageServerProtocol -class LanguageServerFileMap { +/// Tracks data associated with files and language servers. +class LanguageServerFileData { + /// Extend this struct as more objects are associated with a code document. + private struct DocumentObject { + let uri: String + var documentVersion: Int + var contentCoordinator: LSPContentCoordinator + } + private var trackedDocuments: NSMapTable - private var trackedDocumentVersions: [String: Int] = [:] + private var trackedDocumentData: [String: DocumentObject] = [:] init() { trackedDocuments = NSMapTable(valueOptions: [.weakMemory]) @@ -18,10 +26,14 @@ class LanguageServerFileMap { // MARK: - Track & Remove Documents - func addDocument(_ document: CodeFileDocument) { + func addDocument(_ document: CodeFileDocument, for server: LanguageServer) { guard let uri = document.languageServerURI else { return } trackedDocuments.setObject(document, forKey: uri as NSString) - trackedDocumentVersions[uri] = 0 + trackedDocumentData[uri] = DocumentObject( + uri: uri, + documentVersion: 0, + contentCoordinator: LSPContentCoordinator(documentURI: uri, languageServer: server) + ) } func document(for uri: DocumentUri) -> CodeFileDocument? { @@ -36,7 +48,7 @@ class LanguageServerFileMap { func removeDocument(for uri: DocumentUri) { trackedDocuments.removeObject(forKey: uri as NSString) - trackedDocumentVersions.removeValue(forKey: uri) + trackedDocumentData.removeValue(forKey: uri) } // MARK: - Version Number Tracking @@ -47,8 +59,8 @@ class LanguageServerFileMap { } func incrementVersion(for uri: DocumentUri) -> Int { - trackedDocumentVersions[uri] = (trackedDocumentVersions[uri] ?? 0) + 1 - return trackedDocumentVersions[uri] ?? 0 + trackedDocumentData[uri]?.documentVersion += 1 + return trackedDocumentData[uri]?.documentVersion ?? 0 } func documentVersion(for document: CodeFileDocument) -> Int? { @@ -57,6 +69,17 @@ class LanguageServerFileMap { } func documentVersion(for uri: DocumentUri) -> Int? { - return trackedDocumentVersions[uri] + return trackedDocumentData[uri]?.documentVersion + } + + // MARK: - Content Coordinator + + func contentCoordinator(for document: CodeFileDocument) -> LSPContentCoordinator? { + guard let uri = document.languageServerURI else { return nil } + return contentCoordinator(for: uri) + } + + func contentCoordinator(for uri: DocumentUri) -> LSPContentCoordinator? { + trackedDocumentData[uri]?.contentCoordinator } } diff --git a/CodeEdit/Features/LSP/Service/LSPService.swift b/CodeEdit/Features/LSP/Service/LSPService.swift index 0c4b5a812c..ece6b65637 100644 --- a/CodeEdit/Features/LSP/Service/LSPService.swift +++ b/CodeEdit/Features/LSP/Service/LSPService.swift @@ -135,7 +135,7 @@ final class LSPService: ObservableObject { } /// Gets the language server for the specified language and workspace. - func server(for languageId: LanguageIdentifier, workspacePath: String) async -> InitializingServer? { + func server(for languageId: LanguageIdentifier, workspacePath: String) -> InitializingServer? { return languageClients[ClientKey(languageId, workspacePath)]?.lspInstance } diff --git a/CodeEdit/Utils/Extensions/TextView/TextView+LSPRange.swift b/CodeEdit/Utils/Extensions/TextView/TextView+LSPRange.swift index 4d7d3858c2..9dad5d564f 100644 --- a/CodeEdit/Utils/Extensions/TextView/TextView+LSPRange.swift +++ b/CodeEdit/Utils/Extensions/TextView/TextView+LSPRange.swift @@ -20,4 +20,11 @@ extension TextView { end: Position(line: endLine.index, character: nsRange.max - endLine.range.location) ) } + + func nsRangeFrom(line: UInt32, char: UInt32, length: UInt32) -> NSRange? { + guard let line = layoutManager.textLineForIndex(Int(line)) else { + return nil + } + return NSRange(location: line.range.location + Int(char), length: Int(length)) + } } diff --git a/CodeEditTests/Features/LSP/BufferingServerConnection.swift b/CodeEditTests/Features/LSP/BufferingServerConnection.swift index 824e9853a4..8a69de26f0 100644 --- a/CodeEditTests/Features/LSP/BufferingServerConnection.swift +++ b/CodeEditTests/Features/LSP/BufferingServerConnection.swift @@ -11,7 +11,11 @@ import LanguageServerProtocol import JSONRPC class BufferingServerConnection: ServerConnection { + typealias ClientEventSequence = AsyncStream<([ClientRequest], [ClientNotification])> + var eventSequence: EventSequence + var clientEventSequence: ClientEventSequence + private var clientEventContinuation: ClientEventSequence.Continuation private var id = 0 public var clientRequests: [ClientRequest] = [] @@ -20,13 +24,20 @@ class BufferingServerConnection: ServerConnection { init() { let (sequence, _) = EventSequence.makeStream() self.eventSequence = sequence + (clientEventSequence, clientEventContinuation) = ClientEventSequence.makeStream() } func sendNotification(_ notif: ClientNotification) async throws { + print("NOTIFICATION", notif.method) clientNotifications.append(notif) + clientEventContinuation.yield((clientRequests, clientNotifications)) } func sendRequest(_ request: ClientRequest) async throws -> Response { + defer { + clientEventContinuation.yield((clientRequests, clientNotifications)) + } + clientRequests.append(request) id += 1 let response: Codable diff --git a/CodeEditTests/Features/LSP/LanguageServer+DocumentTests.swift b/CodeEditTests/Features/LSP/LanguageServer+DocumentTests.swift index 5f9aad9aaf..244fa06e3c 100644 --- a/CodeEditTests/Features/LSP/LanguageServer+DocumentTests.swift +++ b/CodeEditTests/Features/LSP/LanguageServer+DocumentTests.swift @@ -80,15 +80,46 @@ final class LanguageServerDocumentTests: XCTestCase { return (workspace, fileManager) } + func openCodeFile( + for server: LanguageServer, + connection: BufferingServerConnection, + file: CEWorkspaceFile, + syncOption: TwoTypeOption? + ) async throws -> CodeFileDocument { + let codeFile = try await CodeFileDocument( + for: file.url, + withContentsOf: file.url, + ofType: "public.swift-source" + ) + + await waitForClientEventCount( + 3, + connection: connection, + description: "Initialized (2) and opened (1) notification count" + ) + + // Set up full content changes + server.serverCapabilities = ServerCapabilities() + server.serverCapabilities.textDocumentSync = syncOption + server.openFiles.addDocument(codeFile, for: server) + + return codeFile + } + func waitForClientEventCount(_ count: Int, connection: BufferingServerConnection, description: String) async { let expectation = expectation(description: description) - Task.detached { - while connection.clientNotifications.count + connection.clientRequests.count < count { - try await Task.sleep(for: .milliseconds(10)) + + await withTaskGroup(of: Void.self) { group in + group.addTask { + await self.fulfillment(of: [expectation], timeout: 2) + } + group.addTask { + for await events in connection.clientEventSequence where events.0.count + events.1.count == count { + expectation.fulfill() + return + } } - expectation.fulfill() } - await fulfillment(of: [expectation], timeout: 2) } @MainActor @@ -184,36 +215,29 @@ final class LanguageServerDocumentTests: XCTestCase { for option in syncOptions { // Set up test server let (connection, server) = try await makeTestServer() - // Create a CodeFileDocument to test with, attach it to the workspace and file - let codeFile = try CodeFileDocument( - for: file.url, - withContentsOf: file.url, - ofType: "public.swift-source" - ) - - // Set up full content changes - server.serverCapabilities = ServerCapabilities() - server.serverCapabilities.textDocumentSync = option - server.openFiles.addDocument(codeFile) - codeFile.languageServerCoordinator.languageServer = server - codeFile.languageServerCoordinator.setUpUpdatesTask() + let codeFile = try await openCodeFile(for: server, connection: connection, file: file, syncOption: option) + XCTAssertNotNil(server.openFiles.contentCoordinator(for: codeFile)) + server.openFiles.contentCoordinator(for: codeFile)?.setUpUpdatesTask() codeFile.content?.replaceString(in: .zero, with: #"func testFunction() -> String { "Hello " }"#) let textView = TextView(string: "") textView.setTextStorage(codeFile.content!) - textView.delegate = codeFile.languageServerCoordinator + textView.delegate = server.openFiles.contentCoordinator(for: codeFile) + textView.replaceCharacters(in: NSRange(location: 39, length: 0), with: "Worlld") textView.replaceCharacters(in: NSRange(location: 39, length: 6), with: "") textView.replaceCharacters(in: NSRange(location: 39, length: 0), with: "World") - await waitForClientEventCount(3, connection: connection, description: "Edited notification count") + // Added one notification + await waitForClientEventCount(4, connection: connection, description: "Edited notification count") // Make sure our text view is intact XCTAssertEqual(textView.string, #"func testFunction() -> String { "Hello World" }"#) XCTAssertEqual( [ ClientNotification.Method.initialized, + ClientNotification.Method.textDocumentDidOpen, ClientNotification.Method.textDocumentDidChange ], connection.clientNotifications.map { $0.method } @@ -230,7 +254,7 @@ final class LanguageServerDocumentTests: XCTestCase { @MainActor func testDocumentEditNotificationsIncrementalChanges() async throws { // Set up test server - let (connection, server) = try await makeTestServer() + let (_, _) = try await makeTestServer() // Set up a workspace in the temp directory let (_, fileManager) = try makeTestWorkspace() @@ -250,37 +274,28 @@ final class LanguageServerDocumentTests: XCTestCase { for option in syncOptions { // Set up test server let (connection, server) = try await makeTestServer() + let codeFile = try await openCodeFile(for: server, connection: connection, file: file, syncOption: option) - // Create a CodeFileDocument to test with, attach it to the workspace and file - let codeFile = try CodeFileDocument( - for: file.url, - withContentsOf: file.url, - ofType: "public.swift-source" - ) - - // Set up full content changes - server.serverCapabilities = ServerCapabilities() - server.serverCapabilities.textDocumentSync = option - server.openFiles.addDocument(codeFile) - codeFile.languageServerCoordinator.languageServer = server - codeFile.languageServerCoordinator.setUpUpdatesTask() + XCTAssertNotNil(server.openFiles.contentCoordinator(for: codeFile)) + server.openFiles.contentCoordinator(for: codeFile)?.setUpUpdatesTask() codeFile.content?.replaceString(in: .zero, with: #"func testFunction() -> String { "Hello " }"#) let textView = TextView(string: "") textView.setTextStorage(codeFile.content!) - textView.delegate = codeFile.languageServerCoordinator + textView.delegate = server.openFiles.contentCoordinator(for: codeFile) textView.replaceCharacters(in: NSRange(location: 39, length: 0), with: "Worlld") textView.replaceCharacters(in: NSRange(location: 39, length: 6), with: "") textView.replaceCharacters(in: NSRange(location: 39, length: 0), with: "World") - // Throttling means we should receive one edited notification + init notification + init request - await waitForClientEventCount(3, connection: connection, description: "Edited notification count") + // Throttling means we should receive one edited notification + init notification + didOpen + init request + await waitForClientEventCount(4, connection: connection, description: "Edited notification count") // Make sure our text view is intact XCTAssertEqual(textView.string, #"func testFunction() -> String { "Hello World" }"#) XCTAssertEqual( [ ClientNotification.Method.initialized, + ClientNotification.Method.textDocumentDidOpen, ClientNotification.Method.textDocumentDidChange ], connection.clientNotifications.map { $0.method } diff --git a/CodeEditTests/Features/LSP/SemanticTokenMapTests.swift b/CodeEditTests/Features/LSP/SemanticTokenMapTests.swift new file mode 100644 index 0000000000..746373dc9a --- /dev/null +++ b/CodeEditTests/Features/LSP/SemanticTokenMapTests.swift @@ -0,0 +1,13 @@ +// +// SemanticTokenMapTests.swift +// CodeEditTests +// +// Created by Khan Winter on 12/14/24. +// + +import XCTest +@testable import CodeEdit + +final class SemanticTokenMapTestsTests: XCTestCase { + +} From 71ff22278e41c86a4a7c8cfa57653bc11be84d87 Mon Sep 17 00:00:00 2001 From: Khan Winter <35942988+thecoolwinter@users.noreply.github.com> Date: Thu, 19 Dec 2024 09:43:17 -0600 Subject: [PATCH 02/16] Decouple SemanticTokenMap from TextView --- CodeEdit.xcodeproj/project.pbxproj | 8 +++ .../LSP/Editor/SemanticTokenMap.swift | 52 +++++++++++++------ .../SemanticTokenMapRangeProvider.swift | 12 +++++ .../TextView/TextView+LSPRange.swift | 7 --- .../TextView+SemanticTokenRangeProvider.swift | 18 +++++++ .../Features/LSP/SemanticTokenMapTests.swift | 38 ++++++++++++++ 6 files changed, 112 insertions(+), 23 deletions(-) create mode 100644 CodeEdit/Features/LSP/Editor/SemanticTokenMapRangeProvider.swift create mode 100644 CodeEdit/Utils/Extensions/TextView/TextView+SemanticTokenRangeProvider.swift diff --git a/CodeEdit.xcodeproj/project.pbxproj b/CodeEdit.xcodeproj/project.pbxproj index b6aa04e855..ba760fd0c9 100644 --- a/CodeEdit.xcodeproj/project.pbxproj +++ b/CodeEdit.xcodeproj/project.pbxproj @@ -461,6 +461,8 @@ 6CC17B512C43311900834E2C /* ProjectNavigatorViewController+NSOutlineViewDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6CC17B502C43311900834E2C /* ProjectNavigatorViewController+NSOutlineViewDataSource.swift */; }; 6CC17B532C43314000834E2C /* ProjectNavigatorViewController+NSOutlineViewDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6CC17B522C43314000834E2C /* ProjectNavigatorViewController+NSOutlineViewDelegate.swift */; }; 6CC17B5B2C44258700834E2C /* WindowControllerPropertyWrapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6CC17B5A2C44258700834E2C /* WindowControllerPropertyWrapper.swift */; }; + 6CC3D1FB2D1475EC00822B65 /* TextView+SemanticTokenRangeProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6CC3D1FA2D1475EC00822B65 /* TextView+SemanticTokenRangeProvider.swift */; }; + 6CC3D1FD2D14761A00822B65 /* SemanticTokenMapRangeProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6CC3D1FC2D14761A00822B65 /* SemanticTokenMapRangeProvider.swift */; }; 6CC9E4B229B5669900C97388 /* Environment+ActiveEditor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6CC9E4B129B5669900C97388 /* Environment+ActiveEditor.swift */; }; 6CD0358A2C3461160091E1F4 /* KeyWindowControllerObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6CD035892C3461160091E1F4 /* KeyWindowControllerObserver.swift */; }; 6CD03B6A29FC773F001BD1D0 /* SettingsInjector.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6CD03B6929FC773F001BD1D0 /* SettingsInjector.swift */; }; @@ -1136,6 +1138,8 @@ 6CC17B502C43311900834E2C /* ProjectNavigatorViewController+NSOutlineViewDataSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ProjectNavigatorViewController+NSOutlineViewDataSource.swift"; sourceTree = ""; }; 6CC17B522C43314000834E2C /* ProjectNavigatorViewController+NSOutlineViewDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ProjectNavigatorViewController+NSOutlineViewDelegate.swift"; sourceTree = ""; }; 6CC17B5A2C44258700834E2C /* WindowControllerPropertyWrapper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WindowControllerPropertyWrapper.swift; sourceTree = ""; }; + 6CC3D1FA2D1475EC00822B65 /* TextView+SemanticTokenRangeProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TextView+SemanticTokenRangeProvider.swift"; sourceTree = ""; }; + 6CC3D1FC2D14761A00822B65 /* SemanticTokenMapRangeProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SemanticTokenMapRangeProvider.swift; sourceTree = ""; }; 6CC9E4B129B5669900C97388 /* Environment+ActiveEditor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Environment+ActiveEditor.swift"; sourceTree = ""; }; 6CD035892C3461160091E1F4 /* KeyWindowControllerObserver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyWindowControllerObserver.swift; sourceTree = ""; }; 6CD03B6929FC773F001BD1D0 /* SettingsInjector.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsInjector.swift; sourceTree = ""; }; @@ -2901,6 +2905,7 @@ children = ( 6C278CC62C93971F0066F6D9 /* LSPContentCoordinator.swift */, 6C3B4CD02D0E2C2900C6759E /* SemanticTokenMap.swift */, + 6CC3D1FC2D14761A00822B65 /* SemanticTokenMapRangeProvider.swift */, ); path = Editor; sourceTree = ""; @@ -3038,6 +3043,7 @@ isa = PBXGroup; children = ( 6CB94CFD2C9F1C9A00E8651C /* TextView+LSPRange.swift */, + 6CC3D1FA2D1475EC00822B65 /* TextView+SemanticTokenRangeProvider.swift */, ); path = TextView; sourceTree = ""; @@ -4140,6 +4146,7 @@ 201169E72837B5CA00F92B46 /* SourceControlManager.swift in Sources */, 58822528292C280D00E83CDE /* StatusBarEncodingSelector.swift in Sources */, 0FD96BCE2BEF42530025A697 /* CodeEditWindowController+Toolbar.swift in Sources */, + 6CC3D1FB2D1475EC00822B65 /* TextView+SemanticTokenRangeProvider.swift in Sources */, 6C7F37FE2A3EA6FA00217B83 /* View+focusedValue.swift in Sources */, 6653EE552C34817900B82DE2 /* QuickSearchResultLabel.swift in Sources */, 6139B9152C29B36100CA584B /* CEActiveTask.swift in Sources */, @@ -4485,6 +4492,7 @@ 611191FA2B08CC9000D4459B /* SearchIndexer.swift in Sources */, 58822532292C280D00E83CDE /* UtilityAreaViewModel.swift in Sources */, 043BCF03281DA18A000AC47C /* WorkspaceDocument+SearchState.swift in Sources */, + 6CC3D1FD2D14761A00822B65 /* SemanticTokenMapRangeProvider.swift in Sources */, 58822527292C280D00E83CDE /* StatusBarIndentSelector.swift in Sources */, 587B9E9629301D8F00AC7927 /* BitBucketRepositories.swift in Sources */, 5878DAA6291AE76700DD95A3 /* OpenQuicklyPreviewView.swift in Sources */, diff --git a/CodeEdit/Features/LSP/Editor/SemanticTokenMap.swift b/CodeEdit/Features/LSP/Editor/SemanticTokenMap.swift index cc20a485ea..dae5c909b6 100644 --- a/CodeEdit/Features/LSP/Editor/SemanticTokenMap.swift +++ b/CodeEdit/Features/LSP/Editor/SemanticTokenMap.swift @@ -10,6 +10,7 @@ import LanguageServerProtocol import CodeEditSourceEditor import CodeEditTextView +// swiftlint:disable line_length /// Creates a mapping from a language server's semantic token options to a format readable by CodeEdit /// Provides a convenience method for mapping tokens received from the server to highlight ranges suitable for /// highlighting in the editor @@ -19,7 +20,12 @@ import CodeEditTextView /// /// After initialization, the map it static (until the server is reinitialized). Similarly, this type is `Sendable` /// and immutable after initialization. -struct SemanticTokenMap: Sendable { +/// +/// This type is not coupled to any text system via the use of the ``SemanticTokenMapRangeProvider``. When decoding to +/// highlight ranges, provide a type that can provide ranges for highlighting. +/// +/// [LSP Spec](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#semanticTokensLegend) +struct SemanticTokenMap: Sendable { // swiftlint:enable line_length private let tokenTypeMap: [CaptureName?] private let modifierMap: [CaptureModifier?] @@ -36,26 +42,20 @@ struct SemanticTokenMap: Sendable { modifierMap = legend.tokenModifiers.map { CaptureModifier.fromString($0) } } + /// Decodes the compressed semantic token data into a `HighlightRange` type for use in an editor. + /// This is run on the main actor to prevent runtime errors, due to the use of the actor-isolated `textView`. + /// - Parameters: + /// - tokens: Semantic tokens from a language server. + /// - rangeProvider: The provider to use to translate token ranges to text view ranges. + /// - Returns: An array of decoded highlight ranges. @MainActor - func convert(tokens: SemanticTokens, using textView: TextView) -> [HighlightRange] { + func decode(tokens: SemanticTokens, using rangeProvider: SemanticTokenMapRangeProvider) -> [HighlightRange] { tokens.decode().compactMap { token in - guard let range = textView.nsRangeFrom(line: token.line, char: token.char, length: token.length) else { + guard let range = rangeProvider.nsRangeFrom(line: token.line, char: token.char, length: token.length) else { return nil } - var modifiers: CaptureModifierSet = [] - var raw = token.modifiers - while raw > 0 { - let idx = raw.trailingZeroBitCount - // We don't use `[safe:]` because it creates a double optional here. If someone knows how to extend - // a collection of optionals to make that return only a single optional this could be updated. - guard let modifier = modifierMap.indices.contains(idx) ? modifierMap[idx] : nil else { - raw &= ~(1 << raw.trailingZeroBitCount) - continue - } - modifiers.insert(modifier) - } - + let modifiers = decodeModifier(token.modifiers) let type = Int(token.type) let capture = tokenTypeMap.indices.contains(type) ? tokenTypeMap[type] : nil @@ -66,4 +66,24 @@ struct SemanticTokenMap: Sendable { ) } } + + /// Decodes a raw modifier value into a set of capture modifiers. + /// - Parameter raw: The raw modifier integer to decode. + /// - Returns: A set of modifiers for highlighting. + func decodeModifier(_ raw: UInt32) -> CaptureModifierSet { + var modifiers: CaptureModifierSet = [] + var raw = raw + while raw > 0 { + let idx = raw.trailingZeroBitCount + raw &= ~(1 << idx) + // We don't use `[safe:]` because it creates a double optional here. If someone knows how to extend + // a collection of optionals to make that return only a single optional this could be updated. + guard let modifier = modifierMap.indices.contains(idx) ? modifierMap[idx] : nil else { + continue + } + // modifiers.insert(modifier) + modifiers.rawValue |= 1 << modifier.rawValue + } + return modifiers + } } diff --git a/CodeEdit/Features/LSP/Editor/SemanticTokenMapRangeProvider.swift b/CodeEdit/Features/LSP/Editor/SemanticTokenMapRangeProvider.swift new file mode 100644 index 0000000000..ac0d746973 --- /dev/null +++ b/CodeEdit/Features/LSP/Editor/SemanticTokenMapRangeProvider.swift @@ -0,0 +1,12 @@ +// +// SemanticTokenMapRangeProvider.swift +// CodeEdit +// +// Created by Khan Winter on 12/19/24. +// + +import Foundation + +protocol SemanticTokenMapRangeProvider { + func nsRangeFrom(line: UInt32, char: UInt32, length: UInt32) -> NSRange? +} diff --git a/CodeEdit/Utils/Extensions/TextView/TextView+LSPRange.swift b/CodeEdit/Utils/Extensions/TextView/TextView+LSPRange.swift index 9dad5d564f..4d7d3858c2 100644 --- a/CodeEdit/Utils/Extensions/TextView/TextView+LSPRange.swift +++ b/CodeEdit/Utils/Extensions/TextView/TextView+LSPRange.swift @@ -20,11 +20,4 @@ extension TextView { end: Position(line: endLine.index, character: nsRange.max - endLine.range.location) ) } - - func nsRangeFrom(line: UInt32, char: UInt32, length: UInt32) -> NSRange? { - guard let line = layoutManager.textLineForIndex(Int(line)) else { - return nil - } - return NSRange(location: line.range.location + Int(char), length: Int(length)) - } } diff --git a/CodeEdit/Utils/Extensions/TextView/TextView+SemanticTokenRangeProvider.swift b/CodeEdit/Utils/Extensions/TextView/TextView+SemanticTokenRangeProvider.swift new file mode 100644 index 0000000000..f41060423e --- /dev/null +++ b/CodeEdit/Utils/Extensions/TextView/TextView+SemanticTokenRangeProvider.swift @@ -0,0 +1,18 @@ +// +// TextView+SemanticTokenRangeProvider.swift +// CodeEdit +// +// Created by Khan Winter on 12/19/24. +// + +import Foundation +import CodeEditTextView + +extension TextView: SemanticTokenMapRangeProvider { + func nsRangeFrom(line: UInt32, char: UInt32, length: UInt32) -> NSRange? { + guard let line = layoutManager.textLineForIndex(Int(line)) else { + return nil + } + return NSRange(location: line.range.location + Int(char), length: Int(length)) + } +} diff --git a/CodeEditTests/Features/LSP/SemanticTokenMapTests.swift b/CodeEditTests/Features/LSP/SemanticTokenMapTests.swift index 746373dc9a..93f0a7807e 100644 --- a/CodeEditTests/Features/LSP/SemanticTokenMapTests.swift +++ b/CodeEditTests/Features/LSP/SemanticTokenMapTests.swift @@ -6,8 +6,46 @@ // import XCTest +import CodeEditSourceEditor +import LanguageServerProtocol @testable import CodeEdit final class SemanticTokenMapTestsTests: XCTestCase { + let testLegend: SemanticTokensLegend = .init( + tokenTypes: [ + "include", + "constructor", + "keyword", + "boolean", + "comment", + "number" + ], + tokenModifiers: [ + "declaration", + "definition", + "readonly", + "async", + "modification", + "defaultLibrary" + ] + ) + func testOptionA() { + let map = SemanticTokenMap(semanticCapability: .optionA(SemanticTokensOptions(legend: testLegend))) + + // Test decode modifiers + let modifierRaw = UInt32(0b1101) + let decodedModifiers = map.decodeModifier(modifierRaw) + XCTAssertEqual([.declaration, .readonly, .async], decodedModifiers) + + // Test decode tokens + let tokens = SemanticTokens(tokens: [ + SemanticToken(line: 0, char: 0, length: 1, type: 0b11), + SemanticToken(line: 0, char: 0, length: 1, type: 0b11) + ]) + } + + func testOptionB() { + + } } From 15043d189bd0a43164e42ccae68939251463ef95 Mon Sep 17 00:00:00 2001 From: Khan Winter <35942988+thecoolwinter@users.noreply.github.com> Date: Thu, 19 Dec 2024 11:48:56 -0600 Subject: [PATCH 03/16] Continue Mucking It Up --- CodeEdit.xcodeproj/project.pbxproj | 18 ++--- .../xcshareddata/swiftpm/Package.resolved | 4 +- .../CodeFileDocument/CodeFileDocument.swift | 17 ++--- .../Editor/Models/EditorInstance.swift | 3 +- .../Features/Editor/Views/CodeFileView.swift | 27 +------ .../Editor/Views/EditorAreaView.swift | 6 +- .../LSP/Editor/LSPContentCoordinator.swift | 2 +- .../LSP/Editor/SemanticTokenMap.swift | 5 +- .../LanguageServer+DocumentSync.swift | 2 +- .../LanguageServer+Notifications.swift | 34 --------- .../LSP/LanguageServer/LanguageServer.swift | 2 - .../LanguageServerFileData.swift | 12 ++-- .../Features/LSP/Service/LSPService.swift | 43 +++++++---- .../Extensions/URL/URL+FindWorkspace.swift | 20 ++++++ ...ageServer.swift => URL+absolutePath.swift} | 2 +- .../Features/LSP/SemanticTokenMapTests.swift | 71 ++++++++++++++++++- 16 files changed, 152 insertions(+), 116 deletions(-) delete mode 100644 CodeEdit/Features/LSP/LanguageServer/LanguageServer+Notifications.swift create mode 100644 CodeEdit/Utils/Extensions/URL/URL+FindWorkspace.swift rename CodeEdit/Utils/Extensions/URL/{URL+LanguageServer.swift => URL+absolutePath.swift} (83%) diff --git a/CodeEdit.xcodeproj/project.pbxproj b/CodeEdit.xcodeproj/project.pbxproj index ba760fd0c9..bde1bc1ae5 100644 --- a/CodeEdit.xcodeproj/project.pbxproj +++ b/CodeEdit.xcodeproj/project.pbxproj @@ -384,7 +384,6 @@ 6C2C156129B4F52F00EA60A5 /* SplitViewModifiers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C2C156029B4F52F00EA60A5 /* SplitViewModifiers.swift */; }; 6C3B4CD12D0E2C2900C6759E /* SemanticTokenMap.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C3B4CD02D0E2C2900C6759E /* SemanticTokenMap.swift */; }; 6C3B4CD42D0E2CB000C6759E /* SemanticTokenMapTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C3B4CD32D0E2CB000C6759E /* SemanticTokenMapTests.swift */; }; - 6C3B4CD82D0E39A900C6759E /* LanguageServer+Notifications.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C3B4CD72D0E39A900C6759E /* LanguageServer+Notifications.swift */; }; 6C3E12D32CC830D700DD12F1 /* RecentProjectsStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C3E12D22CC830D700DD12F1 /* RecentProjectsStore.swift */; }; 6C3E12D62CC8388000DD12F1 /* URL+componentCompare.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C3E12D52CC8388000DD12F1 /* URL+componentCompare.swift */; }; 6C3E12D82CC83CB600DD12F1 /* RecentProjectsMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C3E12D72CC83CB600DD12F1 /* RecentProjectsMenu.swift */; }; @@ -443,6 +442,7 @@ 6C9619222C3F27F1009733CE /* Query.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C9619212C3F27F1009733CE /* Query.swift */; }; 6C9619242C3F2809009733CE /* ProjectPath.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C9619232C3F2809009733CE /* ProjectPath.swift */; }; 6C97EBCC2978760400302F95 /* AcknowledgementsWindowController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C97EBCB2978760400302F95 /* AcknowledgementsWindowController.swift */; }; + 6C9AE66F2D148DD200FAE8D2 /* URL+FindWorkspace.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C9AE66E2D148DD200FAE8D2 /* URL+FindWorkspace.swift */; }; 6CA1AE952B46950000378EAB /* EditorInstance.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6CA1AE942B46950000378EAB /* EditorInstance.swift */; }; 6CAAF68A29BC9C2300A1F48A /* (null) in Sources */ = {isa = PBXBuildFile; }; 6CAAF69229BCC71C00A1F48A /* (null) in Sources */ = {isa = PBXBuildFile; }; @@ -467,7 +467,7 @@ 6CD0358A2C3461160091E1F4 /* KeyWindowControllerObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6CD035892C3461160091E1F4 /* KeyWindowControllerObserver.swift */; }; 6CD03B6A29FC773F001BD1D0 /* SettingsInjector.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6CD03B6929FC773F001BD1D0 /* SettingsInjector.swift */; }; 6CD26C6E2C8EA1E600ADBA38 /* LanguageServerFileData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6CD26C6D2C8EA1E600ADBA38 /* LanguageServerFileData.swift */; }; - 6CD26C772C8EA83900ADBA38 /* URL+LanguageServer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6CD26C762C8EA83900ADBA38 /* URL+LanguageServer.swift */; }; + 6CD26C772C8EA83900ADBA38 /* URL+absolutePath.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6CD26C762C8EA83900ADBA38 /* URL+absolutePath.swift */; }; 6CD26C7A2C8EA8A500ADBA38 /* LSPCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6CD26C782C8EA8A500ADBA38 /* LSPCache.swift */; }; 6CD26C7B2C8EA8A500ADBA38 /* LSPCache+Data.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6CD26C792C8EA8A500ADBA38 /* LSPCache+Data.swift */; }; 6CD26C7D2C8EA8F400ADBA38 /* LanguageServer+DocumentSync.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6CD26C7C2C8EA8F400ADBA38 /* LanguageServer+DocumentSync.swift */; }; @@ -1076,7 +1076,6 @@ 6C2C156029B4F52F00EA60A5 /* SplitViewModifiers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SplitViewModifiers.swift; sourceTree = ""; }; 6C3B4CD02D0E2C2900C6759E /* SemanticTokenMap.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SemanticTokenMap.swift; sourceTree = ""; }; 6C3B4CD32D0E2CB000C6759E /* SemanticTokenMapTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SemanticTokenMapTests.swift; sourceTree = ""; }; - 6C3B4CD72D0E39A900C6759E /* LanguageServer+Notifications.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "LanguageServer+Notifications.swift"; sourceTree = ""; }; 6C3E12D22CC830D700DD12F1 /* RecentProjectsStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecentProjectsStore.swift; sourceTree = ""; }; 6C3E12D52CC8388000DD12F1 /* URL+componentCompare.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "URL+componentCompare.swift"; sourceTree = ""; }; 6C3E12D72CC83CB600DD12F1 /* RecentProjectsMenu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecentProjectsMenu.swift; sourceTree = ""; }; @@ -1128,6 +1127,7 @@ 6C9619232C3F2809009733CE /* ProjectPath.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProjectPath.swift; sourceTree = ""; }; 6C9619262C3F285C009733CE /* CodeEditTestPlan.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; path = CodeEditTestPlan.xctestplan; sourceTree = ""; }; 6C97EBCB2978760400302F95 /* AcknowledgementsWindowController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AcknowledgementsWindowController.swift; sourceTree = ""; }; + 6C9AE66E2D148DD200FAE8D2 /* URL+FindWorkspace.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "URL+FindWorkspace.swift"; sourceTree = ""; }; 6CA1AE942B46950000378EAB /* EditorInstance.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditorInstance.swift; sourceTree = ""; }; 6CABB1A029C5593800340467 /* SearchPanelView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SearchPanelView.swift; sourceTree = ""; }; 6CB52DC82AC8DC3E002E75B3 /* CEWorkspaceFileManager+FileManagement.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CEWorkspaceFileManager+FileManagement.swift"; sourceTree = ""; }; @@ -1144,7 +1144,7 @@ 6CD035892C3461160091E1F4 /* KeyWindowControllerObserver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyWindowControllerObserver.swift; sourceTree = ""; }; 6CD03B6929FC773F001BD1D0 /* SettingsInjector.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsInjector.swift; sourceTree = ""; }; 6CD26C6D2C8EA1E600ADBA38 /* LanguageServerFileData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LanguageServerFileData.swift; sourceTree = ""; }; - 6CD26C762C8EA83900ADBA38 /* URL+LanguageServer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "URL+LanguageServer.swift"; sourceTree = ""; }; + 6CD26C762C8EA83900ADBA38 /* URL+absolutePath.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "URL+absolutePath.swift"; sourceTree = ""; }; 6CD26C782C8EA8A500ADBA38 /* LSPCache.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LSPCache.swift; sourceTree = ""; }; 6CD26C792C8EA8A500ADBA38 /* LSPCache+Data.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "LSPCache+Data.swift"; sourceTree = ""; }; 6CD26C7C2C8EA8F400ADBA38 /* LanguageServer+DocumentSync.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "LanguageServer+DocumentSync.swift"; sourceTree = ""; }; @@ -3091,7 +3091,6 @@ isa = PBXGroup; children = ( 30B087F72C0D53080063A882 /* LanguageServer.swift */, - 6C3B4CD72D0E39A900C6759E /* LanguageServer+Notifications.swift */, 6CD26C6D2C8EA1E600ADBA38 /* LanguageServerFileData.swift */, 6CD26C782C8EA8A500ADBA38 /* LSPCache.swift */, 6CD26C792C8EA8A500ADBA38 /* LSPCache+Data.swift */, @@ -3113,8 +3112,9 @@ isa = PBXGroup; children = ( 613899BB2B6E709C00A5CAF6 /* URL+FuzzySearchable.swift */, - 6CD26C762C8EA83900ADBA38 /* URL+LanguageServer.swift */, + 6CD26C762C8EA83900ADBA38 /* URL+absolutePath.swift */, 587B9E2729301D8F00AC7927 /* URL+URLParameters.swift */, + 6C9AE66E2D148DD200FAE8D2 /* URL+FindWorkspace.swift */, ); path = URL; sourceTree = ""; @@ -4015,7 +4015,6 @@ 6CD26C6E2C8EA1E600ADBA38 /* LanguageServerFileData.swift in Sources */, 587B9E6A29301D8F00AC7927 /* GitLabPermissions.swift in Sources */, B6EA1FF529DA380E001BF195 /* TextEditingSettingsView.swift in Sources */, - 6C3B4CD82D0E39A900C6759E /* LanguageServer+Notifications.swift in Sources */, D7DC4B76298FFBE900D6C83D /* ProjectNavigatorViewController+OutlineTableViewCellDelegate.swift in Sources */, 587B9E9229301D8F00AC7927 /* BitBucketAccount.swift in Sources */, DE513F52281B672D002260B9 /* EditorTabBarAccessory.swift in Sources */, @@ -4316,6 +4315,7 @@ 6C092EE02A53BFCF00489202 /* WorkspaceStateKey.swift in Sources */, 618725A82C29F05500987354 /* OptionMenuItemView.swift in Sources */, 613899B52B6E700300A5CAF6 /* FuzzySearchModels.swift in Sources */, + 6C9AE66F2D148DD200FAE8D2 /* URL+FindWorkspace.swift in Sources */, 58D01C94293167DC00C5B6B4 /* Color+HEX.swift in Sources */, 6C578D8729CD345900DC73B2 /* ExtensionSceneView.swift in Sources */, 617DB3D02C25AFAE00B58BFE /* TaskNotificationHandler.swift in Sources */, @@ -4444,7 +4444,7 @@ 6C147C4129A328BF0089B630 /* EditorLayout.swift in Sources */, B6D7EA592971078500301FAC /* InspectorSection.swift in Sources */, B6AB09A32AAABFEC0003A3A6 /* EditorTabBarLeadingAccessories.swift in Sources */, - 6CD26C772C8EA83900ADBA38 /* URL+LanguageServer.swift in Sources */, + 6CD26C772C8EA83900ADBA38 /* URL+absolutePath.swift in Sources */, B69BFDC72B0686910050D9A6 /* GitClient+Initiate.swift in Sources */, 58F2EAEF292FB2B0004A9BDE /* ThemeSettingsView.swift in Sources */, 85745D632A38F8D900089AAB /* String+HighlightOccurrences.swift in Sources */, @@ -5724,7 +5724,7 @@ repositoryURL = "https://github.com/CodeEditApp/CodeEditSourceEditor"; requirement = { kind = upToNextMajorVersion; - minimumVersion = 0.9.0; + minimumVersion = 0.9.1; }; }; 6C0617D42BDB4432008C9C42 /* XCRemoteSwiftPackageReference "LogStream" */ = { diff --git a/CodeEdit.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/CodeEdit.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index ec048d540d..b65c217afc 100644 --- a/CodeEdit.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/CodeEdit.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -33,8 +33,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/CodeEditApp/CodeEditSourceEditor", "state" : { - "revision" : "bfcde1fc536e4159ca3d596fa5b8bbbeb1524362", - "version" : "0.9.0" + "revision" : "b0688fa59fb8060840fb013afb4d6e6a96000f14", + "version" : "0.9.1" } }, { diff --git a/CodeEdit/Features/Documents/CodeFileDocument/CodeFileDocument.swift b/CodeEdit/Features/Documents/CodeFileDocument/CodeFileDocument.swift index e2e015c3dd..7ef7f7425b 100644 --- a/CodeEdit/Features/Documents/CodeFileDocument/CodeFileDocument.swift +++ b/CodeEdit/Features/Documents/CodeFileDocument/CodeFileDocument.swift @@ -80,8 +80,8 @@ final class CodeFileDocument: NSDocument, ObservableObject { return type } - /// A stable string to use when identifying documents with language servers. - var languageServerURI: String? { fileURL?.languageServerURI } + /// Use when identifying documents globally on the user's computer, eg with a language server. + var absolutePath: String? { fileURL?.absolutePath } /// Specify options for opening the file such as the initial cursor positions. /// Nulled by ``CodeFileView`` on first load. @@ -185,7 +185,7 @@ final class CodeFileDocument: NSDocument, ObservableObject { override func close() { super.close() - NotificationCenter.default.post(name: Self.didCloseNotification, object: self.fileURL) + NotificationCenter.default.post(name: Self.didCloseNotification, object: fileURL) } func getLanguage() -> CodeLanguage { @@ -200,15 +200,6 @@ final class CodeFileDocument: NSDocument, ObservableObject { } func findWorkspace() -> WorkspaceDocument? { - CodeEditDocumentController.shared.documents.first(where: { doc in - guard let workspace = doc as? WorkspaceDocument, let path = self.languageServerURI else { return false } - // createIfNotFound is safe here because it will still exit if the file and the workspace - // do not share a path prefix - return workspace - .workspaceFileManager? - .getFile(path, createIfNotFound: true)? - .fileDocument? - .isEqual(self) ?? false - }) as? WorkspaceDocument + fileURL?.findWorkspace() } } diff --git a/CodeEdit/Features/Editor/Models/EditorInstance.swift b/CodeEdit/Features/Editor/Models/EditorInstance.swift index f8aeb8ebc6..6072fa262d 100644 --- a/CodeEdit/Features/Editor/Models/EditorInstance.swift +++ b/CodeEdit/Features/Editor/Models/EditorInstance.swift @@ -26,7 +26,8 @@ class EditorInstance: Hashable { // Public TextViewCoordinator APIs - var rangeTranslator: RangeTranslator? + @Published var rangeTranslator: RangeTranslator? + @Published var lspContentCoordinator: LSPContentCoordinator? // Internal Combine subjects diff --git a/CodeEdit/Features/Editor/Views/CodeFileView.swift b/CodeEdit/Features/Editor/Views/CodeFileView.swift index 3d199ba157..1d2f50961c 100644 --- a/CodeEdit/Features/Editor/Views/CodeFileView.swift +++ b/CodeEdit/Features/Editor/Views/CodeFileView.swift @@ -56,17 +56,8 @@ struct CodeFileView: View { init(codeFile: CodeFileDocument, textViewCoordinators: [TextViewCoordinator] = [], isEditable: Bool = true) { self._codeFile = .init(wrappedValue: codeFile) - self.textViewCoordinators = [] // Temporary value, so we can use ``getLSPAddons`` self.isEditable = isEditable - - let lspCoordinator: [TextViewCoordinator] - if let coordinator = getLSPAddons(codeFile: codeFile) { - lspCoordinator = [coordinator] - } else { - lspCoordinator = [] - } - - self.textViewCoordinators = textViewCoordinators + [codeFile.contentCoordinator] + lspCoordinator + self.textViewCoordinators = textViewCoordinators + [codeFile.contentCoordinator] if let openOptions = codeFile.openOptions { codeFile.openOptions = nil @@ -144,7 +135,6 @@ struct CodeFileView: View { undoManager: undoManager, coordinators: textViewCoordinators ) - .id(codeFile.fileURL) .background { if colorScheme == .dark { @@ -182,21 +172,6 @@ struct CodeFileView: View { return .underline(color: color) } } - - /// Use this method to find and return any LSP-related objects that should be injected into the editor view. - /// - Returns: The content coordinator for language server content synchronization. - private func getLSPAddons(codeFile: CodeFileDocument) -> LSPContentCoordinator? { - guard let lspService = ServiceContainer.resolve(.singleton, LSPService.self), - let workspaceURL = codeFile.findWorkspace()?.fileURL, - let language = codeFile.language?.lspLanguage, - let coordinator = lspService.languageClient( - for: language, - workspacePath: workspaceURL.absoluteURL.path() - )?.openFiles.contentCoordinator(for: codeFile) else { - return nil - } - return coordinator - } } // This extension is kept here because it should not be used elsewhere in the app and may cause confusion diff --git a/CodeEdit/Features/Editor/Views/EditorAreaView.swift b/CodeEdit/Features/Editor/Views/EditorAreaView.swift index 421adf9d69..0f79c78b37 100644 --- a/CodeEdit/Features/Editor/Views/EditorAreaView.swift +++ b/CodeEdit/Features/Editor/Views/EditorAreaView.swift @@ -7,6 +7,7 @@ import SwiftUI import CodeEditTextView +import CodeEditSourceEditor struct EditorAreaView: View { @AppSettings(\.general.showEditorPathBar) @@ -52,7 +53,10 @@ struct EditorAreaView: View { if let codeFile = codeFile { EditorAreaFileView( codeFile: codeFile, - textViewCoordinators: [selected.rangeTranslator].compactMap({ $0 }) + textViewCoordinators: [ + selected.rangeTranslator as Any, + selected.lspContentCoordinator as Any + ].compactMap({ $0 as? any TextViewCoordinator }) ) .focusedObject(editor) .transformEnvironment(\.edgeInsets) { insets in diff --git a/CodeEdit/Features/LSP/Editor/LSPContentCoordinator.swift b/CodeEdit/Features/LSP/Editor/LSPContentCoordinator.swift index 2c87596fcc..7764ad5302 100644 --- a/CodeEdit/Features/LSP/Editor/LSPContentCoordinator.swift +++ b/CodeEdit/Features/LSP/Editor/LSPContentCoordinator.swift @@ -36,7 +36,7 @@ class LSPContentCoordinator: TextViewCoordinator, TextViewDelegate { var documentURI: String /// Initializes a content coordinator, and begins an async stream of updates - init(documentURI: String, languageServer: LanguageServer) { + init(documentURI: String, languageServer: LanguageServer?) { self.documentURI = documentURI self.languageServer = languageServer self.stream = AsyncStream { continuation in diff --git a/CodeEdit/Features/LSP/Editor/SemanticTokenMap.swift b/CodeEdit/Features/LSP/Editor/SemanticTokenMap.swift index dae5c909b6..a5ab2ae958 100644 --- a/CodeEdit/Features/LSP/Editor/SemanticTokenMap.swift +++ b/CodeEdit/Features/LSP/Editor/SemanticTokenMap.swift @@ -56,7 +56,7 @@ struct SemanticTokenMap: Sendable { // swiftlint:enable line_length } let modifiers = decodeModifier(token.modifiers) - let type = Int(token.type) + let type = token.type > 0 ? Int(token.type.trailingZeroBitCount) : -1 // Don't decode 0 let capture = tokenTypeMap.indices.contains(type) ? tokenTypeMap[type] : nil return HighlightRange( @@ -81,8 +81,7 @@ struct SemanticTokenMap: Sendable { // swiftlint:enable line_length guard let modifier = modifierMap.indices.contains(idx) ? modifierMap[idx] : nil else { continue } - // modifiers.insert(modifier) - modifiers.rawValue |= 1 << modifier.rawValue + modifiers.insert(modifier) } return modifiers } diff --git a/CodeEdit/Features/LSP/LanguageServer/Capabilities/LanguageServer+DocumentSync.swift b/CodeEdit/Features/LSP/LanguageServer/Capabilities/LanguageServer+DocumentSync.swift index c06831d334..b9bd8b63c2 100644 --- a/CodeEdit/Features/LSP/LanguageServer/Capabilities/LanguageServer+DocumentSync.swift +++ b/CodeEdit/Features/LSP/LanguageServer/Capabilities/LanguageServer+DocumentSync.swift @@ -109,7 +109,7 @@ extension LanguageServer { /// Helper function for grabbing a document's content from the main actor. @MainActor private func getIsolatedDocumentContent(_ document: CodeFileDocument) -> DocumentContent? { - guard let uri = document.languageServerURI, + guard let uri = document.absolutePath, let language = document.getLanguage().lspLanguage, let content = document.content?.string else { return nil diff --git a/CodeEdit/Features/LSP/LanguageServer/LanguageServer+Notifications.swift b/CodeEdit/Features/LSP/LanguageServer/LanguageServer+Notifications.swift deleted file mode 100644 index c56c4da3fe..0000000000 --- a/CodeEdit/Features/LSP/LanguageServer/LanguageServer+Notifications.swift +++ /dev/null @@ -1,34 +0,0 @@ -// -// LanguageServer+Notifications.swift -// CodeEdit -// -// Created by Khan Winter on 12/14/24. -// - -import Foundation - -extension LanguageServer { - func setUpNotifications() { - NotificationCenter.default.addObserver( - forName: CodeFileDocument.didOpenNotification, - object: nil, - queue: .main - ) { notification in - guard let document = notification.object as? CodeFileDocument else { return } - Task { @MainActor in - try await self.openDocument(document) - } - } - - NotificationCenter.default.addObserver( - forName: CodeFileDocument.didCloseNotification, - object: nil, - queue: .main - ) { notification in - guard let uri = notification.object as? URL else { return } - Task { @MainActor in - try await self.closeDocument(uri.languageServerURI) - } - } - } -} diff --git a/CodeEdit/Features/LSP/LanguageServer/LanguageServer.swift b/CodeEdit/Features/LSP/LanguageServer/LanguageServer.swift index 0ef8023009..676bc4dcab 100644 --- a/CodeEdit/Features/LSP/LanguageServer/LanguageServer.swift +++ b/CodeEdit/Features/LSP/LanguageServer/LanguageServer.swift @@ -60,8 +60,6 @@ class LanguageServer { } else { self.highlightMap = nil // Server doesn't support semantic highlights } - - setUpNotifications() } /// Creates and initializes a language server. diff --git a/CodeEdit/Features/LSP/LanguageServer/LanguageServerFileData.swift b/CodeEdit/Features/LSP/LanguageServer/LanguageServerFileData.swift index b4e452c898..f46d02eda8 100644 --- a/CodeEdit/Features/LSP/LanguageServer/LanguageServerFileData.swift +++ b/CodeEdit/Features/LSP/LanguageServer/LanguageServerFileData.swift @@ -27,7 +27,7 @@ class LanguageServerFileData { // MARK: - Track & Remove Documents func addDocument(_ document: CodeFileDocument, for server: LanguageServer) { - guard let uri = document.languageServerURI else { return } + guard let uri = document.absolutePath else { return } trackedDocuments.setObject(document, forKey: uri as NSString) trackedDocumentData[uri] = DocumentObject( uri: uri, @@ -38,11 +38,11 @@ class LanguageServerFileData { func document(for uri: DocumentUri) -> CodeFileDocument? { let url = URL(filePath: uri) - return trackedDocuments.object(forKey: url.languageServerURI as NSString) + return trackedDocuments.object(forKey: url.absolutePath as NSString) } func removeDocument(for document: CodeFileDocument) { - guard let uri = document.languageServerURI else { return } + guard let uri = document.absolutePath else { return } removeDocument(for: uri) } @@ -54,7 +54,7 @@ class LanguageServerFileData { // MARK: - Version Number Tracking func incrementVersion(for document: CodeFileDocument) -> Int { - guard let uri = document.languageServerURI else { return 0 } + guard let uri = document.absolutePath else { return 0 } return incrementVersion(for: uri) } @@ -64,7 +64,7 @@ class LanguageServerFileData { } func documentVersion(for document: CodeFileDocument) -> Int? { - guard let uri = document.languageServerURI else { return nil } + guard let uri = document.absolutePath else { return nil } return documentVersion(for: uri) } @@ -75,7 +75,7 @@ class LanguageServerFileData { // MARK: - Content Coordinator func contentCoordinator(for document: CodeFileDocument) -> LSPContentCoordinator? { - guard let uri = document.languageServerURI else { return nil } + guard let uri = document.absolutePath else { return nil } return contentCoordinator(for: uri) } diff --git a/CodeEdit/Features/LSP/Service/LSPService.swift b/CodeEdit/Features/LSP/Service/LSPService.swift index ece6b65637..e872557c76 100644 --- a/CodeEdit/Features/LSP/Service/LSPService.swift +++ b/CodeEdit/Features/LSP/Service/LSPService.swift @@ -97,7 +97,6 @@ import CodeEditLanguages /// } /// } /// ``` -@MainActor final class LSPService: ObservableObject { let logger: Logger = Logger(subsystem: Bundle.main.bundleIdentifier ?? "", category: "LSPService") @@ -132,6 +131,24 @@ final class LSPService: ObservableObject { ) } } + + NotificationCenter.default.addObserver( + forName: CodeFileDocument.didOpenNotification, + object: nil, + queue: .main + ) { notification in + guard let document = notification.object as? CodeFileDocument else { return } + self.openDocument(document) + } + + NotificationCenter.default.addObserver( + forName: CodeFileDocument.didCloseNotification, + object: nil, + queue: .main + ) { notification in + guard let url = notification.object as? URL else { return } + self.closeDocument(url) + } } /// Gets the language server for the specified language and workspace. @@ -180,10 +197,10 @@ final class LSPService: ObservableObject { let lspLanguage = document.getLanguage().lspLanguage else { return } - Task.detached { + Task { let languageServer: LanguageServer do { - if let server = await self.languageClients[ClientKey(lspLanguage, workspacePath)] { + if let server = self.languageClients[ClientKey(lspLanguage, workspacePath)] { languageServer = server } else { languageServer = try await self.startServer(for: lspLanguage, workspacePath: workspacePath) @@ -196,7 +213,7 @@ final class LSPService: ObservableObject { do { try await languageServer.openDocument(document) } catch { - let uri = await document.languageServerURI + let uri = await document.absolutePath // swiftlint:disable:next line_length self.logger.error("Failed to close document: \(uri ?? "", privacy: .private), language: \(lspLanguage.rawValue). Error \(error)") } @@ -204,21 +221,19 @@ final class LSPService: ObservableObject { } /// Notify all relevant language clients that a document was closed. - /// - Parameter document: The code document that was closed. - func closeDocument(_ document: CodeFileDocument) { - guard let workspace = document.findWorkspace(), - let workspacePath = workspace.fileURL?.absoluteURL.path(), - let lspLanguage = document.getLanguage().lspLanguage, - let languageClient = self.languageClient(for: lspLanguage, workspacePath: workspacePath), - let uri = document.languageServerURI else { + /// - Parameter url: The url of the document that was closed + func closeDocument(_ url: URL) { + guard let languageClient = languageClients.first(where: { + $0.value.openFiles.document(for: url.absolutePath) != nil + })?.value else { return } Task { do { - try await languageClient.closeDocument(uri) + try await languageClient.closeDocument(url.absolutePath) } catch { // swiftlint:disable:next line_length - logger.error("Failed to close document: \(uri, privacy: .private), language: \(lspLanguage.rawValue). Error \(error)") + logger.error("Failed to close document: \(url.absolutePath, privacy: .private), language: \(languageClient.languageId.rawValue). Error \(error)") } } } @@ -252,7 +267,7 @@ final class LSPService: ObservableObject { /// - languageId: The ID of the language server to stop. /// - workspacePath: The path of the workspace to stop the language server for. func stopServer(forLanguage languageId: LanguageIdentifier, workspacePath: String) async throws { - guard let server = await self.server(for: languageId, workspacePath: workspacePath) else { + guard let server = server(for: languageId, workspacePath: workspacePath) else { logger.error("Server not found for language \(languageId.rawValue) during stop operation") throw ServerManagerError.serverNotFound } diff --git a/CodeEdit/Utils/Extensions/URL/URL+FindWorkspace.swift b/CodeEdit/Utils/Extensions/URL/URL+FindWorkspace.swift new file mode 100644 index 0000000000..a3259ef6bd --- /dev/null +++ b/CodeEdit/Utils/Extensions/URL/URL+FindWorkspace.swift @@ -0,0 +1,20 @@ +// +// URL+FindWorkspace.swift +// CodeEdit +// +// Created by Khan Winter on 12/19/24. +// + +import Foundation + +extension URL { + /// Finds a workspace that contains the url. + func findWorkspace() -> WorkspaceDocument? { + CodeEditDocumentController.shared.documents.first(where: { doc in + guard let workspace = doc as? WorkspaceDocument else { return false } + // createIfNotFound is safe here because it will still exit if the file and the workspace + // do not share a path prefix + return workspace.workspaceFileManager?.getFile(absolutePath, createIfNotFound: true) != nil + }) as? WorkspaceDocument + } +} diff --git a/CodeEdit/Utils/Extensions/URL/URL+LanguageServer.swift b/CodeEdit/Utils/Extensions/URL/URL+absolutePath.swift similarity index 83% rename from CodeEdit/Utils/Extensions/URL/URL+LanguageServer.swift rename to CodeEdit/Utils/Extensions/URL/URL+absolutePath.swift index c6c86966a2..8270af913f 100644 --- a/CodeEdit/Utils/Extensions/URL/URL+LanguageServer.swift +++ b/CodeEdit/Utils/Extensions/URL/URL+absolutePath.swift @@ -8,7 +8,7 @@ import Foundation extension URL { - var languageServerURI: String { + var absolutePath: String { absoluteURL.path(percentEncoded: false) } } diff --git a/CodeEditTests/Features/LSP/SemanticTokenMapTests.swift b/CodeEditTests/Features/LSP/SemanticTokenMapTests.swift index 93f0a7807e..0e4be8b7f6 100644 --- a/CodeEditTests/Features/LSP/SemanticTokenMapTests.swift +++ b/CodeEditTests/Features/LSP/SemanticTokenMapTests.swift @@ -11,6 +11,13 @@ import LanguageServerProtocol @testable import CodeEdit final class SemanticTokenMapTestsTests: XCTestCase { + // Ignores the line parameter and just returns a range from the char and length for testing + struct MockRangeProvider: SemanticTokenMapRangeProvider { + func nsRangeFrom(line: UInt32, char: UInt32, length: UInt32) -> NSRange? { + return NSRange(location: Int(char), length: Int(length)) + } + } + let testLegend: SemanticTokensLegend = .init( tokenTypes: [ "include", @@ -29,7 +36,9 @@ final class SemanticTokenMapTestsTests: XCTestCase { "defaultLibrary" ] ) + let mockProvider = MockRangeProvider() + @MainActor func testOptionA() { let map = SemanticTokenMap(semanticCapability: .optionA(SemanticTokensOptions(legend: testLegend))) @@ -40,12 +49,70 @@ final class SemanticTokenMapTestsTests: XCTestCase { // Test decode tokens let tokens = SemanticTokens(tokens: [ - SemanticToken(line: 0, char: 0, length: 1, type: 0b11), - SemanticToken(line: 0, char: 0, length: 1, type: 0b11) + SemanticToken(line: 0, char: 0, length: 1, type: 0, modifiers: 0b11), // First two indices set + SemanticToken(line: 0, char: 1, length: 2, type: 0, modifiers: 0b100100), // 6th and 3rd indices set + SemanticToken(line: 0, char: 4, length: 1, type: 0b1, modifiers: 0b101), + SemanticToken(line: 0, char: 5, length: 1, type: 0b100, modifiers: 0b1010), + SemanticToken(line: 0, char: 7, length: 10, type: 0, modifiers: 0) ]) + let decoded = map.decode(tokens: tokens, using: mockProvider) + XCTAssertEqual(decoded.count, 5, "Decoded count") + + XCTAssertEqual(decoded[0].range, NSRange(location: 0, length: 1), "Decoded range") + XCTAssertEqual(decoded[1].range, NSRange(location: 1, length: 2), "Decoded range") + XCTAssertEqual(decoded[2].range, NSRange(location: 4, length: 1), "Decoded range") + XCTAssertEqual(decoded[3].range, NSRange(location: 5, length: 1), "Decoded range") + XCTAssertEqual(decoded[4].range, NSRange(location: 7, length: 10), "Decoded range") + + XCTAssertEqual(decoded[0].capture, nil, "No Decoded Capture") + XCTAssertEqual(decoded[1].capture, nil, "No Decoded Capture") + XCTAssertEqual(decoded[2].capture, .include, "Decoded Capture") + XCTAssertEqual(decoded[3].capture, .keyword, "Decoded Capture") + XCTAssertEqual(decoded[4].capture, nil, "No Decoded Capture") + + XCTAssertEqual(decoded[0].modifiers, [.declaration, .definition], "Decoded Modifiers") + XCTAssertEqual(decoded[1].modifiers, [.readonly, .defaultLibrary], "Decoded Modifiers") + XCTAssertEqual(decoded[2].modifiers, [.declaration, .readonly], "Decoded Modifiers") + XCTAssertEqual(decoded[3].modifiers, [.definition, .async], "Decoded Modifiers") + XCTAssertEqual(decoded[4].modifiers, [], "Decoded Modifiers") } + @MainActor func testOptionB() { + let map = SemanticTokenMap(semanticCapability: .optionB(SemanticTokensRegistrationOptions(legend: testLegend))) + + // Test decode modifiers + let modifierRaw = UInt32(0b1101) + let decodedModifiers = map.decodeModifier(modifierRaw) + XCTAssertEqual([.declaration, .readonly, .async], decodedModifiers) + + // Test decode tokens + let tokens = SemanticTokens(tokens: [ + SemanticToken(line: 0, char: 0, length: 1, type: 0, modifiers: 0b11), // First two indices set + SemanticToken(line: 0, char: 1, length: 2, type: 0, modifiers: 0b100100), // 6th and 3rd indices set + SemanticToken(line: 0, char: 4, length: 1, type: 0b1, modifiers: 0b101), + SemanticToken(line: 0, char: 5, length: 1, type: 0b100, modifiers: 0b1010), + SemanticToken(line: 0, char: 7, length: 10, type: 0, modifiers: 0) + ]) + let decoded = map.decode(tokens: tokens, using: mockProvider) + XCTAssertEqual(decoded.count, 5, "Decoded count") + + XCTAssertEqual(decoded[0].range, NSRange(location: 0, length: 1), "Decoded range") + XCTAssertEqual(decoded[1].range, NSRange(location: 1, length: 2), "Decoded range") + XCTAssertEqual(decoded[2].range, NSRange(location: 4, length: 1), "Decoded range") + XCTAssertEqual(decoded[3].range, NSRange(location: 5, length: 1), "Decoded range") + XCTAssertEqual(decoded[4].range, NSRange(location: 7, length: 10), "Decoded range") + + XCTAssertEqual(decoded[0].capture, nil, "No Decoded Capture") + XCTAssertEqual(decoded[1].capture, nil, "No Decoded Capture") + XCTAssertEqual(decoded[2].capture, .include, "Decoded Capture") + XCTAssertEqual(decoded[3].capture, .keyword, "Decoded Capture") + XCTAssertEqual(decoded[4].capture, nil, "No Decoded Capture") + XCTAssertEqual(decoded[0].modifiers, [.declaration, .definition], "Decoded Modifiers") + XCTAssertEqual(decoded[1].modifiers, [.readonly, .defaultLibrary], "Decoded Modifiers") + XCTAssertEqual(decoded[2].modifiers, [.declaration, .readonly], "Decoded Modifiers") + XCTAssertEqual(decoded[3].modifiers, [.definition, .async], "Decoded Modifiers") + XCTAssertEqual(decoded[4].modifiers, [], "Decoded Modifiers") } } From f694d94cb63d12d83a70431252d3bafce0694d9a Mon Sep 17 00:00:00 2001 From: Khan Winter <35942988+thecoolwinter@users.noreply.github.com> Date: Thu, 19 Dec 2024 13:01:37 -0600 Subject: [PATCH 04/16] Revert EditorInstance --- CodeEdit/Features/Editor/Models/EditorInstance.swift | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/CodeEdit/Features/Editor/Models/EditorInstance.swift b/CodeEdit/Features/Editor/Models/EditorInstance.swift index 6072fa262d..f8aeb8ebc6 100644 --- a/CodeEdit/Features/Editor/Models/EditorInstance.swift +++ b/CodeEdit/Features/Editor/Models/EditorInstance.swift @@ -26,8 +26,7 @@ class EditorInstance: Hashable { // Public TextViewCoordinator APIs - @Published var rangeTranslator: RangeTranslator? - @Published var lspContentCoordinator: LSPContentCoordinator? + var rangeTranslator: RangeTranslator? // Internal Combine subjects From 71bac797106016ea52a3ab6b848a83f65ec0af79 Mon Sep 17 00:00:00 2001 From: Khan Winter <35942988+thecoolwinter@users.noreply.github.com> Date: Thu, 19 Dec 2024 13:23:44 -0600 Subject: [PATCH 05/16] Figure it out --- CodeEdit.xcodeproj/project.pbxproj | 8 ++++---- CodeEdit/AppDelegate.swift | 9 --------- CodeEdit/CodeEditApp.swift | 5 +++++ .../Documents/CodeFileDocument/CodeFileDocument.swift | 3 +++ .../Controllers/CodeEditDocumentController.swift | 2 +- CodeEdit/Features/Editor/Views/CodeFileView.swift | 4 +++- CodeEdit/Features/Editor/Views/EditorAreaView.swift | 6 +----- CodeEdit/Features/LSP/Editor/LSPContentCoordinator.swift | 3 +-- .../Capabilities/LanguageServer+DocumentSync.swift | 9 ++++++++- 9 files changed, 26 insertions(+), 23 deletions(-) diff --git a/CodeEdit.xcodeproj/project.pbxproj b/CodeEdit.xcodeproj/project.pbxproj index bde1bc1ae5..a2cf599a6c 100644 --- a/CodeEdit.xcodeproj/project.pbxproj +++ b/CodeEdit.xcodeproj/project.pbxproj @@ -443,6 +443,7 @@ 6C9619242C3F2809009733CE /* ProjectPath.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C9619232C3F2809009733CE /* ProjectPath.swift */; }; 6C97EBCC2978760400302F95 /* AcknowledgementsWindowController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C97EBCB2978760400302F95 /* AcknowledgementsWindowController.swift */; }; 6C9AE66F2D148DD200FAE8D2 /* URL+FindWorkspace.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C9AE66E2D148DD200FAE8D2 /* URL+FindWorkspace.swift */; }; + 6C9AE6712D14A9F700FAE8D2 /* LazyServiceWrapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C9AE6702D14A9F700FAE8D2 /* LazyServiceWrapper.swift */; }; 6CA1AE952B46950000378EAB /* EditorInstance.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6CA1AE942B46950000378EAB /* EditorInstance.swift */; }; 6CAAF68A29BC9C2300A1F48A /* (null) in Sources */ = {isa = PBXBuildFile; }; 6CAAF69229BCC71C00A1F48A /* (null) in Sources */ = {isa = PBXBuildFile; }; @@ -472,7 +473,6 @@ 6CD26C7B2C8EA8A500ADBA38 /* LSPCache+Data.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6CD26C792C8EA8A500ADBA38 /* LSPCache+Data.swift */; }; 6CD26C7D2C8EA8F400ADBA38 /* LanguageServer+DocumentSync.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6CD26C7C2C8EA8F400ADBA38 /* LanguageServer+DocumentSync.swift */; }; 6CD26C812C8F8A4400ADBA38 /* LanguageIdentifier+CodeLanguage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6CD26C802C8F8A4400ADBA38 /* LanguageIdentifier+CodeLanguage.swift */; }; - 6CD26C872C8F90FD00ADBA38 /* LazyServiceWrapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6CD26C862C8F90FD00ADBA38 /* LazyServiceWrapper.swift */; }; 6CD26C8A2C8F91ED00ADBA38 /* LanguageServer+DocumentTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6CD26C892C8F91ED00ADBA38 /* LanguageServer+DocumentTests.swift */; }; 6CD3CA552C8B508200D83DCD /* CodeEditSourceEditor in Frameworks */ = {isa = PBXBuildFile; productRef = 6CD3CA542C8B508200D83DCD /* CodeEditSourceEditor */; }; 6CDA84AD284C1BA000C1CC3A /* EditorTabBarContextMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6CDA84AC284C1BA000C1CC3A /* EditorTabBarContextMenu.swift */; }; @@ -1128,6 +1128,7 @@ 6C9619262C3F285C009733CE /* CodeEditTestPlan.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; path = CodeEditTestPlan.xctestplan; sourceTree = ""; }; 6C97EBCB2978760400302F95 /* AcknowledgementsWindowController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AcknowledgementsWindowController.swift; sourceTree = ""; }; 6C9AE66E2D148DD200FAE8D2 /* URL+FindWorkspace.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "URL+FindWorkspace.swift"; sourceTree = ""; }; + 6C9AE6702D14A9F700FAE8D2 /* LazyServiceWrapper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LazyServiceWrapper.swift; sourceTree = ""; }; 6CA1AE942B46950000378EAB /* EditorInstance.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditorInstance.swift; sourceTree = ""; }; 6CABB1A029C5593800340467 /* SearchPanelView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SearchPanelView.swift; sourceTree = ""; }; 6CB52DC82AC8DC3E002E75B3 /* CEWorkspaceFileManager+FileManagement.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CEWorkspaceFileManager+FileManagement.swift"; sourceTree = ""; }; @@ -1149,7 +1150,6 @@ 6CD26C792C8EA8A500ADBA38 /* LSPCache+Data.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "LSPCache+Data.swift"; sourceTree = ""; }; 6CD26C7C2C8EA8F400ADBA38 /* LanguageServer+DocumentSync.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "LanguageServer+DocumentSync.swift"; sourceTree = ""; }; 6CD26C802C8F8A4400ADBA38 /* LanguageIdentifier+CodeLanguage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "LanguageIdentifier+CodeLanguage.swift"; sourceTree = ""; }; - 6CD26C862C8F90FD00ADBA38 /* LazyServiceWrapper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LazyServiceWrapper.swift; sourceTree = ""; }; 6CD26C892C8F91ED00ADBA38 /* LanguageServer+DocumentTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "LanguageServer+DocumentTests.swift"; sourceTree = ""; }; 6CDA84AC284C1BA000C1CC3A /* EditorTabBarContextMenu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditorTabBarContextMenu.swift; sourceTree = ""; }; 6CE21E802C643D8F0031B056 /* CETerminalView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CETerminalView.swift; sourceTree = ""; }; @@ -1556,7 +1556,7 @@ 300051662BBD3A5D00A98562 /* ServiceContainer.swift */, 300051692BBD3A8200A98562 /* ServiceType.swift */, 3000516B2BBD3A9500A98562 /* ServiceWrapper.swift */, - 6CD26C862C8F90FD00ADBA38 /* LazyServiceWrapper.swift */, + 6C9AE6702D14A9F700FAE8D2 /* LazyServiceWrapper.swift */, ); path = DependencyInjection; sourceTree = ""; @@ -4371,7 +4371,6 @@ B697937A29FF5668002027EC /* AccountsSettingsAccountLink.swift in Sources */, 5B698A0D2B26327800DE9392 /* SearchSettings.swift in Sources */, B685DE7929CC9CCD002860C8 /* StatusBarIcon.swift in Sources */, - 6CD26C872C8F90FD00ADBA38 /* LazyServiceWrapper.swift in Sources */, 587B9DA629300ABD00AC7927 /* ToolbarBranchPicker.swift in Sources */, 6C6BD6F629CD145F00235D17 /* ExtensionInfo.swift in Sources */, 04BA7C202AE2D92B00584E1C /* GitClient+Status.swift in Sources */, @@ -4533,6 +4532,7 @@ B6AB09B32AB919CF0003A3A6 /* View+actionBar.swift in Sources */, 617DB3DA2C25B07F00B58BFE /* TaskNotificationsDetailView.swift in Sources */, 6C05A8AF284D0CA3007F4EAA /* WorkspaceDocument+Listeners.swift in Sources */, + 6C9AE6712D14A9F700FAE8D2 /* LazyServiceWrapper.swift in Sources */, 588847632992A2A200996D95 /* CEWorkspaceFile.swift in Sources */, 30B088082C0D53080063A882 /* LanguageServer+FoldingRange.swift in Sources */, 6C2C155D29B4F4E500EA60A5 /* SplitViewReader.swift in Sources */, diff --git a/CodeEdit/AppDelegate.swift b/CodeEdit/AppDelegate.swift index 6c77d134b3..7945d1e715 100644 --- a/CodeEdit/AppDelegate.swift +++ b/CodeEdit/AppDelegate.swift @@ -20,7 +20,6 @@ final class AppDelegate: NSObject, NSApplicationDelegate, ObservableObject { @LazyService var lspService: LSPService func applicationDidFinishLaunching(_ notification: Notification) { - setupServiceContainer() enableWindowSizeSaveOnQuit() Settings.shared.preferences.general.appAppearance.applyAppearance() checkForFilesToOpen() @@ -271,14 +270,6 @@ final class AppDelegate: NSObject, NSApplicationDelegate, ObservableObject { workspace.taskManager?.stopAllTasks() } } - - /// Setup all the services into a ServiceContainer for the application to use. - @MainActor - private func setupServiceContainer() { - ServiceContainer.register( - LSPService() - ) - } } extension AppDelegate { diff --git a/CodeEdit/CodeEditApp.swift b/CodeEdit/CodeEditApp.swift index dffcd0ea75..51dd6c2365 100644 --- a/CodeEdit/CodeEditApp.swift +++ b/CodeEdit/CodeEditApp.swift @@ -15,6 +15,11 @@ struct CodeEditApp: App { let updater: SoftwareUpdater = SoftwareUpdater() init() { + // Register singleton services before anything else + ServiceContainer.register( + LSPService() + ) + _ = CodeEditDocumentController.shared NSMenuItem.swizzle() NSSplitViewItem.swizzle() diff --git a/CodeEdit/Features/Documents/CodeFileDocument/CodeFileDocument.swift b/CodeEdit/Features/Documents/CodeFileDocument/CodeFileDocument.swift index 7ef7f7425b..0b82f77806 100644 --- a/CodeEdit/Features/Documents/CodeFileDocument/CodeFileDocument.swift +++ b/CodeEdit/Features/Documents/CodeFileDocument/CodeFileDocument.swift @@ -50,6 +50,9 @@ final class CodeFileDocument: NSDocument, ObservableObject { /// See ``CodeEditSourceEditor/CombineCoordinator``. @Published var contentCoordinator: CombineCoordinator = CombineCoordinator() + /// Set by ``LanguageServer`` when initialized. + @Published var lspCoordinator: LSPContentCoordinator? + /// Used to override detected languages. @Published var language: CodeLanguage? diff --git a/CodeEdit/Features/Documents/Controllers/CodeEditDocumentController.swift b/CodeEdit/Features/Documents/Controllers/CodeEditDocumentController.swift index 5f58b5d3a8..034d3de75e 100644 --- a/CodeEdit/Features/Documents/Controllers/CodeEditDocumentController.swift +++ b/CodeEdit/Features/Documents/Controllers/CodeEditDocumentController.swift @@ -12,7 +12,7 @@ final class CodeEditDocumentController: NSDocumentController { @Environment(\.openWindow) private var openWindow - @LazyService var lspService: LSPService + @Service var lspService: LSPService private let fileManager = FileManager.default diff --git a/CodeEdit/Features/Editor/Views/CodeFileView.swift b/CodeEdit/Features/Editor/Views/CodeFileView.swift index 1d2f50961c..a1d4c95516 100644 --- a/CodeEdit/Features/Editor/Views/CodeFileView.swift +++ b/CodeEdit/Features/Editor/Views/CodeFileView.swift @@ -57,7 +57,9 @@ struct CodeFileView: View { init(codeFile: CodeFileDocument, textViewCoordinators: [TextViewCoordinator] = [], isEditable: Bool = true) { self._codeFile = .init(wrappedValue: codeFile) self.isEditable = isEditable - self.textViewCoordinators = textViewCoordinators + [codeFile.contentCoordinator] + self.textViewCoordinators = textViewCoordinators + + [codeFile.contentCoordinator] + + [codeFile.lspCoordinator].compactMap({ $0 }) if let openOptions = codeFile.openOptions { codeFile.openOptions = nil diff --git a/CodeEdit/Features/Editor/Views/EditorAreaView.swift b/CodeEdit/Features/Editor/Views/EditorAreaView.swift index 0f79c78b37..421adf9d69 100644 --- a/CodeEdit/Features/Editor/Views/EditorAreaView.swift +++ b/CodeEdit/Features/Editor/Views/EditorAreaView.swift @@ -7,7 +7,6 @@ import SwiftUI import CodeEditTextView -import CodeEditSourceEditor struct EditorAreaView: View { @AppSettings(\.general.showEditorPathBar) @@ -53,10 +52,7 @@ struct EditorAreaView: View { if let codeFile = codeFile { EditorAreaFileView( codeFile: codeFile, - textViewCoordinators: [ - selected.rangeTranslator as Any, - selected.lspContentCoordinator as Any - ].compactMap({ $0 as? any TextViewCoordinator }) + textViewCoordinators: [selected.rangeTranslator].compactMap({ $0 }) ) .focusedObject(editor) .transformEnvironment(\.edgeInsets) { insets in diff --git a/CodeEdit/Features/LSP/Editor/LSPContentCoordinator.swift b/CodeEdit/Features/LSP/Editor/LSPContentCoordinator.swift index 7764ad5302..dc17481e61 100644 --- a/CodeEdit/Features/LSP/Editor/LSPContentCoordinator.swift +++ b/CodeEdit/Features/LSP/Editor/LSPContentCoordinator.swift @@ -36,7 +36,7 @@ class LSPContentCoordinator: TextViewCoordinator, TextViewDelegate { var documentURI: String /// Initializes a content coordinator, and begins an async stream of updates - init(documentURI: String, languageServer: LanguageServer?) { + init(documentURI: String, languageServer: LanguageServer) { self.documentURI = documentURI self.languageServer = languageServer self.stream = AsyncStream { continuation in @@ -74,7 +74,6 @@ class LSPContentCoordinator: TextViewCoordinator, TextViewDelegate { } func textView(_ textView: TextView, didReplaceContentsIn range: NSRange, with string: String) { - print("didReplaceContents") guard let lspRange = editedRange else { return } diff --git a/CodeEdit/Features/LSP/LanguageServer/Capabilities/LanguageServer+DocumentSync.swift b/CodeEdit/Features/LSP/LanguageServer/Capabilities/LanguageServer+DocumentSync.swift index b9bd8b63c2..4ec0aaa8dd 100644 --- a/CodeEdit/Features/LSP/LanguageServer/Capabilities/LanguageServer+DocumentSync.swift +++ b/CodeEdit/Features/LSP/LanguageServer/Capabilities/LanguageServer+DocumentSync.swift @@ -19,7 +19,7 @@ extension LanguageServer { } logger.debug("Opening Document \(content.uri, privacy: .private)") - self.openFiles.addDocument(document, for: self) + openFiles.addDocument(document, for: self) let textDocument = TextDocumentItem( uri: content.uri, @@ -28,6 +28,8 @@ extension LanguageServer { text: content.string ) try await lspInstance.textDocumentDidOpen(DidOpenTextDocumentParams(textDocument: textDocument)) + + await updateIsolatedDocument(document, coordinator: openFiles.contentCoordinator(for: document)) } catch { logger.warning("addDocument: Error \(error)") throw error @@ -117,6 +119,11 @@ extension LanguageServer { return DocumentContent(uri: uri, language: language, string: content) } + @MainActor + private func updateIsolatedDocument(_ document: CodeFileDocument, coordinator: LSPContentCoordinator?) { + document.lspCoordinator = coordinator + } + // swiftlint:disable line_length /// Determines the type of document sync the server supports. /// https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_synchronization_sc From 775b3464740169be1b08ef271881a8d32713df61 Mon Sep 17 00:00:00 2001 From: Khan Winter <35942988+thecoolwinter@users.noreply.github.com> Date: Thu, 19 Dec 2024 13:24:53 -0600 Subject: [PATCH 06/16] Code Style --- CodeEdit/Features/Editor/Views/CodeFileView.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CodeEdit/Features/Editor/Views/CodeFileView.swift b/CodeEdit/Features/Editor/Views/CodeFileView.swift index a1d4c95516..ba1fbe6603 100644 --- a/CodeEdit/Features/Editor/Views/CodeFileView.swift +++ b/CodeEdit/Features/Editor/Views/CodeFileView.swift @@ -56,10 +56,10 @@ struct CodeFileView: View { init(codeFile: CodeFileDocument, textViewCoordinators: [TextViewCoordinator] = [], isEditable: Bool = true) { self._codeFile = .init(wrappedValue: codeFile) - self.isEditable = isEditable self.textViewCoordinators = textViewCoordinators + [codeFile.contentCoordinator] + [codeFile.lspCoordinator].compactMap({ $0 }) + self.isEditable = isEditable if let openOptions = codeFile.openOptions { codeFile.openOptions = nil From aff4a3b4eea77c2e09fd9c95d9433c12eaedad98 Mon Sep 17 00:00:00 2001 From: Khan Winter <35942988+thecoolwinter@users.noreply.github.com> Date: Thu, 19 Dec 2024 13:25:21 -0600 Subject: [PATCH 07/16] Rename To Map --- CodeEdit.xcodeproj/project.pbxproj | 8 ++++---- ...geServerFileData.swift => LanguageServerFileMap.swift} | 0 2 files changed, 4 insertions(+), 4 deletions(-) rename CodeEdit/Features/LSP/LanguageServer/{LanguageServerFileData.swift => LanguageServerFileMap.swift} (100%) diff --git a/CodeEdit.xcodeproj/project.pbxproj b/CodeEdit.xcodeproj/project.pbxproj index a2cf599a6c..bf5c71f978 100644 --- a/CodeEdit.xcodeproj/project.pbxproj +++ b/CodeEdit.xcodeproj/project.pbxproj @@ -467,7 +467,7 @@ 6CC9E4B229B5669900C97388 /* Environment+ActiveEditor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6CC9E4B129B5669900C97388 /* Environment+ActiveEditor.swift */; }; 6CD0358A2C3461160091E1F4 /* KeyWindowControllerObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6CD035892C3461160091E1F4 /* KeyWindowControllerObserver.swift */; }; 6CD03B6A29FC773F001BD1D0 /* SettingsInjector.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6CD03B6929FC773F001BD1D0 /* SettingsInjector.swift */; }; - 6CD26C6E2C8EA1E600ADBA38 /* LanguageServerFileData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6CD26C6D2C8EA1E600ADBA38 /* LanguageServerFileData.swift */; }; + 6CD26C6E2C8EA1E600ADBA38 /* LanguageServerFileMap.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6CD26C6D2C8EA1E600ADBA38 /* LanguageServerFileMap.swift */; }; 6CD26C772C8EA83900ADBA38 /* URL+absolutePath.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6CD26C762C8EA83900ADBA38 /* URL+absolutePath.swift */; }; 6CD26C7A2C8EA8A500ADBA38 /* LSPCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6CD26C782C8EA8A500ADBA38 /* LSPCache.swift */; }; 6CD26C7B2C8EA8A500ADBA38 /* LSPCache+Data.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6CD26C792C8EA8A500ADBA38 /* LSPCache+Data.swift */; }; @@ -1144,7 +1144,7 @@ 6CC9E4B129B5669900C97388 /* Environment+ActiveEditor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Environment+ActiveEditor.swift"; sourceTree = ""; }; 6CD035892C3461160091E1F4 /* KeyWindowControllerObserver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyWindowControllerObserver.swift; sourceTree = ""; }; 6CD03B6929FC773F001BD1D0 /* SettingsInjector.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsInjector.swift; sourceTree = ""; }; - 6CD26C6D2C8EA1E600ADBA38 /* LanguageServerFileData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LanguageServerFileData.swift; sourceTree = ""; }; + 6CD26C6D2C8EA1E600ADBA38 /* LanguageServerFileMap.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LanguageServerFileMap.swift; sourceTree = ""; }; 6CD26C762C8EA83900ADBA38 /* URL+absolutePath.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "URL+absolutePath.swift"; sourceTree = ""; }; 6CD26C782C8EA8A500ADBA38 /* LSPCache.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LSPCache.swift; sourceTree = ""; }; 6CD26C792C8EA8A500ADBA38 /* LSPCache+Data.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "LSPCache+Data.swift"; sourceTree = ""; }; @@ -3091,7 +3091,7 @@ isa = PBXGroup; children = ( 30B087F72C0D53080063A882 /* LanguageServer.swift */, - 6CD26C6D2C8EA1E600ADBA38 /* LanguageServerFileData.swift */, + 6CD26C6D2C8EA1E600ADBA38 /* LanguageServerFileMap.swift */, 6CD26C782C8EA8A500ADBA38 /* LSPCache.swift */, 6CD26C792C8EA8A500ADBA38 /* LSPCache+Data.swift */, 30B0881E2C12626B0063A882 /* Capabilities */, @@ -4012,7 +4012,7 @@ 3000516C2BBD3A9500A98562 /* ServiceWrapper.swift in Sources */, 587B9DA029300ABD00AC7927 /* PanelDivider.swift in Sources */, 201169E52837B40300F92B46 /* SourceControlNavigatorRepositoryView.swift in Sources */, - 6CD26C6E2C8EA1E600ADBA38 /* LanguageServerFileData.swift in Sources */, + 6CD26C6E2C8EA1E600ADBA38 /* LanguageServerFileMap.swift in Sources */, 587B9E6A29301D8F00AC7927 /* GitLabPermissions.swift in Sources */, B6EA1FF529DA380E001BF195 /* TextEditingSettingsView.swift in Sources */, D7DC4B76298FFBE900D6C83D /* ProjectNavigatorViewController+OutlineTableViewCellDelegate.swift in Sources */, diff --git a/CodeEdit/Features/LSP/LanguageServer/LanguageServerFileData.swift b/CodeEdit/Features/LSP/LanguageServer/LanguageServerFileMap.swift similarity index 100% rename from CodeEdit/Features/LSP/LanguageServer/LanguageServerFileData.swift rename to CodeEdit/Features/LSP/LanguageServer/LanguageServerFileMap.swift From 03579ecae8fd3f97b2df5eb65bab63dd5135aafd Mon Sep 17 00:00:00 2001 From: Khan Winter <35942988+thecoolwinter@users.noreply.github.com> Date: Thu, 19 Dec 2024 13:26:07 -0600 Subject: [PATCH 08/16] Remove DEBUG Print --- CodeEditTests/Features/LSP/BufferingServerConnection.swift | 1 - 1 file changed, 1 deletion(-) diff --git a/CodeEditTests/Features/LSP/BufferingServerConnection.swift b/CodeEditTests/Features/LSP/BufferingServerConnection.swift index 8a69de26f0..fcd276b8a1 100644 --- a/CodeEditTests/Features/LSP/BufferingServerConnection.swift +++ b/CodeEditTests/Features/LSP/BufferingServerConnection.swift @@ -28,7 +28,6 @@ class BufferingServerConnection: ServerConnection { } func sendNotification(_ notif: ClientNotification) async throws { - print("NOTIFICATION", notif.method) clientNotifications.append(notif) clientEventContinuation.yield((clientRequests, clientNotifications)) } From f875671f76bd8a8dc9d7eac66de4291fe0c2b955 Mon Sep 17 00:00:00 2001 From: Khan Winter <35942988+thecoolwinter@users.noreply.github.com> Date: Thu, 19 Dec 2024 13:28:26 -0600 Subject: [PATCH 09/16] Explain BufferingServerConnection --- .../Features/LSP/BufferingServerConnection.swift | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/CodeEditTests/Features/LSP/BufferingServerConnection.swift b/CodeEditTests/Features/LSP/BufferingServerConnection.swift index fcd276b8a1..4fabf0a4fc 100644 --- a/CodeEditTests/Features/LSP/BufferingServerConnection.swift +++ b/CodeEditTests/Features/LSP/BufferingServerConnection.swift @@ -10,12 +10,20 @@ import LanguageClient import LanguageServerProtocol import JSONRPC +/// Mock server connection that retains all requests and notifications in an array for comparing later. +/// +/// To listen for changes, this type produces an async stream of all requests and notifications. Use the +/// `clientEventSequence` sequence to receive a copy of both whenever they're updated. +/// class BufferingServerConnection: ServerConnection { typealias ClientEventSequence = AsyncStream<([ClientRequest], [ClientNotification])> - var eventSequence: EventSequence - var clientEventSequence: ClientEventSequence + public var eventSequence: EventSequence + + /// A sequence of all events. + public var clientEventSequence: ClientEventSequence private var clientEventContinuation: ClientEventSequence.Continuation + private var id = 0 public var clientRequests: [ClientRequest] = [] From 5e1ec7add5344de0a5ed76a0030642f67bdfb7f5 Mon Sep 17 00:00:00 2001 From: Khan Winter <35942988+thecoolwinter@users.noreply.github.com> Date: Thu, 19 Dec 2024 13:33:08 -0600 Subject: [PATCH 10/16] Docs --- CodeEdit/Features/LSP/Editor/SemanticTokenMap.swift | 12 +++++++----- .../LSP/Editor/SemanticTokenMapRangeProvider.swift | 1 + 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/CodeEdit/Features/LSP/Editor/SemanticTokenMap.swift b/CodeEdit/Features/LSP/Editor/SemanticTokenMap.swift index a5ab2ae958..5a196cf60f 100644 --- a/CodeEdit/Features/LSP/Editor/SemanticTokenMap.swift +++ b/CodeEdit/Features/LSP/Editor/SemanticTokenMap.swift @@ -11,14 +11,14 @@ import CodeEditSourceEditor import CodeEditTextView // swiftlint:disable line_length -/// Creates a mapping from a language server's semantic token options to a format readable by CodeEdit +/// Creates a mapping from a language server's semantic token options to a format readable by CodeEdit. /// Provides a convenience method for mapping tokens received from the server to highlight ranges suitable for -/// highlighting in the editor +/// highlighting in the editor. /// /// Use this type to handle the initially received semantic highlight capabilities structures. This type will figure /// out how to read it into a format it can use. /// -/// After initialization, the map it static (until the server is reinitialized). Similarly, this type is `Sendable` +/// After initialization, the map is static until the server is reinitialized. Consequently, this type is `Sendable` /// and immutable after initialization. /// /// This type is not coupled to any text system via the use of the ``SemanticTokenMapRangeProvider``. When decoding to @@ -43,7 +43,7 @@ struct SemanticTokenMap: Sendable { // swiftlint:enable line_length } /// Decodes the compressed semantic token data into a `HighlightRange` type for use in an editor. - /// This is run on the main actor to prevent runtime errors, due to the use of the actor-isolated `textView`. + /// This is marked main actor to prevent runtime errors, due to the use of the actor-isolated `rangeProvider`. /// - Parameters: /// - tokens: Semantic tokens from a language server. /// - rangeProvider: The provider to use to translate token ranges to text view ranges. @@ -56,7 +56,9 @@ struct SemanticTokenMap: Sendable { // swiftlint:enable line_length } let modifiers = decodeModifier(token.modifiers) - let type = token.type > 0 ? Int(token.type.trailingZeroBitCount) : -1 // Don't decode 0 + + // Capture types are indicated by the index of the set bit. + let type = token.type > 0 ? Int(token.type.trailingZeroBitCount) : -1 // Don't try to decode 0 let capture = tokenTypeMap.indices.contains(type) ? tokenTypeMap[type] : nil return HighlightRange( diff --git a/CodeEdit/Features/LSP/Editor/SemanticTokenMapRangeProvider.swift b/CodeEdit/Features/LSP/Editor/SemanticTokenMapRangeProvider.swift index ac0d746973..c8ab4a40cb 100644 --- a/CodeEdit/Features/LSP/Editor/SemanticTokenMapRangeProvider.swift +++ b/CodeEdit/Features/LSP/Editor/SemanticTokenMapRangeProvider.swift @@ -7,6 +7,7 @@ import Foundation +@MainActor protocol SemanticTokenMapRangeProvider { func nsRangeFrom(line: UInt32, char: UInt32, length: UInt32) -> NSRange? } From b9dc742fdf7c372d309e8224596acabb1f19d336 Mon Sep 17 00:00:00 2001 From: Khan Winter <35942988+thecoolwinter@users.noreply.github.com> Date: Thu, 19 Dec 2024 13:37:01 -0600 Subject: [PATCH 11/16] Restore LSPService MainActor --- CodeEdit/Features/LSP/Service/LSPService.swift | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/CodeEdit/Features/LSP/Service/LSPService.swift b/CodeEdit/Features/LSP/Service/LSPService.swift index e872557c76..8afc505acc 100644 --- a/CodeEdit/Features/LSP/Service/LSPService.swift +++ b/CodeEdit/Features/LSP/Service/LSPService.swift @@ -97,6 +97,7 @@ import CodeEditLanguages /// } /// } /// ``` +@MainActor final class LSPService: ObservableObject { let logger: Logger = Logger(subsystem: Bundle.main.bundleIdentifier ?? "", category: "LSPService") @@ -137,8 +138,10 @@ final class LSPService: ObservableObject { object: nil, queue: .main ) { notification in - guard let document = notification.object as? CodeFileDocument else { return } - self.openDocument(document) + MainActor.assumeIsolated { + guard let document = notification.object as? CodeFileDocument else { return } + self.openDocument(document) + } } NotificationCenter.default.addObserver( @@ -146,8 +149,10 @@ final class LSPService: ObservableObject { object: nil, queue: .main ) { notification in - guard let url = notification.object as? URL else { return } - self.closeDocument(url) + MainActor.assumeIsolated { + guard let url = notification.object as? URL else { return } + self.closeDocument(url) + } } } From fc40ffdebfdf63f12b987c4ba2f33a7902521979 Mon Sep 17 00:00:00 2001 From: Khan Winter <35942988+thecoolwinter@users.noreply.github.com> Date: Thu, 19 Dec 2024 13:37:38 -0600 Subject: [PATCH 12/16] Rename Back to Map --- CodeEdit/Features/LSP/LanguageServer/LanguageServer.swift | 4 ++-- .../Features/LSP/LanguageServer/LanguageServerFileMap.swift | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/CodeEdit/Features/LSP/LanguageServer/LanguageServer.swift b/CodeEdit/Features/LSP/LanguageServer/LanguageServer.swift index 676bc4dcab..e082ee93fe 100644 --- a/CodeEdit/Features/LSP/LanguageServer/LanguageServer.swift +++ b/CodeEdit/Features/LSP/LanguageServer/LanguageServer.swift @@ -25,7 +25,7 @@ class LanguageServer { /// Tracks documents and their associated objects. /// Use this property when adding new objects that need to track file data, or have a state associated with the /// language server and a document. For example, the content coordinator. - let openFiles: LanguageServerFileData + let openFiles: LanguageServerFileMap /// Maps the language server's highlight config to one CodeEdit can read. See ``SemanticTokenMap``. let highlightMap: SemanticTokenMap? @@ -50,7 +50,7 @@ class LanguageServer { self.lspInstance = lspInstance self.serverCapabilities = serverCapabilities self.rootPath = rootPath - self.openFiles = LanguageServerFileData() + self.openFiles = LanguageServerFileMap() self.logger = Logger( subsystem: Bundle.main.bundleIdentifier ?? "", category: "LanguageServer.\(languageId.rawValue)" diff --git a/CodeEdit/Features/LSP/LanguageServer/LanguageServerFileMap.swift b/CodeEdit/Features/LSP/LanguageServer/LanguageServerFileMap.swift index f46d02eda8..f08ddfeac5 100644 --- a/CodeEdit/Features/LSP/LanguageServer/LanguageServerFileMap.swift +++ b/CodeEdit/Features/LSP/LanguageServer/LanguageServerFileMap.swift @@ -9,7 +9,7 @@ import Foundation import LanguageServerProtocol /// Tracks data associated with files and language servers. -class LanguageServerFileData { +class LanguageServerFileMap { /// Extend this struct as more objects are associated with a code document. private struct DocumentObject { let uri: String From 4164d53797344b7c4510406607bfe6ead99fe364 Mon Sep 17 00:00:00 2001 From: Khan Winter <35942988+thecoolwinter@users.noreply.github.com> Date: Thu, 19 Dec 2024 13:38:23 -0600 Subject: [PATCH 13/16] Remove Unnecessary Deinit --- CodeEdit/Features/LSP/LanguageServer/LanguageServer.swift | 4 ---- 1 file changed, 4 deletions(-) diff --git a/CodeEdit/Features/LSP/LanguageServer/LanguageServer.swift b/CodeEdit/Features/LSP/LanguageServer/LanguageServer.swift index e082ee93fe..eab8be5504 100644 --- a/CodeEdit/Features/LSP/LanguageServer/LanguageServer.swift +++ b/CodeEdit/Features/LSP/LanguageServer/LanguageServer.swift @@ -234,10 +234,6 @@ class LanguageServer { self.logger.info("Shutting down language server") try await lspInstance.shutdownAndExit() } - - deinit { - NotificationCenter.default.removeObserver(self) - } } /// Represents a language server binary. From db5327c6f9b29106672704e516f32aceff4d4bb4 Mon Sep 17 00:00:00 2001 From: Khan Winter <35942988+thecoolwinter@users.noreply.github.com> Date: Thu, 19 Dec 2024 13:39:39 -0600 Subject: [PATCH 14/16] Revert languageServerURI Name Change --- .../Documents/CodeFileDocument/CodeFileDocument.swift | 4 ++-- .../Capabilities/LanguageServer+DocumentSync.swift | 2 +- .../LSP/LanguageServer/LanguageServerFileMap.swift | 10 +++++----- CodeEdit/Features/LSP/Service/LSPService.swift | 2 +- 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/CodeEdit/Features/Documents/CodeFileDocument/CodeFileDocument.swift b/CodeEdit/Features/Documents/CodeFileDocument/CodeFileDocument.swift index 0b82f77806..4e1638b9d6 100644 --- a/CodeEdit/Features/Documents/CodeFileDocument/CodeFileDocument.swift +++ b/CodeEdit/Features/Documents/CodeFileDocument/CodeFileDocument.swift @@ -83,8 +83,8 @@ final class CodeFileDocument: NSDocument, ObservableObject { return type } - /// Use when identifying documents globally on the user's computer, eg with a language server. - var absolutePath: String? { fileURL?.absolutePath } + /// A stable string to use when identifying documents with language servers. + var languageServerURI: String? { fileURL?.absolutePath } /// Specify options for opening the file such as the initial cursor positions. /// Nulled by ``CodeFileView`` on first load. diff --git a/CodeEdit/Features/LSP/LanguageServer/Capabilities/LanguageServer+DocumentSync.swift b/CodeEdit/Features/LSP/LanguageServer/Capabilities/LanguageServer+DocumentSync.swift index 4ec0aaa8dd..563604aa7e 100644 --- a/CodeEdit/Features/LSP/LanguageServer/Capabilities/LanguageServer+DocumentSync.swift +++ b/CodeEdit/Features/LSP/LanguageServer/Capabilities/LanguageServer+DocumentSync.swift @@ -111,7 +111,7 @@ extension LanguageServer { /// Helper function for grabbing a document's content from the main actor. @MainActor private func getIsolatedDocumentContent(_ document: CodeFileDocument) -> DocumentContent? { - guard let uri = document.absolutePath, + guard let uri = document.languageServerURI, let language = document.getLanguage().lspLanguage, let content = document.content?.string else { return nil diff --git a/CodeEdit/Features/LSP/LanguageServer/LanguageServerFileMap.swift b/CodeEdit/Features/LSP/LanguageServer/LanguageServerFileMap.swift index f08ddfeac5..c681e894a6 100644 --- a/CodeEdit/Features/LSP/LanguageServer/LanguageServerFileMap.swift +++ b/CodeEdit/Features/LSP/LanguageServer/LanguageServerFileMap.swift @@ -27,7 +27,7 @@ class LanguageServerFileMap { // MARK: - Track & Remove Documents func addDocument(_ document: CodeFileDocument, for server: LanguageServer) { - guard let uri = document.absolutePath else { return } + guard let uri = document.languageServerURI else { return } trackedDocuments.setObject(document, forKey: uri as NSString) trackedDocumentData[uri] = DocumentObject( uri: uri, @@ -42,7 +42,7 @@ class LanguageServerFileMap { } func removeDocument(for document: CodeFileDocument) { - guard let uri = document.absolutePath else { return } + guard let uri = document.languageServerURI else { return } removeDocument(for: uri) } @@ -54,7 +54,7 @@ class LanguageServerFileMap { // MARK: - Version Number Tracking func incrementVersion(for document: CodeFileDocument) -> Int { - guard let uri = document.absolutePath else { return 0 } + guard let uri = document.languageServerURI else { return 0 } return incrementVersion(for: uri) } @@ -64,7 +64,7 @@ class LanguageServerFileMap { } func documentVersion(for document: CodeFileDocument) -> Int? { - guard let uri = document.absolutePath else { return nil } + guard let uri = document.languageServerURI else { return nil } return documentVersion(for: uri) } @@ -75,7 +75,7 @@ class LanguageServerFileMap { // MARK: - Content Coordinator func contentCoordinator(for document: CodeFileDocument) -> LSPContentCoordinator? { - guard let uri = document.absolutePath else { return nil } + guard let uri = document.languageServerURI else { return nil } return contentCoordinator(for: uri) } diff --git a/CodeEdit/Features/LSP/Service/LSPService.swift b/CodeEdit/Features/LSP/Service/LSPService.swift index 8afc505acc..2eaab98d69 100644 --- a/CodeEdit/Features/LSP/Service/LSPService.swift +++ b/CodeEdit/Features/LSP/Service/LSPService.swift @@ -218,7 +218,7 @@ final class LSPService: ObservableObject { do { try await languageServer.openDocument(document) } catch { - let uri = await document.absolutePath + let uri = await document.languageServerURI // swiftlint:disable:next line_length self.logger.error("Failed to close document: \(uri ?? "", privacy: .private), language: \(lspLanguage.rawValue). Error \(error)") } From ca9fb94f367e0b1edb9e9fc3be98349ecf63a447 Mon Sep 17 00:00:00 2001 From: Khan Winter <35942988+thecoolwinter@users.noreply.github.com> Date: Thu, 19 Dec 2024 14:00:51 -0600 Subject: [PATCH 15/16] Fix MainActor Build Error --- CodeEditTests/Features/LSP/SemanticTokenMapTests.swift | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/CodeEditTests/Features/LSP/SemanticTokenMapTests.swift b/CodeEditTests/Features/LSP/SemanticTokenMapTests.swift index 0e4be8b7f6..4c941de1a4 100644 --- a/CodeEditTests/Features/LSP/SemanticTokenMapTests.swift +++ b/CodeEditTests/Features/LSP/SemanticTokenMapTests.swift @@ -36,7 +36,11 @@ final class SemanticTokenMapTestsTests: XCTestCase { "defaultLibrary" ] ) - let mockProvider = MockRangeProvider() + var mockProvider: MockRangeProvider! + + override func setUp() async throws { + mockProvider = await MockRangeProvider() + } @MainActor func testOptionA() { From cdb9fe2f7b03d307d1a4423cc19fc08e9f222948 Mon Sep 17 00:00:00 2001 From: Khan Winter <35942988+thecoolwinter@users.noreply.github.com> Date: Fri, 20 Dec 2024 16:23:04 -0600 Subject: [PATCH 16/16] Fix Tests --- .../Features/LSP/LanguageServer+DocumentTests.swift | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/CodeEditTests/Features/LSP/LanguageServer+DocumentTests.swift b/CodeEditTests/Features/LSP/LanguageServer+DocumentTests.swift index 244fa06e3c..e4a726b57b 100644 --- a/CodeEditTests/Features/LSP/LanguageServer+DocumentTests.swift +++ b/CodeEditTests/Features/LSP/LanguageServer+DocumentTests.swift @@ -92,6 +92,9 @@ final class LanguageServerDocumentTests: XCTestCase { ofType: "public.swift-source" ) + // This is usually sent from the LSPService + try await server.openDocument(codeFile) + await waitForClientEventCount( 3, connection: connection, @@ -101,7 +104,6 @@ final class LanguageServerDocumentTests: XCTestCase { // Set up full content changes server.serverCapabilities = ServerCapabilities() server.serverCapabilities.textDocumentSync = syncOption - server.openFiles.addDocument(codeFile, for: server) return codeFile } @@ -127,6 +129,7 @@ final class LanguageServerDocumentTests: XCTestCase { // Set up test server let (connection, server) = try await makeTestServer() + // This service should receive the didOpen/didClose notifications let lspService = ServiceContainer.resolve(.singleton, LSPService.self) await MainActor.run { lspService?.languageClients[.init(.swift, tempTestDir.path() + "/")] = server } @@ -148,8 +151,6 @@ final class LanguageServerDocumentTests: XCTestCase { ofType: "public.swift-source" ) file.fileDocument = codeFile - - // This should trigger a documentDidOpen event CodeEditDocumentController.shared.addDocument(codeFile) await waitForClientEventCount(3, connection: connection, description: "Pre-close event count")