From ab126d5da8b87f1577f639d73d510dc8359e63a7 Mon Sep 17 00:00:00 2001 From: Abe M Date: Sat, 19 Oct 2024 22:31:39 -0700 Subject: [PATCH 1/8] Autocomplete Coordinator --- CodeEdit.xcodeproj/project.pbxproj | 50 ++++---- .../xcshareddata/swiftpm/Package.resolved | 34 ++---- .../CodeFileDocument/CodeFileDocument.swift | 1 - .../Editor/AutoCompleteCoordinator.swift | 114 ++++++++++++++++++ .../Features/Editor/Views/CodeFileView.swift | 6 +- .../LanguageServerFileMap.swift | 4 +- .../Features/LSP/Views/CompletionItem.swift | 30 +++++ .../LSP/Views/CompletionItemKind.swift | 85 +++++++++++++ 8 files changed, 268 insertions(+), 56 deletions(-) create mode 100644 CodeEdit/Features/Editor/AutoCompleteCoordinator.swift create mode 100644 CodeEdit/Features/LSP/Views/CompletionItem.swift create mode 100644 CodeEdit/Features/LSP/Views/CompletionItemKind.swift diff --git a/CodeEdit.xcodeproj/project.pbxproj b/CodeEdit.xcodeproj/project.pbxproj index 0d697ab9f..c916577a0 100644 --- a/CodeEdit.xcodeproj/project.pbxproj +++ b/CodeEdit.xcodeproj/project.pbxproj @@ -3,7 +3,7 @@ archiveVersion = 1; classes = { }; - objectVersion = 55; + objectVersion = 70; objects = { /* Begin PBXBuildFile section */ @@ -64,6 +64,7 @@ 3000516A2BBD3A8200A98562 /* ServiceType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 300051692BBD3A8200A98562 /* ServiceType.swift */; }; 3000516C2BBD3A9500A98562 /* ServiceWrapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3000516B2BBD3A9500A98562 /* ServiceWrapper.swift */; }; 3026F50F2AC006C80061227E /* InspectorAreaViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3026F50E2AC006C80061227E /* InspectorAreaViewModel.swift */; }; + 3046374E2CB15FA900180667 /* AutoCompleteCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3046374D2CB15F9200180667 /* AutoCompleteCoordinator.swift */; }; 30AB4EBB2BF718A100ED4431 /* DeveloperSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30AB4EBA2BF718A100ED4431 /* DeveloperSettings.swift */; }; 30AB4EBD2BF71CA800ED4431 /* DeveloperSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30AB4EBC2BF71CA800ED4431 /* DeveloperSettingsView.swift */; }; 30AB4EC22BF7253200ED4431 /* KeyValueTable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30AB4EC12BF7253200ED4431 /* KeyValueTable.swift */; }; @@ -463,10 +464,10 @@ 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 */; }; - 6CD26C852C8F907800ADBA38 /* CodeEditSourceEditor in Frameworks */ = {isa = PBXBuildFile; productRef = 6CD26C842C8F907800ADBA38 /* CodeEditSourceEditor */; }; + 6CD26C852C8F907800ADBA38 /* (null) in Frameworks */ = {isa = PBXBuildFile; }; 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 */; }; + 6CD3CA552C8B508200D83DCD /* (null) in Frameworks */ = {isa = PBXBuildFile; }; 6CDA84AD284C1BA000C1CC3A /* EditorTabBarContextMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6CDA84AC284C1BA000C1CC3A /* EditorTabBarContextMenu.swift */; }; 6CE21E812C643D8F0031B056 /* CETerminalView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6CE21E802C643D8F0031B056 /* CETerminalView.swift */; }; 6CE21E872C650D2C0031B056 /* SwiftTerm in Frameworks */ = {isa = PBXBuildFile; productRef = 6CE21E862C650D2C0031B056 /* SwiftTerm */; }; @@ -742,6 +743,7 @@ 300051692BBD3A8200A98562 /* ServiceType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServiceType.swift; sourceTree = ""; }; 3000516B2BBD3A9500A98562 /* ServiceWrapper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServiceWrapper.swift; sourceTree = ""; }; 3026F50E2AC006C80061227E /* InspectorAreaViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InspectorAreaViewModel.swift; sourceTree = ""; }; + 3046374D2CB15F9200180667 /* AutoCompleteCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AutoCompleteCoordinator.swift; sourceTree = ""; }; 30AB4EBA2BF718A100ED4431 /* DeveloperSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeveloperSettings.swift; sourceTree = ""; }; 30AB4EBC2BF71CA800ED4431 /* DeveloperSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeveloperSettingsView.swift; sourceTree = ""; }; 30AB4EC12BF7253200ED4431 /* KeyValueTable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyValueTable.swift; sourceTree = ""; }; @@ -1284,6 +1286,10 @@ EC0870F62A455F6400EB8692 /* ProjectNavigatorViewController+NSMenuDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ProjectNavigatorViewController+NSMenuDelegate.swift"; sourceTree = ""; }; /* End PBXFileReference section */ +/* Begin PBXFileSystemSynchronizedRootGroup section */ + 302EFC1F2CC3C034004A74DF /* Views */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); path = Views; sourceTree = ""; }; +/* End PBXFileSystemSynchronizedRootGroup section */ + /* Begin PBXFrameworksBuildPhase section */ 2BE487E928245162003F3F64 /* Frameworks */ = { isa = PBXFrameworksBuildPhase; @@ -1301,7 +1307,7 @@ 58F2EB1E292FB954004A9BDE /* Sparkle in Frameworks */, 6C147C4529A329350089B630 /* OrderedCollections in Frameworks */, 6CE21E872C650D2C0031B056 /* SwiftTerm in Frameworks */, - 6CD3CA552C8B508200D83DCD /* CodeEditSourceEditor in Frameworks */, + 6CD3CA552C8B508200D83DCD /* (null) in Frameworks */, 6C0617D62BDB4432008C9C42 /* LogStream in Frameworks */, 6CC17B4F2C432AE000834E2C /* CodeEditSourceEditor in Frameworks */, 30CB64912C16CA8100CC8A9E /* LanguageServerProtocol in Frameworks */, @@ -1310,7 +1316,7 @@ 6C85BB442C210EFD00EB5DEF /* SwiftUIIntrospect in Frameworks */, 6CB446402B6DFF3A00539ED0 /* CodeEditSourceEditor in Frameworks */, 2816F594280CF50500DD548B /* CodeEditSymbols in Frameworks */, - 6CD26C852C8F907800ADBA38 /* CodeEditSourceEditor in Frameworks */, + 6CD26C852C8F907800ADBA38 /* (null) in Frameworks */, 30CB64942C16CA9100CC8A9E /* LanguageClient in Frameworks */, 6C6BD6F829CD14D100235D17 /* CodeEditKit in Frameworks */, 6C0824A12C5C0C9700A0751E /* SwiftTerm in Frameworks */, @@ -1560,6 +1566,7 @@ children = ( 6CD26C732C8EA71F00ADBA38 /* LanguageServer */, 6CD26C742C8EA79100ADBA38 /* Service */, + 302EFC1F2CC3C034004A74DF /* Views */, 30B087FA2C0D53080063A882 /* LSPUtil.swift */, ); path = LSP; @@ -2823,6 +2830,7 @@ 287776EB27E350BA00D46668 /* TabBar */, B67660642AA970ED00CD56B0 /* Models */, B67660632AA970E300CD56B0 /* Views */, + 3046374D2CB15F9200180667 /* AutoCompleteCoordinator.swift */, ); path = Editor; sourceTree = ""; @@ -3675,6 +3683,9 @@ 6C7B1C762A1D57CE005CBBFC /* PBXTargetDependency */, 2BE487F328245162003F3F64 /* PBXTargetDependency */, ); + fileSystemSynchronizedGroups = ( + 302EFC1F2CC3C034004A74DF /* Views */, + ); name = CodeEdit; packageProductDependencies = ( 2816F593280CF50500DD548B /* CodeEditSymbols */, @@ -3692,8 +3703,6 @@ 6C0824A02C5C0C9700A0751E /* SwiftTerm */, 6CE21E862C650D2C0031B056 /* SwiftTerm */, 6C4E37FB2C73E00700AEE7B5 /* SwiftTerm */, - 6CD3CA542C8B508200D83DCD /* CodeEditSourceEditor */, - 6CD26C842C8F907800ADBA38 /* CodeEditSourceEditor */, 6CB94D022CA1205100E8651C /* AsyncAlgorithms */, ); productName = CodeEdit; @@ -3791,8 +3800,8 @@ 303E88452C276FD100EEA8D9 /* XCRemoteSwiftPackageReference "LanguageClient" */, 303E88462C276FD600EEA8D9 /* XCRemoteSwiftPackageReference "LanguageServerProtocol" */, 6C4E37FA2C73E00700AEE7B5 /* XCRemoteSwiftPackageReference "SwiftTerm" */, - 6CD26C832C8F907800ADBA38 /* XCRemoteSwiftPackageReference "CodeEditSourceEditor" */, 6CB94D012CA1205100E8651C /* XCRemoteSwiftPackageReference "swift-async-algorithms" */, + 302EFBFB2CC3284D004A74DF /* XCLocalSwiftPackageReference "../CodeEditSourceEditor" */, ); productRefGroup = B658FB2D27DA9E0F00EA4DBD /* Products */; projectDirPath = ""; @@ -4162,6 +4171,7 @@ 58A5DFA229339F6400D1BD5D /* KeybindingManager.swift in Sources */, B62AEDB32A1FD95B009A9F52 /* UtilityAreaTerminalView.swift in Sources */, 661EF7BD2BEE215300C3E577 /* LoadingFileView.swift in Sources */, + 3046374E2CB15FA900180667 /* AutoCompleteCoordinator.swift in Sources */, 58AFAA2E2933C69E00482B53 /* EditorTabRepresentable.swift in Sources */, 6C4104E6297C884F00F472BA /* AboutDetailView.swift in Sources */, 6C6BD6F129CD13FA00235D17 /* ExtensionDiscovery.swift in Sources */, @@ -5558,6 +5568,13 @@ }; /* End XCConfigurationList section */ +/* Begin XCLocalSwiftPackageReference section */ + 302EFBFB2CC3284D004A74DF /* XCLocalSwiftPackageReference "../CodeEditSourceEditor" */ = { + isa = XCLocalSwiftPackageReference; + relativePath = ../CodeEditSourceEditor; + }; +/* End XCLocalSwiftPackageReference section */ + /* Begin XCRemoteSwiftPackageReference section */ 2816F592280CF50500DD548B /* XCRemoteSwiftPackageReference "CodeEditSymbols" */ = { isa = XCRemoteSwiftPackageReference; @@ -5687,14 +5704,6 @@ version = 1.0.1; }; }; - 6CD26C832C8F907800ADBA38 /* XCRemoteSwiftPackageReference "CodeEditSourceEditor" */ = { - isa = XCRemoteSwiftPackageReference; - repositoryURL = "https://github.com/CodeEditApp/CodeEditSourceEditor"; - requirement = { - kind = upToNextMajorVersion; - minimumVersion = 0.8.1; - }; - }; /* End XCRemoteSwiftPackageReference section */ /* Begin XCSwiftPackageProductDependency section */ @@ -5789,15 +5798,6 @@ isa = XCSwiftPackageProductDependency; productName = CodeEditSourceEditor; }; - 6CD26C842C8F907800ADBA38 /* CodeEditSourceEditor */ = { - isa = XCSwiftPackageProductDependency; - package = 6CD26C832C8F907800ADBA38 /* XCRemoteSwiftPackageReference "CodeEditSourceEditor" */; - productName = CodeEditSourceEditor; - }; - 6CD3CA542C8B508200D83DCD /* CodeEditSourceEditor */ = { - isa = XCSwiftPackageProductDependency; - productName = CodeEditSourceEditor; - }; 6CE21E862C650D2C0031B056 /* SwiftTerm */ = { isa = XCSwiftPackageProductDependency; productName = SwiftTerm; diff --git a/CodeEdit.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/CodeEdit.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index a723cdba1..06161ebdd 100644 --- a/CodeEdit.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/CodeEdit.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -1,5 +1,5 @@ { - "originHash" : "5c4a5d433333474763817b9804d7f1856ab3b416ed87b190a2bd6e86c0c9834c", + "originHash" : "ebc53976f916f8c118f851ec7fe31170fe35ad33238a18b10b0fb7a4ba86f0b2", "pins" : [ { "identity" : "anycodable", @@ -13,7 +13,7 @@ { "identity" : "codeeditkit", "kind" : "remoteSourceControl", - "location" : "https://github.com/CodeEditApp/CodeEditKit.git", + "location" : "https://github.com/CodeEditApp/CodeEditKit", "state" : { "revision" : "ad28213a968586abb0cb21a8a56a3587227895f1", "version" : "0.1.2" @@ -28,15 +28,6 @@ "version" : "0.1.19" } }, - { - "identity" : "codeeditsourceeditor", - "kind" : "remoteSourceControl", - "location" : "https://github.com/CodeEditApp/CodeEditSourceEditor", - "state" : { - "revision" : "033b68d3e3e845984fbc3d405720d5cc6ce61f71", - "version" : "0.8.1" - } - }, { "identity" : "codeeditsymbols", "kind" : "remoteSourceControl", @@ -46,15 +37,6 @@ "version" : "0.2.2" } }, - { - "identity" : "codeedittextview", - "kind" : "remoteSourceControl", - "location" : "https://github.com/CodeEditApp/CodeEditTextView.git", - "state" : { - "revision" : "2619cb945b4d6c2fc13f22ba873ba891f552b0f3", - "version" : "0.7.6" - } - }, { "identity" : "collectionconcurrencykit", "kind" : "remoteSourceControl", @@ -168,8 +150,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/groue/Semaphore", "state" : { - "revision" : "2543679282aa6f6c8ecf2138acd613ed20790bc2", - "version" : "0.1.0" + "revision" : "f1c4a0acabeb591068dea6cffdd39660b86dec28", + "version" : "0.0.8" } }, { @@ -195,8 +177,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-collections.git", "state" : { - "revision" : "9bf03ff58ce34478e66aaee630e491823326fd06", - "version" : "1.1.3" + "revision" : "ee97538f5b81ae89698fd95938896dec5217b148", + "version" : "1.1.1" } }, { @@ -248,8 +230,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/siteline/SwiftUI-Introspect.git", "state" : { - "revision" : "807f73ce09a9b9723f12385e592b4e0aaebd3336", - "version" : "1.3.0" + "revision" : "668a65735751432b640260c56dfa621cec568368", + "version" : "1.2.0" } }, { diff --git a/CodeEdit/Features/Documents/CodeFileDocument/CodeFileDocument.swift b/CodeEdit/Features/Documents/CodeFileDocument/CodeFileDocument.swift index e3afda72c..d7760893a 100644 --- a/CodeEdit/Features/Documents/CodeFileDocument/CodeFileDocument.swift +++ b/CodeEdit/Features/Documents/CodeFileDocument/CodeFileDocument.swift @@ -10,7 +10,6 @@ import Foundation import SwiftUI import UniformTypeIdentifiers import CodeEditSourceEditor -import CodeEditTextView import CodeEditLanguages import Combine import OSLog diff --git a/CodeEdit/Features/Editor/AutoCompleteCoordinator.swift b/CodeEdit/Features/Editor/AutoCompleteCoordinator.swift new file mode 100644 index 000000000..4ba890168 --- /dev/null +++ b/CodeEdit/Features/Editor/AutoCompleteCoordinator.swift @@ -0,0 +1,114 @@ +// +// AutoCompleteCoordinator.swift +// CodeEdit +// +// Created by Abe Malla on 9/20/24. +// + +import AppKit +import CodeEditTextView +import CodeEditSourceEditor +import LanguageServerProtocol + +class AutoCompleteCoordinator: TextViewCoordinator { + private weak var textViewController: TextViewController? + private var localEventMonitor: Any? + + private let itemBoxController = ItemBoxWindowController() + + func prepareCoordinator(controller: TextViewController) { + itemBoxController.close() + self.textViewController = controller + + localEventMonitor = NSEvent.addLocalMonitorForEvents(matching: .keyDown) { event in + // `ctrl + space` keyboard shortcut listener for the item box to show + if event.modifierFlags.contains(.control) && event.charactersIgnoringModifiers == " " { + self.showAutocompleteWindow() + return nil + } + return event + } + } + + func showAutocompleteWindow() { + guard let cursorPos = textViewController?.cursorPositions.last, + let textView = textViewController?.textView, + let window = NSApplication.shared.keyWindow, + !itemBoxController.isVisible + else { + return + } + + itemBoxController.items = [ + CompletionItem(label: "item1", kind: .class), + CompletionItem(label: "item2", kind: .enum), + CompletionItem(label: "item3", kind: .function), + CompletionItem(label: "item4", kind: .color), + CompletionItem(label: "item5", kind: .constant), + CompletionItem(label: "item6", kind: .constructor), + CompletionItem(label: "item7", kind: .enumMember), + CompletionItem(label: "item8", kind: .field), + CompletionItem(label: "item9", kind: .file), + CompletionItem(label: "item10", kind: .folder), + CompletionItem(label: "item11", kind: .snippet), + CompletionItem(label: "item12", kind: .reference), + ] + + // Reset the size of the window + let windowSize = ItemBoxWindowController.DEFAULT_SIZE + itemBoxController.window?.setContentSize(windowSize) + + let cursorRect = textView.firstRect(forCharacterRange: cursorPos.range, actualRange: nil) + let screenFrame = window.screen!.visibleFrame + let padding: CGFloat = 22 + var autocompleteWindowOrigin = NSPoint( + x: cursorRect.origin.x, + y: cursorRect.origin.y + ) + + // Keep the horizontal position within the screen and some padding + let minX = screenFrame.minX + padding + let maxX = screenFrame.maxX - windowSize.width - padding + + if autocompleteWindowOrigin.x < minX { + autocompleteWindowOrigin.x = minX + } else if autocompleteWindowOrigin.x > maxX { + autocompleteWindowOrigin.x = maxX + } + + // Check if the window will go below the screen + // We determine whether the window drops down or upwards by choosing which + // corner of the window we will position: `setFrameOrigin` or `setFrameTopLeftPoint` + if autocompleteWindowOrigin.y - windowSize.height < screenFrame.minY { + // If the cursor itself if below the screen, then position the window + // at the bottom of the screen with some padding + if autocompleteWindowOrigin.y < screenFrame.minY { + autocompleteWindowOrigin.y = screenFrame.minY + padding + } else { + // Place above the cursor + autocompleteWindowOrigin.y += cursorRect.height + } + + itemBoxController.window?.setFrameOrigin(autocompleteWindowOrigin) + } else { + // If the window goes above the screen, position it below the screen with padding + let maxY = screenFrame.maxY - padding + if autocompleteWindowOrigin.y > maxY { + autocompleteWindowOrigin.y = maxY + } + + itemBoxController.window?.setFrameTopLeftPoint(autocompleteWindowOrigin) + } + + itemBoxController.showWindow(attachedTo: window) + } + + deinit { + print("Destroyed AutoCompleteCoordinator") + itemBoxController.close() + if let localEventMonitor = localEventMonitor { + NSEvent.removeMonitor(localEventMonitor) + self.localEventMonitor = nil + } + } +} diff --git a/CodeEdit/Features/Editor/Views/CodeFileView.swift b/CodeEdit/Features/Editor/Views/CodeFileView.swift index 1a0ae19c9..698acd736 100644 --- a/CodeEdit/Features/Editor/Views/CodeFileView.swift +++ b/CodeEdit/Features/Editor/Views/CodeFileView.swift @@ -21,6 +21,8 @@ struct CodeFileView: View { /// Any coordinators passed to the view. private var textViewCoordinators: [TextViewCoordinator] + /// The coordinator that manages the autocomplete window (item box) + private let autocompleteCoordinator = AutoCompleteCoordinator() @AppSettings(\.textEditing.defaultTabWidth) var defaultTabWidth @@ -58,7 +60,8 @@ struct CodeFileView: View { self._codeFile = .init(wrappedValue: codeFile) self.textViewCoordinators = textViewCoordinators + [ codeFile.contentCoordinator, - codeFile.languageServerCoordinator + codeFile.languageServerCoordinator, + autocompleteCoordinator, ] self.isEditable = isEditable @@ -139,7 +142,6 @@ struct CodeFileView: View { undoManager: undoManager, coordinators: textViewCoordinators ) - .id(codeFile.fileURL) .background { if colorScheme == .dark { diff --git a/CodeEdit/Features/LSP/LanguageServer/LanguageServerFileMap.swift b/CodeEdit/Features/LSP/LanguageServer/LanguageServerFileMap.swift index 0f3d4469f..7da7fd434 100644 --- a/CodeEdit/Features/LSP/LanguageServer/LanguageServerFileMap.swift +++ b/CodeEdit/Features/LSP/LanguageServer/LanguageServerFileMap.swift @@ -47,8 +47,8 @@ class LanguageServerFileMap { } func incrementVersion(for uri: DocumentUri) -> Int { - trackedDocumentVersions[uri] = (trackedDocumentVersions[uri] ?? 0) + 1 - return trackedDocumentVersions[uri] ?? 0 + trackedDocumentVersions[uri, default: 0] += 1 + return trackedDocumentVersions[uri, default: 1] } func documentVersion(for document: CodeFileDocument) -> Int? { diff --git a/CodeEdit/Features/LSP/Views/CompletionItem.swift b/CodeEdit/Features/LSP/Views/CompletionItem.swift new file mode 100644 index 000000000..65b276f4d --- /dev/null +++ b/CodeEdit/Features/LSP/Views/CompletionItem.swift @@ -0,0 +1,30 @@ +// +// CompletionItem.swift +// CodeEdit +// +// Created by Abe Malla on 10/05/24. +// + +import SwiftUI +import CodeEditTextView +import LanguageServerProtocol +import CodeEditSourceEditor + +extension CompletionItem: @retroactive ItemBoxEntry { + public var view: NSView { + NSHostingView(rootView: HStack(spacing: 0) { + Image(systemName: CompletionItemKind.toSymbolName(kind: self.kind)) + .font(.system(size: 16)) + .foregroundStyle(.white, CompletionItemKind.toSymbolColor(kind: self.kind)) + .padding(0) + .padding(.trailing, 2) + + Text(label) + .font(.system(size: 12, design: .monospaced)) + .foregroundStyle(.secondary) + .padding(0) + + Spacer() + }) + } +} diff --git a/CodeEdit/Features/LSP/Views/CompletionItemKind.swift b/CodeEdit/Features/LSP/Views/CompletionItemKind.swift new file mode 100644 index 000000000..6f2144a2a --- /dev/null +++ b/CodeEdit/Features/LSP/Views/CompletionItemKind.swift @@ -0,0 +1,85 @@ +// +// CompletionItemKind.swift +// CodeEdit +// +// Created by Abe Malla on 10/05/24. +// + +import SwiftUI +import LanguageServerProtocol + +extension CompletionItemKind { + static func toSymbolName(kind: CompletionItemKind?) -> String { + let defaultSymbol = "dot.square.fill" + + guard let kind = kind else { + return defaultSymbol + } + + let symbolMap: [CompletionItemKind: String] = [ + .text: "t.square.fill", + .method: "m.square.fill", + .function: "curlybraces.square.fill", + .constructor: "i.square.fill", + .field: "c.square.fill", + .variable: "v.square.fill", + .class: "c.square.fill", + .interface: "i.square.fill", + .module: "m.square.fill", + .property: "p.square.fill", + .unit: "u.square.fill", + .value: "n.square.fill", + .enum: "e.square.fill", + .keyword: "k.square.fill", + .snippet: "s.square.fill", + .color: "c.square.fill", + .file: "d.square.fill", + .reference: "r.square.fill", + .folder: "f.square.fill", + .enumMember: "e.square.fill", + .constant: "k.square.fill", + .struct: "s.square.fill", + .event: "e.square.fill", + .operator: "plus.slash.minus", + .typeParameter: "t.square.fill" + ] + return symbolMap[kind] ?? defaultSymbol + } + + static func toSymbolColor(kind: CompletionItemKind?) -> SwiftUICore.Color { + let defaultColor = Color.gray + + guard let kind = kind else { + return defaultColor + } + + let symbolMap: [CompletionItemKind: SwiftUICore.Color] = [ + .text: Color.blue, + .method: Color.blue, + .function: Color.blue, + .constructor: Color.teal, + .field: Color.blue, + .variable: Color.blue, + .class: Color.pink, + .interface: Color.blue, + .module: Color.blue, + .property: Color.secondary, + .unit: Color.blue, + .value: Color.blue, + .enum: Color.blue, + .keyword: Color.blue, + .snippet: Color.blue, + .color: Color.blue, + .file: Color.blue, + .reference: Color.blue, + .folder: Color.blue, + .enumMember: Color.blue, + .constant: Color.blue, + .struct: Color.blue, + .event: Color.blue, + .operator: Color.blue, + .typeParameter: Color.blue, + ] + return symbolMap[kind] ?? defaultColor + } +} From 02fb3b5bb21ddc7701b5ae4d5bbfc463dbecafe5 Mon Sep 17 00:00:00 2001 From: Abe M Date: Tue, 17 Dec 2024 23:24:28 -0800 Subject: [PATCH 2/8] ItemBox updates --- .../xcshareddata/swiftpm/Package.resolved | 19 ++- .../Editor/AutoCompleteCoordinator.swift | 121 +++++++++--------- .../Editor/Models/EditorInstance.swift | 2 + .../Features/Editor/Views/CodeFileView.swift | 3 - .../Editor/Views/EditorAreaView.swift | 6 +- .../LSP/Service/LSPService+Events.swift | 100 +++++++-------- .../Features/LSP/Service/LSPService.swift | 28 ++++ .../Features/LSP/Views/CompletionItem.swift | 84 ++++++++++-- .../LSP/Views/CompletionItemKind.swift | 12 +- 9 files changed, 237 insertions(+), 138 deletions(-) diff --git a/CodeEdit.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/CodeEdit.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 06161ebdd..0eba906d6 100644 --- a/CodeEdit.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/CodeEdit.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -1,5 +1,5 @@ { - "originHash" : "ebc53976f916f8c118f851ec7fe31170fe35ad33238a18b10b0fb7a4ba86f0b2", + "originHash" : "bb72acfad31b288599b6721256b508d8209ba1bc1d7ab0fff6a358d49a1deae0", "pins" : [ { "identity" : "anycodable", @@ -24,8 +24,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/CodeEditApp/CodeEditLanguages.git", "state" : { - "revision" : "5b27f139269e1ea49ceae5e56dca44a3ccad50a1", - "version" : "0.1.19" + "revision" : "331d5dbc5fc8513be5848fce8a2a312908f36a11", + "version" : "0.1.20" } }, { @@ -221,8 +221,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/ChimeHQ/SwiftTreeSitter.git", "state" : { - "revision" : "2599e95310b3159641469d8a21baf2d3d200e61f", - "version" : "0.8.0" + "revision" : "36aa61d1b531f744f35229f010efba9c6d6cbbdd", + "version" : "0.9.0" } }, { @@ -251,6 +251,15 @@ "revision" : "8dc9148b46fcf93b08ea9d4ef9bdb5e4f700e008", "version" : "0.9.0" } + }, + { + "identity" : "tree-sitter", + "kind" : "remoteSourceControl", + "location" : "https://github.com/tree-sitter/tree-sitter", + "state" : { + "revision" : "d97db6d63507eb62c536bcb2c4ac7d70c8ec665e", + "version" : "0.23.2" + } } ], "version" : 3 diff --git a/CodeEdit/Features/Editor/AutoCompleteCoordinator.swift b/CodeEdit/Features/Editor/AutoCompleteCoordinator.swift index 4ba890168..eacb50a59 100644 --- a/CodeEdit/Features/Editor/AutoCompleteCoordinator.swift +++ b/CodeEdit/Features/Editor/AutoCompleteCoordinator.swift @@ -14,10 +14,12 @@ class AutoCompleteCoordinator: TextViewCoordinator { private weak var textViewController: TextViewController? private var localEventMonitor: Any? - private let itemBoxController = ItemBoxWindowController() + private var itemBoxController: ItemBoxWindowController? func prepareCoordinator(controller: TextViewController) { - itemBoxController.close() + itemBoxController = ItemBoxWindowController() + itemBoxController?.delegate = self + itemBoxController?.close() self.textViewController = controller localEventMonitor = NSEvent.addLocalMonitorForEvents(matching: .keyDown) { event in @@ -31,84 +33,79 @@ class AutoCompleteCoordinator: TextViewCoordinator { } func showAutocompleteWindow() { - guard let cursorPos = textViewController?.cursorPositions.last, + guard let cursorPos = textViewController?.cursorPositions.first, let textView = textViewController?.textView, let window = NSApplication.shared.keyWindow, + let itemBoxController = itemBoxController, !itemBoxController.isVisible else { return } + @Service var lspService: LSPService + +// lspService. + itemBoxController.items = [ - CompletionItem(label: "item1", kind: .class), - CompletionItem(label: "item2", kind: .enum), - CompletionItem(label: "item3", kind: .function), - CompletionItem(label: "item4", kind: .color), - CompletionItem(label: "item5", kind: .constant), - CompletionItem(label: "item6", kind: .constructor), - CompletionItem(label: "item7", kind: .enumMember), - CompletionItem(label: "item8", kind: .field), - CompletionItem(label: "item9", kind: .file), - CompletionItem(label: "item10", kind: .folder), - CompletionItem(label: "item11", kind: .snippet), - CompletionItem(label: "item12", kind: .reference), + CompletionItem(label: "CETable", kind: .class), + CompletionItem(label: "CETask", kind: .enum), + CompletionItem(label: "CETarget", kind: .function), + CompletionItem(label: "CEItem", kind: .color), + CompletionItem(label: "tableView", kind: .constant), + CompletionItem(label: "itemBoxController", kind: .constructor), + CompletionItem(label: "showAutocompleteWindow", kind: .enumMember), + CompletionItem(label: "NSApplication", kind: .field), + CompletionItem(label: "CECell", kind: .file), + CompletionItem(label: "Item10", kind: .folder), + CompletionItem(label: "Item11", kind: .snippet), + CompletionItem(label: "Item12", kind: .reference), ] - // Reset the size of the window - let windowSize = ItemBoxWindowController.DEFAULT_SIZE - itemBoxController.window?.setContentSize(windowSize) - let cursorRect = textView.firstRect(forCharacterRange: cursorPos.range, actualRange: nil) - let screenFrame = window.screen!.visibleFrame - let padding: CGFloat = 22 - var autocompleteWindowOrigin = NSPoint( - x: cursorRect.origin.x, - y: cursorRect.origin.y - ) - - // Keep the horizontal position within the screen and some padding - let minX = screenFrame.minX + padding - let maxX = screenFrame.maxX - windowSize.width - padding - - if autocompleteWindowOrigin.x < minX { - autocompleteWindowOrigin.x = minX - } else if autocompleteWindowOrigin.x > maxX { - autocompleteWindowOrigin.x = maxX - } - - // Check if the window will go below the screen - // We determine whether the window drops down or upwards by choosing which - // corner of the window we will position: `setFrameOrigin` or `setFrameTopLeftPoint` - if autocompleteWindowOrigin.y - windowSize.height < screenFrame.minY { - // If the cursor itself if below the screen, then position the window - // at the bottom of the screen with some padding - if autocompleteWindowOrigin.y < screenFrame.minY { - autocompleteWindowOrigin.y = screenFrame.minY + padding - } else { - // Place above the cursor - autocompleteWindowOrigin.y += cursorRect.height - } - - itemBoxController.window?.setFrameOrigin(autocompleteWindowOrigin) - } else { - // If the window goes above the screen, position it below the screen with padding - let maxY = screenFrame.maxY - padding - if autocompleteWindowOrigin.y > maxY { - autocompleteWindowOrigin.y = maxY - } - - itemBoxController.window?.setFrameTopLeftPoint(autocompleteWindowOrigin) - } - + itemBoxController.constrainWindowToScreenEdges(cursorRect: cursorRect) itemBoxController.showWindow(attachedTo: window) } deinit { - print("Destroyed AutoCompleteCoordinator") - itemBoxController.close() + itemBoxController?.close() if let localEventMonitor = localEventMonitor { NSEvent.removeMonitor(localEventMonitor) self.localEventMonitor = nil } } } + +extension MarkupContent { + public init(kind: MarkupKind, value: String) { + do { + let dictionary: [String: Any] = ["kind": kind.rawValue, "value": value] + let data = try JSONSerialization.data(withJSONObject: dictionary) + self = try JSONDecoder().decode(MarkupContent.self, from: data) + } catch { + print("Failed to create MarkupContent: \(error)") + // swiftlint:disable:next force_try + self = try! JSONDecoder().decode(MarkupContent.self, from: """ + {"kind": "plaintext", "value": ""} + """.data(using: .utf8)!) + } + } +} + +extension AutoCompleteCoordinator: ItemBoxDelegate { + func applyCompletionItem(_ item: CompletionItem) { + guard let cursorPos = textViewController?.cursorPositions.first else { + return + } + + do { + let token = try textViewController?.treeSitterClient?.nodesAt(range: cursorPos.range) + guard let token = token?.first else { + return + } + print("Token \(token)") + } catch { + print("\(error)") + return + } + } +} diff --git a/CodeEdit/Features/Editor/Models/EditorInstance.swift b/CodeEdit/Features/Editor/Models/EditorInstance.swift index f8aeb8ebc..eb50baa4b 100644 --- a/CodeEdit/Features/Editor/Models/EditorInstance.swift +++ b/CodeEdit/Features/Editor/Models/EditorInstance.swift @@ -27,6 +27,7 @@ class EditorInstance: Hashable { // Public TextViewCoordinator APIs var rangeTranslator: RangeTranslator? + var autoCompleteCoordinator: AutoCompleteCoordinator? // Internal Combine subjects @@ -38,6 +39,7 @@ class EditorInstance: Hashable { self.file = file self.cursorSubject.send(cursorPositions) self.rangeTranslator = RangeTranslator(cursorSubject: cursorSubject) + self.autoCompleteCoordinator = AutoCompleteCoordinator() } func hash(into hasher: inout Hasher) { diff --git a/CodeEdit/Features/Editor/Views/CodeFileView.swift b/CodeEdit/Features/Editor/Views/CodeFileView.swift index 622d733c9..696d44541 100644 --- a/CodeEdit/Features/Editor/Views/CodeFileView.swift +++ b/CodeEdit/Features/Editor/Views/CodeFileView.swift @@ -21,8 +21,6 @@ struct CodeFileView: View { /// Any coordinators passed to the view. private var textViewCoordinators: [TextViewCoordinator] - /// The coordinator that manages the autocomplete window (item box) - private let autocompleteCoordinator = AutoCompleteCoordinator() @AppSettings(\.textEditing.defaultTabWidth) var defaultTabWidth @@ -61,7 +59,6 @@ struct CodeFileView: View { self.textViewCoordinators = textViewCoordinators + [ codeFile.contentCoordinator, codeFile.languageServerCoordinator, - autocompleteCoordinator, ] self.isEditable = isEditable diff --git a/CodeEdit/Features/Editor/Views/EditorAreaView.swift b/CodeEdit/Features/Editor/Views/EditorAreaView.swift index 421adf9d6..384a65536 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 }) + // Linter keeps complaining about types, which is why there are these weird casts + textViewCoordinators: [ + selected.rangeTranslator as Any, selected.autoCompleteCoordinator as Any + ].compactMap({ $0 as? any TextViewCoordinator }) ) .focusedObject(editor) .transformEnvironment(\.edgeInsets) { insets in diff --git a/CodeEdit/Features/LSP/Service/LSPService+Events.swift b/CodeEdit/Features/LSP/Service/LSPService+Events.swift index b4baa73bb..1d5bee6fe 100644 --- a/CodeEdit/Features/LSP/Service/LSPService+Events.swift +++ b/CodeEdit/Features/LSP/Service/LSPService+Events.swift @@ -33,63 +33,63 @@ extension LSPService { private func handleEvent(_ event: ServerEvent, for key: ClientKey) { // TODO: Handle Events -// switch event { -// case let .request(id, request): -// print("Request ID: \(id) for \(key.languageId.rawValue)") -// handleRequest(request) -// case let .notification(notification): -// handleNotification(notification) -// case let .error(error): -// print("Error from EventStream for \(key.languageId.rawValue): \(error)") -// } + switch event { + case let .request(id, request): + print("Request ID: \(id) for \(key.languageId.rawValue)") + handleRequest(request) + case let .notification(notification): + handleNotification(notification) + case let .error(error): + print("Error from EventStream for \(key.languageId.rawValue): \(error)") + } } private func handleRequest(_ request: ServerRequest) { // TODO: Handle Requests -// switch request { -// case let .workspaceConfiguration(params, _): -// print("workspaceConfiguration: \(params)") -// case let .workspaceFolders(handler): -// print("workspaceFolders: \(String(describing: handler))") -// case let .workspaceApplyEdit(params, _): -// print("workspaceApplyEdit: \(params)") -// case let .clientRegisterCapability(params, _): -// print("clientRegisterCapability: \(params)") -// case let .clientUnregisterCapability(params, _): -// print("clientUnregisterCapability: \(params)") -// case let .workspaceCodeLensRefresh(handler): -// print("workspaceCodeLensRefresh: \(String(describing: handler))") -// case let .workspaceSemanticTokenRefresh(handler): -// print("workspaceSemanticTokenRefresh: \(String(describing: handler))") -// case let .windowShowMessageRequest(params, _): -// print("windowShowMessageRequest: \(params)") -// case let .windowShowDocument(params, _): -// print("windowShowDocument: \(params)") -// case let .windowWorkDoneProgressCreate(params, _): -// print("windowWorkDoneProgressCreate: \(params)") -// -// default: -// print() -// } + switch request { + case let .workspaceConfiguration(params, _): + print("workspaceConfiguration: \(params)") + case let .workspaceFolders(handler): + print("workspaceFolders: \(String(describing: handler))") + case let .workspaceApplyEdit(params, _): + print("workspaceApplyEdit: \(params)") + case let .clientRegisterCapability(params, _): + print("clientRegisterCapability: \(params)") + case let .clientUnregisterCapability(params, _): + print("clientUnregisterCapability: \(params)") + case let .workspaceCodeLensRefresh(handler): + print("workspaceCodeLensRefresh: \(String(describing: handler))") + case let .workspaceSemanticTokenRefresh(handler): + print("workspaceSemanticTokenRefresh: \(String(describing: handler))") + case let .windowShowMessageRequest(params, _): + print("windowShowMessageRequest: \(params)") + case let .windowShowDocument(params, _): + print("windowShowDocument: \(params)") + case let .windowWorkDoneProgressCreate(params, _): + print("windowWorkDoneProgressCreate: \(params)") + + default: + print() + } } private func handleNotification(_ notification: ServerNotification) { // TODO: Handle Notifications -// switch notification { -// case let .windowLogMessage(params): -// print("windowLogMessage \(params.type)\n```\n\(params.message)\n```\n") -// case let .windowShowMessage(params): -// print("windowShowMessage \(params.type)\n```\n\(params.message)\n```\n") -// case let .textDocumentPublishDiagnostics(params): -// print("textDocumentPublishDiagnostics: \(params)") -// case let .telemetryEvent(params): -// print("telemetryEvent: \(params)") -// case let .protocolCancelRequest(params): -// print("protocolCancelRequest: \(params)") -// case let .protocolProgress(params): -// print("protocolProgress: \(params)") -// case let .protocolLogTrace(params): -// print("protocolLogTrace: \(params)") -// } + switch notification { + case let .windowLogMessage(params): + print("windowLogMessage \(params.type)\n```\n\(params.message)\n```\n") + case let .windowShowMessage(params): + print("windowShowMessage \(params.type)\n```\n\(params.message)\n```\n") + case let .textDocumentPublishDiagnostics(params): + print("textDocumentPublishDiagnostics: \(params)") + case let .telemetryEvent(params): + print("telemetryEvent: \(params)") + case let .protocolCancelRequest(params): + print("protocolCancelRequest: \(params)") + case let .protocolProgress(params): + print("protocolProgress: \(params)") + case let .protocolLogTrace(params): + print("protocolLogTrace: \(params)") + } } } diff --git a/CodeEdit/Features/LSP/Service/LSPService.swift b/CodeEdit/Features/LSP/Service/LSPService.swift index 0c4b5a812..7ddfcb1d4 100644 --- a/CodeEdit/Features/LSP/Service/LSPService.swift +++ b/CodeEdit/Features/LSP/Service/LSPService.swift @@ -158,14 +158,42 @@ final class LSPService: ObservableObject { throw LSPError.binaryNotFound } + let taskUuidString = UUID().uuidString + + // Log start message to the activity viewer + let createInfo: [String: Any] = [ + "id": taskUuidString, + "action": "create", + "title": "Starting \(languageId.rawValue) language server", + "isLoading": true + ] logger.info("Starting \(languageId.rawValue) language server") + NotificationCenter.default.post(name: .taskNotification, object: nil, userInfo: createInfo) + + // Attempt to start the language server let server = try await LanguageServer.createServer( for: languageId, with: serverBinary, workspacePath: workspacePath ) languageClients[ClientKey(languageId, workspacePath)] = server + + // Log success message update + let updateInfo: [String: Any] = [ + "id": taskUuidString, + "action": "update", + "title": "Successfully started \(languageId.rawValue) language server", + "isLoading": false + ] + NotificationCenter.default.post(name: .taskNotification, object: nil, userInfo: updateInfo) + + let deleteInfo: [String: Any] = [ + "id": taskUuidString, + "action": "deleteWithDelay", + "delay": 4.0 + ] logger.info("Successfully started \(languageId.rawValue) language server") + NotificationCenter.default.post(name: .taskNotification, object: nil, userInfo: deleteInfo) self.startListeningToEvents(for: ClientKey(languageId, workspacePath)) return server diff --git a/CodeEdit/Features/LSP/Views/CompletionItem.swift b/CodeEdit/Features/LSP/Views/CompletionItem.swift index 65b276f4d..8d7bd3bf4 100644 --- a/CodeEdit/Features/LSP/Views/CompletionItem.swift +++ b/CodeEdit/Features/LSP/Views/CompletionItem.swift @@ -10,21 +10,83 @@ import CodeEditTextView import LanguageServerProtocol import CodeEditSourceEditor +// TODO: REMOVE Y OFFSET ON 16 PX? + +// TODO: IMPORT FONT SIZE +let FONT_SIZE: CGFloat = 12 +let fontSizeToImageSize: [CGFloat: CGFloat] = [ + 12: 16.5, + 13: 17.75, // Not sure + 14: 19, // checking this + 16: 22, + 18: 24, +] +let fontSizeToRowHeight: [CGFloat: CGFloat] = [ + 12: 21, + 13: 22, + 14: 23, + 15: 0, // TODO + 16: 26, + 17: 0, // TODO + 18: 28, +] +let fontSizeToRightPadding: [CGFloat: CGFloat] = [ + 12: 13, + 13: 13, + 14: 13, // TODO + 15: 12.5, + 16: 12.5, + 17: 12.5, + 18: 12.5, +] + extension CompletionItem: @retroactive ItemBoxEntry { public var view: NSView { - NSHostingView(rootView: HStack(spacing: 0) { - Image(systemName: CompletionItemKind.toSymbolName(kind: self.kind)) - .font(.system(size: 16)) - .foregroundStyle(.white, CompletionItemKind.toSymbolColor(kind: self.kind)) - .padding(0) - .padding(.trailing, 2) + NSHostingView( + rootView: HStack(spacing: 0) { + Image(systemName: CompletionItemKind.toSymbolName(kind: self.kind)) + .font(.system(size: fontSizeToImageSize[FONT_SIZE]!)) + .foregroundStyle( + .white, + deprecated == true ? .gray : CompletionItemKind.toSymbolColor(kind: self.kind) + ) + .padding(0) + .padding(.trailing, 2) - Text(label) - .font(.system(size: 12, design: .monospaced)) - .foregroundStyle(.secondary) + // Main label + HStack(spacing: 0) { + Text(label) + .font(.system(size: FONT_SIZE, design: .monospaced)) + .foregroundStyle(deprecated == true ? .secondary : .primary) + + if let detail = detail { + Text(detail) + .font(.system(size: FONT_SIZE, design: .monospaced)) + .foregroundStyle(.secondary) + } + } .padding(0) + .offset(y: -1) + + Spacer() - Spacer() - }) + // Right side indicators + HStack(spacing: 6.5) { + if deprecated == true { + Image(systemName: "exclamationmark.triangle") + .font(.system(size: FONT_SIZE + 2)) + .foregroundStyle(.primary, .secondary) + } + if documentation != nil { + Image(systemName: "chevron.right") + .font(.system(size: FONT_SIZE - 2.5)) + .fontWeight(.semibold) + } + } + .padding(.leading, 4) + .padding(.trailing, 6.5) + } + .padding(.horizontal, fontSizeToRightPadding[FONT_SIZE]) + ) } } diff --git a/CodeEdit/Features/LSP/Views/CompletionItemKind.swift b/CodeEdit/Features/LSP/Views/CompletionItemKind.swift index 6f2144a2a..dbe551505 100644 --- a/CodeEdit/Features/LSP/Views/CompletionItemKind.swift +++ b/CodeEdit/Features/LSP/Views/CompletionItemKind.swift @@ -55,20 +55,20 @@ extension CompletionItemKind { let symbolMap: [CompletionItemKind: SwiftUICore.Color] = [ .text: Color.blue, - .method: Color.blue, + .method: Color.cyan, .function: Color.blue, .constructor: Color.teal, - .field: Color.blue, + .field: Color.indigo, .variable: Color.blue, .class: Color.pink, .interface: Color.blue, .module: Color.blue, - .property: Color.secondary, + .property: Color.purple, .unit: Color.blue, .value: Color.blue, - .enum: Color.blue, - .keyword: Color.blue, - .snippet: Color.blue, + .enum: Color.mint, + .keyword: Color.pink, + .snippet: Color.purple, .color: Color.blue, .file: Color.blue, .reference: Color.blue, From a2de09056087ecacfabfd01847b05fab0da8345b Mon Sep 17 00:00:00 2001 From: Abe M Date: Sun, 22 Dec 2024 03:47:09 -0800 Subject: [PATCH 3/8] Autocomplete updates --- .../xcshareddata/swiftpm/Package.resolved | 20 +-- .../Editor/AutoCompleteCoordinator.swift | 120 +++++++++++------- .../Editor/Models/EditorInstance.swift | 2 +- 3 files changed, 78 insertions(+), 64 deletions(-) diff --git a/CodeEdit.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/CodeEdit.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 96f4d6574..0eba906d6 100644 --- a/CodeEdit.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/CodeEdit.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -1,5 +1,5 @@ { - "originHash" : "ac57a6899925c3e4ac6d43aed791c845c6fc24a4441b6a10297a207d951b7836", + "originHash" : "bb72acfad31b288599b6721256b508d8209ba1bc1d7ab0fff6a358d49a1deae0", "pins" : [ { "identity" : "anycodable", @@ -28,15 +28,6 @@ "version" : "0.1.20" } }, - { - "identity" : "codeeditsourceeditor", - "kind" : "remoteSourceControl", - "location" : "https://github.com/CodeEditApp/CodeEditSourceEditor", - "state" : { - "revision" : "b0688fa59fb8060840fb013afb4d6e6a96000f14", - "version" : "0.9.1" - } - }, { "identity" : "codeeditsymbols", "kind" : "remoteSourceControl", @@ -46,15 +37,6 @@ "version" : "0.2.2" } }, - { - "identity" : "codeedittextview", - "kind" : "remoteSourceControl", - "location" : "https://github.com/CodeEditApp/CodeEditTextView.git", - "state" : { - "revision" : "509d7b2e86460e8ec15b0dd5410cbc8e8c05940f", - "version" : "0.7.7" - } - }, { "identity" : "collectionconcurrencykit", "kind" : "remoteSourceControl", diff --git a/CodeEdit/Features/Editor/AutoCompleteCoordinator.swift b/CodeEdit/Features/Editor/AutoCompleteCoordinator.swift index eacb50a59..bea465144 100644 --- a/CodeEdit/Features/Editor/AutoCompleteCoordinator.swift +++ b/CodeEdit/Features/Editor/AutoCompleteCoordinator.swift @@ -12,10 +12,15 @@ import LanguageServerProtocol class AutoCompleteCoordinator: TextViewCoordinator { private weak var textViewController: TextViewController? + private unowned var file: CEWorkspaceFile private var localEventMonitor: Any? private var itemBoxController: ItemBoxWindowController? + init(_ file: CEWorkspaceFile) { + self.file = file + } + func prepareCoordinator(controller: TextViewController) { itemBoxController = ItemBoxWindowController() itemBoxController?.delegate = self @@ -25,45 +30,65 @@ class AutoCompleteCoordinator: TextViewCoordinator { localEventMonitor = NSEvent.addLocalMonitorForEvents(matching: .keyDown) { event in // `ctrl + space` keyboard shortcut listener for the item box to show if event.modifierFlags.contains(.control) && event.charactersIgnoringModifiers == " " { - self.showAutocompleteWindow() + Task { + await self.showAutocompleteWindow() + } return nil } return event } } + @MainActor func showAutocompleteWindow() { guard let cursorPos = textViewController?.cursorPositions.first, let textView = textViewController?.textView, let window = NSApplication.shared.keyWindow, - let itemBoxController = itemBoxController, - !itemBoxController.isVisible + let itemBoxController = itemBoxController else { return } + Task { + let textPosition = Position(line: cursorPos.line - 1, character: cursorPos.column - 1) + let completionItems = await fetchCompletions(position: textPosition) + itemBoxController.items = completionItems + + let cursorRect = textView.firstRect(forCharacterRange: cursorPos.range, actualRange: nil) + itemBoxController.constrainWindowToScreenEdges(cursorRect: cursorRect) + itemBoxController.showWindow(attachedTo: window) + } + } + + private func fetchCompletions(position: Position) async -> [CompletionItem] { + let workspace = await file.fileDocument?.findWorkspace() + guard let workspacePath = workspace?.fileURL?.absoluteURL.path() else { return [] } + guard let language = await file.fileDocument?.getLanguage().lspLanguage else { return [] } + @Service var lspService: LSPService + guard let client = await lspService.languageClient( + for: language, workspacePath: workspacePath + ) else { + return [] + } -// lspService. - - itemBoxController.items = [ - CompletionItem(label: "CETable", kind: .class), - CompletionItem(label: "CETask", kind: .enum), - CompletionItem(label: "CETarget", kind: .function), - CompletionItem(label: "CEItem", kind: .color), - CompletionItem(label: "tableView", kind: .constant), - CompletionItem(label: "itemBoxController", kind: .constructor), - CompletionItem(label: "showAutocompleteWindow", kind: .enumMember), - CompletionItem(label: "NSApplication", kind: .field), - CompletionItem(label: "CECell", kind: .file), - CompletionItem(label: "Item10", kind: .folder), - CompletionItem(label: "Item11", kind: .snippet), - CompletionItem(label: "Item12", kind: .reference), - ] - - let cursorRect = textView.firstRect(forCharacterRange: cursorPos.range, actualRange: nil) - itemBoxController.constrainWindowToScreenEdges(cursorRect: cursorRect) - itemBoxController.showWindow(attachedTo: window) + do { + let completions = try await client.requestCompletion( + for: file.url.absoluteURL.path(), position: position + ) + + // Extract the completion items list + switch completions { + case .optionA(let completionItems): + return completionItems + case .optionB(let completionList): + return completionList.items + case .none: + return [] + } + } catch { + return [] + } } deinit { @@ -75,34 +100,41 @@ class AutoCompleteCoordinator: TextViewCoordinator { } } -extension MarkupContent { - public init(kind: MarkupKind, value: String) { - do { - let dictionary: [String: Any] = ["kind": kind.rawValue, "value": value] - let data = try JSONSerialization.data(withJSONObject: dictionary) - self = try JSONDecoder().decode(MarkupContent.self, from: data) - } catch { - print("Failed to create MarkupContent: \(error)") - // swiftlint:disable:next force_try - self = try! JSONDecoder().decode(MarkupContent.self, from: """ - {"kind": "plaintext", "value": ""} - """.data(using: .utf8)!) - } - } -} - extension AutoCompleteCoordinator: ItemBoxDelegate { func applyCompletionItem(_ item: CompletionItem) { - guard let cursorPos = textViewController?.cursorPositions.first else { + guard let cursorPos = textViewController?.cursorPositions.first, + let textView = textViewController?.textView else { return } do { - let token = try textViewController?.treeSitterClient?.nodesAt(range: cursorPos.range) - guard let token = token?.first else { - return + let textPosition = Position( + line: cursorPos.line - 1, + character: cursorPos.column - 1 + ) + var textEdits = LSPCompletionItemsUtil.getCompletionItemEdits( + startPosition: textPosition, + item: item + ) + // Appropriately order the text edits + textEdits = TextEdit.makeApplicable(textEdits) + + // Make the updates + textView.undoManager?.beginUndoGrouping() + for textEdit in textEdits { + textView.replaceString( + in: NSRange(location: 0, length: 0), + with: textEdit.newText + ) } - print("Token \(token)") + textView.undoManager?.endUndoGrouping() + +// textViewController?.textView.applyMutations(<#T##mutations: [TextMutation]##[TextMutation]#>) +// let token = try textViewController?.treeSitterClient?.nodesAt(range: cursorPos.range) +// guard let token = token?.first else { +// return +// } +// print("Token \(token)") } catch { print("\(error)") return diff --git a/CodeEdit/Features/Editor/Models/EditorInstance.swift b/CodeEdit/Features/Editor/Models/EditorInstance.swift index eb50baa4b..2029bd45c 100644 --- a/CodeEdit/Features/Editor/Models/EditorInstance.swift +++ b/CodeEdit/Features/Editor/Models/EditorInstance.swift @@ -39,7 +39,7 @@ class EditorInstance: Hashable { self.file = file self.cursorSubject.send(cursorPositions) self.rangeTranslator = RangeTranslator(cursorSubject: cursorSubject) - self.autoCompleteCoordinator = AutoCompleteCoordinator() + self.autoCompleteCoordinator = AutoCompleteCoordinator(file) } func hash(into hasher: inout Hasher) { From a4326881729c47ccc00c98cedbf2206cf1ea72fd Mon Sep 17 00:00:00 2001 From: Abe M Date: Mon, 23 Dec 2024 04:25:57 -0800 Subject: [PATCH 4/8] Update cursor positioning --- .../Editor/AutoCompleteCoordinator.swift | 69 +++++++++++-------- CodeEdit/Features/LSP/LSPUtil.swift | 16 +++++ 2 files changed, 57 insertions(+), 28 deletions(-) diff --git a/CodeEdit/Features/Editor/AutoCompleteCoordinator.swift b/CodeEdit/Features/Editor/AutoCompleteCoordinator.swift index bea465144..6954697a9 100644 --- a/CodeEdit/Features/Editor/AutoCompleteCoordinator.swift +++ b/CodeEdit/Features/Editor/AutoCompleteCoordinator.swift @@ -11,10 +11,13 @@ import CodeEditSourceEditor import LanguageServerProtocol class AutoCompleteCoordinator: TextViewCoordinator { + /// A reference to the `TextViewController`, to be able to make edits private weak var textViewController: TextViewController? + /// A reference to the file we are working with, to be able to query file information private unowned var file: CEWorkspaceFile + /// The event monitor that looks for the keyboard shortcut to bring up the autocomplete menu private var localEventMonitor: Any? - + /// The `ItemBoxWindowController` lets us display the autocomplete items private var itemBoxController: ItemBoxWindowController? init(_ file: CEWorkspaceFile) { @@ -39,6 +42,7 @@ class AutoCompleteCoordinator: TextViewCoordinator { } } + /// Will query the language server for autocomplete suggestions and then display the window. @MainActor func showAutocompleteWindow() { guard let cursorPos = textViewController?.cursorPositions.first, @@ -67,14 +71,16 @@ class AutoCompleteCoordinator: TextViewCoordinator { @Service var lspService: LSPService guard let client = await lspService.languageClient( - for: language, workspacePath: workspacePath + for: language, + workspacePath: workspacePath ) else { return [] } do { let completions = try await client.requestCompletion( - for: file.url.absoluteURL.path(), position: position + for: file.url.absoluteURL.path(), + position: position ) // Extract the completion items list @@ -101,43 +107,50 @@ class AutoCompleteCoordinator: TextViewCoordinator { } extension AutoCompleteCoordinator: ItemBoxDelegate { + /// Takes a `CompletionItem` and modifies the text view with the new string func applyCompletionItem(_ item: CompletionItem) { guard let cursorPos = textViewController?.cursorPositions.first, let textView = textViewController?.textView else { return } - do { - let textPosition = Position( - line: cursorPos.line - 1, - character: cursorPos.column - 1 - ) - var textEdits = LSPCompletionItemsUtil.getCompletionItemEdits( - startPosition: textPosition, - item: item + let textPosition = Position( + line: cursorPos.line - 1, + character: cursorPos.column - 1 + ) + var textEdits = LSPCompletionItemsUtil.getCompletionItemEdits( + startPosition: textPosition, + item: item + ) + // Appropriately order the text edits + textEdits = TextEdit.makeApplicable(textEdits) + + // Make the updates + textView.undoManager?.beginUndoGrouping() + for textEdit in textEdits { + textView.replaceString( + in: cursorPos.range, + with: textEdit.newText ) - // Appropriately order the text edits - textEdits = TextEdit.makeApplicable(textEdits) - - // Make the updates - textView.undoManager?.beginUndoGrouping() - for textEdit in textEdits { - textView.replaceString( - in: NSRange(location: 0, length: 0), - with: textEdit.newText - ) - } - textView.undoManager?.endUndoGrouping() + } + textView.undoManager?.endUndoGrouping() -// textViewController?.textView.applyMutations(<#T##mutations: [TextMutation]##[TextMutation]#>) + // Set the cursor to the end of the completion + let insertText = LSPCompletionItemsUtil.getInsertText(from: item) + guard let newCursorPos = cursorPos.range.shifted(by: insertText.count) else { + return + } + textViewController?.setCursorPositions([CursorPosition(range: newCursorPos)]) + +// do { // let token = try textViewController?.treeSitterClient?.nodesAt(range: cursorPos.range) // guard let token = token?.first else { // return // } // print("Token \(token)") - } catch { - print("\(error)") - return - } +// } catch { +// print("\(error)") +// return +// } } } diff --git a/CodeEdit/Features/LSP/LSPUtil.swift b/CodeEdit/Features/LSP/LSPUtil.swift index 740a82104..b71b94553 100644 --- a/CodeEdit/Features/LSP/LSPUtil.swift +++ b/CodeEdit/Features/LSP/LSPUtil.swift @@ -38,6 +38,22 @@ enum LSPCompletionItemsUtil { return edits } + static func getInsertText(from completionItem: CompletionItem) -> String { + // According to LSP spec, textEdit takes precedence if present, then insertText, then label + if let textEdit = completionItem.textEdit { + switch textEdit { + case .optionA(let edit): + return edit.newText + case .optionB(let insertReplaceEdit): + return insertReplaceEdit.newText + } + } + if let insertText = completionItem.insertText { + return insertText + } + return completionItem.label + } + private static func editOrReplaceItem(edit: TwoTypeOption, _ edits: inout [TextEdit]) { switch edit { case .optionA(let textEdit): From e81ac208eb6b8ffbfd928fec07453e249d4145dd Mon Sep 17 00:00:00 2001 From: Abe M Date: Thu, 26 Dec 2024 18:00:01 -0800 Subject: [PATCH 5/8] UX updates --- CodeEdit.xcodeproj/project.pbxproj | 9 +- .../xcshareddata/swiftpm/Package.resolved | 9 + .../Editor/AutoCompleteCoordinator.swift | 170 +++++++++++++----- .../Features/LSP/Views/CompletionItem.swift | 5 +- 4 files changed, 143 insertions(+), 50 deletions(-) diff --git a/CodeEdit.xcodeproj/project.pbxproj b/CodeEdit.xcodeproj/project.pbxproj index 84e52222b..14af561e4 100644 --- a/CodeEdit.xcodeproj/project.pbxproj +++ b/CodeEdit.xcodeproj/project.pbxproj @@ -65,6 +65,7 @@ 3000516C2BBD3A9500A98562 /* ServiceWrapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3000516B2BBD3A9500A98562 /* ServiceWrapper.swift */; }; 3026F50F2AC006C80061227E /* InspectorAreaViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3026F50E2AC006C80061227E /* InspectorAreaViewModel.swift */; }; 3046374E2CB15FA900180667 /* AutoCompleteCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3046374D2CB15F9200180667 /* AutoCompleteCoordinator.swift */; }; + 3048523D2D182DA6000CD5CF /* CodeEditSourceEditor in Frameworks */ = {isa = PBXBuildFile; productRef = 6CC00A8A2CBEF150004E8134 /* CodeEditSourceEditor */; }; 30AB4EBB2BF718A100ED4431 /* DeveloperSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30AB4EBA2BF718A100ED4431 /* DeveloperSettings.swift */; }; 30AB4EBD2BF71CA800ED4431 /* DeveloperSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30AB4EBC2BF71CA800ED4431 /* DeveloperSettingsView.swift */; }; 30AB4EC22BF7253200ED4431 /* KeyValueTable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30AB4EC12BF7253200ED4431 /* KeyValueTable.swift */; }; @@ -458,8 +459,7 @@ 6CBA0D512A1BF524002C6FAA /* SegmentedControlImproved.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6CBA0D502A1BF524002C6FAA /* SegmentedControlImproved.swift */; }; 6CBD1BC62978DE53006639D5 /* Font+Caption3.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6CBD1BC52978DE53006639D5 /* Font+Caption3.swift */; }; 6CBE1CFB2B71DAA6003AC32E /* Loopable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6CBE1CFA2B71DAA6003AC32E /* Loopable.swift */; }; - 6CC00A8B2CBEF150004E8134 /* CodeEditSourceEditor in Frameworks */ = {isa = PBXBuildFile; productRef = 6CC00A8A2CBEF150004E8134 /* CodeEditSourceEditor */; }; - 6CC17B4F2C432AE000834E2C /* CodeEditSourceEditor in Frameworks */ = {isa = PBXBuildFile; productRef = 6CC17B4E2C432AE000834E2C /* CodeEditSourceEditor */; }; + 6CC17B4F2C432AE000834E2C /* (null) in Frameworks */ = {isa = PBXBuildFile; }; 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 */; }; @@ -1339,6 +1339,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + 3048523D2D182DA6000CD5CF /* CodeEditSourceEditor in Frameworks */, 6C85BB402C2105ED00EB5DEF /* CodeEditKit in Frameworks */, 6C66C31329D05CDC00DE9ED2 /* GRDB in Frameworks */, 58F2EB1E292FB954004A9BDE /* Sparkle in Frameworks */, @@ -1346,7 +1347,7 @@ 6CE21E872C650D2C0031B056 /* SwiftTerm in Frameworks */, 6CD3CA552C8B508200D83DCD /* (null) in Frameworks */, 6C0617D62BDB4432008C9C42 /* LogStream in Frameworks */, - 6CC17B4F2C432AE000834E2C /* CodeEditSourceEditor in Frameworks */, + 6CC17B4F2C432AE000834E2C /* (null) in Frameworks */, 30CB64912C16CA8100CC8A9E /* LanguageServerProtocol in Frameworks */, 6C4E37FC2C73E00700AEE7B5 /* SwiftTerm in Frameworks */, 6C6BD6F429CD142C00235D17 /* CollectionConcurrencyKit in Frameworks */, @@ -1354,7 +1355,6 @@ 6CB446402B6DFF3A00539ED0 /* CodeEditSourceEditor in Frameworks */, 6C05CF9E2CDE8699006AAECD /* CodeEditSourceEditor in Frameworks */, 2816F594280CF50500DD548B /* CodeEditSymbols in Frameworks */, - 6CD26C852C8F907800ADBA38 /* (null) in Frameworks */, 30CB64942C16CA9100CC8A9E /* LanguageClient in Frameworks */, 6C6BD6F829CD14D100235D17 /* CodeEditKit in Frameworks */, 6C0824A12C5C0C9700A0751E /* SwiftTerm in Frameworks */, @@ -3771,7 +3771,6 @@ 6C0617D52BDB4432008C9C42 /* LogStream */, 6C85BB3F2C2105ED00EB5DEF /* CodeEditKit */, 6C85BB432C210EFD00EB5DEF /* SwiftUIIntrospect */, - 6CC17B4E2C432AE000834E2C /* CodeEditSourceEditor */, 6C0824A02C5C0C9700A0751E /* SwiftTerm */, 6CE21E862C650D2C0031B056 /* SwiftTerm */, 6C4E37FB2C73E00700AEE7B5 /* SwiftTerm */, diff --git a/CodeEdit.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/CodeEdit.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 0eba906d6..5a7e2ce18 100644 --- a/CodeEdit.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/CodeEdit.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -37,6 +37,15 @@ "version" : "0.2.2" } }, + { + "identity" : "codeedittextview", + "kind" : "remoteSourceControl", + "location" : "https://github.com/CodeEditApp/CodeEditTextView.git", + "state" : { + "revision" : "509d7b2e86460e8ec15b0dd5410cbc8e8c05940f", + "version" : "0.7.7" + } + }, { "identity" : "collectionconcurrencykit", "kind" : "remoteSourceControl", diff --git a/CodeEdit/Features/Editor/AutoCompleteCoordinator.swift b/CodeEdit/Features/Editor/AutoCompleteCoordinator.swift index 6954697a9..945fdd52c 100644 --- a/CodeEdit/Features/Editor/AutoCompleteCoordinator.swift +++ b/CodeEdit/Features/Editor/AutoCompleteCoordinator.swift @@ -6,6 +6,7 @@ // import AppKit +import SwiftTreeSitter import CodeEditTextView import CodeEditSourceEditor import LanguageServerProtocol @@ -17,17 +18,19 @@ class AutoCompleteCoordinator: TextViewCoordinator { private unowned var file: CEWorkspaceFile /// The event monitor that looks for the keyboard shortcut to bring up the autocomplete menu private var localEventMonitor: Any? - /// The `ItemBoxWindowController` lets us display the autocomplete items - private var itemBoxController: ItemBoxWindowController? + /// The `SuggestionController` lets us display the autocomplete items + private var suggestionController: SuggestionController? + /// The current TreeSitter node that the main cursor is at + private var currentNode: SwiftTreeSitter.Node? init(_ file: CEWorkspaceFile) { self.file = file } func prepareCoordinator(controller: TextViewController) { - itemBoxController = ItemBoxWindowController() - itemBoxController?.delegate = self - itemBoxController?.close() + suggestionController = SuggestionController() + suggestionController?.delegate = self + suggestionController?.close() self.textViewController = controller localEventMonitor = NSEvent.addLocalMonitorForEvents(matching: .keyDown) { event in @@ -48,19 +51,48 @@ class AutoCompleteCoordinator: TextViewCoordinator { guard let cursorPos = textViewController?.cursorPositions.first, let textView = textViewController?.textView, let window = NSApplication.shared.keyWindow, - let itemBoxController = itemBoxController + let suggestionController = suggestionController else { return } + do { + if let token = try textViewController?.treeSitterClient?.nodesAt(range: cursorPos.range).first { + if tokenIsActionable(token.node) { + currentNode = token.node + } + + // Get the string from the start of the token to the location of the cursor + if cursorPos.range.location > token.node.range.location { + let selectedRange = NSRange( + location: token.node.range.location, + length: cursorPos.range.location - token.node.range.location + ) + let tokenSubstring = textView.textStorage?.substring(from: selectedRange) +// print("Token word: \(String(describing: tokenSubstring))") + } + } + } catch { + print("Error getting TreeSitter node: \(error)") + } + Task { let textPosition = Position(line: cursorPos.line - 1, character: cursorPos.column - 1) + // If we are asking for completions in the middle of a token, then + // query the language server for completion items at the start of the token +// if let currentNode = currentNode, tokenIsActionable(currentNode) { +// if let newPos = textView.lspRangeFrom(nsRange: currentNode.range) { +// _currentNode +// } +// } + print("Getting completion items at token position: \(textPosition)") + let completionItems = await fetchCompletions(position: textPosition) - itemBoxController.items = completionItems + suggestionController.items = completionItems let cursorRect = textView.firstRect(forCharacterRange: cursorPos.range, actualRange: nil) - itemBoxController.constrainWindowToScreenEdges(cursorRect: cursorRect) - itemBoxController.showWindow(attachedTo: window) + suggestionController.constrainWindowToScreenEdges(cursorRect: cursorRect) + suggestionController.showWindow(attachedTo: window) } } @@ -97,8 +129,24 @@ class AutoCompleteCoordinator: TextViewCoordinator { } } + /// Determines if a TreeSitter node is a type where we can build featues off of. This helps filter out + /// nodes that represent blank spaces or other information that is not useful. + private func tokenIsActionable(_ node: SwiftTreeSitter.Node) -> Bool { + // List of node types that should have their text be replaced + let replaceableTypes: Set = [ + "identifier", + "property_identifier", + "field_identifier", + "variable_name", + "method_name", + "function_name", + "type_identifier" + ] + return replaceableTypes.contains(node.nodeType ?? "") + } + deinit { - itemBoxController?.close() + suggestionController?.close() if let localEventMonitor = localEventMonitor { NSEvent.removeMonitor(localEventMonitor) self.localEventMonitor = nil @@ -106,51 +154,89 @@ class AutoCompleteCoordinator: TextViewCoordinator { } } -extension AutoCompleteCoordinator: ItemBoxDelegate { +extension AutoCompleteCoordinator: SuggestionControllerDelegate { /// Takes a `CompletionItem` and modifies the text view with the new string - func applyCompletionItem(_ item: CompletionItem) { + func applyCompletionItem(item: CompletionItem) { guard let cursorPos = textViewController?.cursorPositions.first, let textView = textViewController?.textView else { return } - let textPosition = Position( - line: cursorPos.line - 1, - character: cursorPos.column - 1 - ) - var textEdits = LSPCompletionItemsUtil.getCompletionItemEdits( - startPosition: textPosition, - item: item - ) - // Appropriately order the text edits - textEdits = TextEdit.makeApplicable(textEdits) + // Get the token the cursor is currently on. Here we will check if we want to + // replace the current token we are on or just add text onto it. + var replacementRange = cursorPos.range + do { + if let token = try textViewController?.treeSitterClient?.nodesAt(range: cursorPos.range).first { + if tokenIsActionable(token.node) { + replacementRange = token.node.range + } + } + } catch { + print("Error getting TreeSitter node: \(error)") + } // Make the updates + let insertText = LSPCompletionItemsUtil.getInsertText(from: item) textView.undoManager?.beginUndoGrouping() - for textEdit in textEdits { - textView.replaceString( - in: cursorPos.range, - with: textEdit.newText - ) - } + textView.replaceString(in: replacementRange, with: insertText) textView.undoManager?.endUndoGrouping() - // Set the cursor to the end of the completion - let insertText = LSPCompletionItemsUtil.getInsertText(from: item) - guard let newCursorPos = cursorPos.range.shifted(by: insertText.count) else { + // Set cursor position to end of inserted text + let newCursorRange = NSRange(location: replacementRange.location + insertText.count, length: 0) + textViewController?.setCursorPositions([CursorPosition(range: newCursorRange)]) + + self.onCompletion() + } + + func onCompletion() { + + } + + func onCursorMove() { + guard let cursorPos = textViewController?.cursorPositions.first, + let suggestionController = suggestionController, + let textView = self.textViewController?.textView, + suggestionController.isVisible + else { + return + } + guard let currentNode = currentNode, + !suggestionController.items.isEmpty else { + self.suggestionController?.close() return } - textViewController?.setCursorPositions([CursorPosition(range: newCursorPos)]) -// do { -// let token = try textViewController?.treeSitterClient?.nodesAt(range: cursorPos.range) -// guard let token = token?.first else { -// return -// } -// print("Token \(token)") -// } catch { -// print("\(error)") -// return -// } + do { + if let token = try textViewController?.treeSitterClient?.nodesAt(range: cursorPos.range).first { + // Moving to a new token requires a new call to the language server + // We extend the range so that the `contains` can include the end value of + // the token, since its check is exclusive. + let adjustedRange = currentNode.range.shifted(endBy: 1) + if let adjustedRange = adjustedRange, + !adjustedRange.contains(cursorPos.range.location) { + suggestionController.close() + return + } + + // 1. Print cursor position and token range + print("Current node: \(String(describing: currentNode))") + print("Cursor pos: \(cursorPos.range.location) : Line: \(cursorPos.line) Col: \(cursorPos.column)") + + // Get the token string from the start of the token to the location of the cursor +// print("Token contains cursor position: \(String(describing: currentNode.range.contains(cursorPos.range.location)))") +// print("Token info: \(String(describing: tokenSubstring)) Range: \(String(describing: adjustedRange))") +// print("Current cursor position: \(cursorPos.range)") + } + } catch { + print("Error getting TreeSitter node: \(error)") + } + } + + func onItemSelect(item: LanguageServerProtocol.CompletionItem) { + + } + + func onClose() { + currentNode = nil } } diff --git a/CodeEdit/Features/LSP/Views/CompletionItem.swift b/CodeEdit/Features/LSP/Views/CompletionItem.swift index 8d7bd3bf4..4e1c938f1 100644 --- a/CodeEdit/Features/LSP/Views/CompletionItem.swift +++ b/CodeEdit/Features/LSP/Views/CompletionItem.swift @@ -6,9 +6,8 @@ // import SwiftUI -import CodeEditTextView -import LanguageServerProtocol import CodeEditSourceEditor +import LanguageServerProtocol // TODO: REMOVE Y OFFSET ON 16 PX? @@ -40,7 +39,7 @@ let fontSizeToRightPadding: [CGFloat: CGFloat] = [ 18: 12.5, ] -extension CompletionItem: @retroactive ItemBoxEntry { +extension CompletionItem: @retroactive CodeSuggestionEntry { public var view: NSView { NSHostingView( rootView: HStack(spacing: 0) { From 8e29a0cf7f34c63dfdc89a95f62790f09d49da7b Mon Sep 17 00:00:00 2001 From: Abe M Date: Sun, 29 Dec 2024 05:11:40 -0800 Subject: [PATCH 6/8] Added item filtering based on cursor position --- .../Editor/AutoCompleteCoordinator.swift | 138 ++++++++++-------- .../LSP/Service/LSPService+Events.swift | 82 +++++------ .../Features/LSP/Service/LSPService.swift | 2 +- 3 files changed, 121 insertions(+), 101 deletions(-) diff --git a/CodeEdit/Features/Editor/AutoCompleteCoordinator.swift b/CodeEdit/Features/Editor/AutoCompleteCoordinator.swift index 945fdd52c..e5aa0bb66 100644 --- a/CodeEdit/Features/Editor/AutoCompleteCoordinator.swift +++ b/CodeEdit/Features/Editor/AutoCompleteCoordinator.swift @@ -22,6 +22,10 @@ class AutoCompleteCoordinator: TextViewCoordinator { private var suggestionController: SuggestionController? /// The current TreeSitter node that the main cursor is at private var currentNode: SwiftTreeSitter.Node? + /// The current filter text based on partial token input + private var currentFilterText: String = "" + /// Stores the unfiltered completion items + private var completionItems: [CompletionItem] = [] init(_ file: CEWorkspaceFile) { self.file = file @@ -56,20 +60,24 @@ class AutoCompleteCoordinator: TextViewCoordinator { return } + var tokenSubstringCount = 0 + currentFilterText = "" do { if let token = try textViewController?.treeSitterClient?.nodesAt(range: cursorPos.range).first { if tokenIsActionable(token.node) { currentNode = token.node - } - // Get the string from the start of the token to the location of the cursor - if cursorPos.range.location > token.node.range.location { - let selectedRange = NSRange( - location: token.node.range.location, - length: cursorPos.range.location - token.node.range.location - ) - let tokenSubstring = textView.textStorage?.substring(from: selectedRange) -// print("Token word: \(String(describing: tokenSubstring))") + // Get the string from the start of the token to the location of the cursor + if cursorPos.range.location > token.node.range.location { + let selectedRange = NSRange( + location: token.node.range.location, + length: cursorPos.range.location - token.node.range.location + ) + if let tokenSubstring = textView.textStorage?.substring(from: selectedRange) { + currentFilterText = tokenSubstring + tokenSubstringCount = tokenSubstring.count + } + } } } } catch { @@ -77,21 +85,24 @@ class AutoCompleteCoordinator: TextViewCoordinator { } Task { - let textPosition = Position(line: cursorPos.line - 1, character: cursorPos.column - 1) + var textPosition = Position(line: cursorPos.line - 1, character: cursorPos.column - 1) // If we are asking for completions in the middle of a token, then // query the language server for completion items at the start of the token -// if let currentNode = currentNode, tokenIsActionable(currentNode) { -// if let newPos = textView.lspRangeFrom(nsRange: currentNode.range) { -// _currentNode -// } -// } - print("Getting completion items at token position: \(textPosition)") - - let completionItems = await fetchCompletions(position: textPosition) - suggestionController.items = completionItems + if currentNode != nil { + textPosition = Position( + line: cursorPos.line - 1, + character: cursorPos.column - tokenSubstringCount - 1 + ) + } + completionItems = await fetchCompletions(position: textPosition) + suggestionController.items = filterCompletionItems(completionItems) let cursorRect = textView.firstRect(forCharacterRange: cursorPos.range, actualRange: nil) - suggestionController.constrainWindowToScreenEdges(cursorRect: cursorRect) + suggestionController.constrainWindowToScreenEdges( + cursorRect: cursorRect, + // TODO: CALCULATE PADDING BASED ON FONT SIZE, THIS IS JUST TEMP + horizontalOffset: 13 + 16.5 + CGFloat(tokenSubstringCount) * 7.4 + ) suggestionController.showWindow(attachedTo: window) } } @@ -129,6 +140,26 @@ class AutoCompleteCoordinator: TextViewCoordinator { } } + /// Filters completion items based on the current partial token input + private func filterCompletionItems(_ items: [CompletionItem]) -> [CompletionItem] { + guard !currentFilterText.isEmpty else { + return items + } + + return items.filter { item in + let insertText = LSPCompletionItemsUtil.getInsertText(from: item) + let label = item.label.lowercased() + let filterText = currentFilterText.lowercased() + if insertText.lowercased().hasPrefix(filterText) { + return true + } + if label.hasPrefix(filterText) { + return true + } + return false + } + } + /// Determines if a TreeSitter node is a type where we can build featues off of. This helps filter out /// nodes that represent blank spaces or other information that is not useful. private func tokenIsActionable(_ node: SwiftTreeSitter.Node) -> Bool { @@ -162,20 +193,8 @@ extension AutoCompleteCoordinator: SuggestionControllerDelegate { return } - // Get the token the cursor is currently on. Here we will check if we want to - // replace the current token we are on or just add text onto it. - var replacementRange = cursorPos.range - do { - if let token = try textViewController?.treeSitterClient?.nodesAt(range: cursorPos.range).first { - if tokenIsActionable(token.node) { - replacementRange = token.node.range - } - } - } catch { - print("Error getting TreeSitter node: \(error)") - } - // Make the updates + let replacementRange = currentNode?.range ?? cursorPos.range let insertText = LSPCompletionItemsUtil.getInsertText(from: item) textView.undoManager?.beginUndoGrouping() textView.replaceString(in: replacementRange, with: insertText) @@ -188,9 +207,7 @@ extension AutoCompleteCoordinator: SuggestionControllerDelegate { self.onCompletion() } - func onCompletion() { - - } + func onCompletion() { } func onCursorMove() { guard let cursorPos = textViewController?.cursorPositions.first, @@ -206,37 +223,40 @@ extension AutoCompleteCoordinator: SuggestionControllerDelegate { return } - do { - if let token = try textViewController?.treeSitterClient?.nodesAt(range: cursorPos.range).first { - // Moving to a new token requires a new call to the language server - // We extend the range so that the `contains` can include the end value of - // the token, since its check is exclusive. - let adjustedRange = currentNode.range.shifted(endBy: 1) - if let adjustedRange = adjustedRange, - !adjustedRange.contains(cursorPos.range.location) { - suggestionController.close() - return - } + // Moving to a new token requires a new call to the language server + // We extend the range so that the `contains` can include the end value of + // the token, since its check is exclusive. + let adjustedRange = currentNode.range.shifted(endBy: 1) + if let adjustedRange = adjustedRange, + !adjustedRange.contains(cursorPos.range.location) { + suggestionController.close() + return + } - // 1. Print cursor position and token range - print("Current node: \(String(describing: currentNode))") - print("Cursor pos: \(cursorPos.range.location) : Line: \(cursorPos.line) Col: \(cursorPos.column)") + // Check if cursor is at the start of the token + if cursorPos.range.location == currentNode.range.location { + currentFilterText = "" + suggestionController.items = completionItems + return + } - // Get the token string from the start of the token to the location of the cursor -// print("Token contains cursor position: \(String(describing: currentNode.range.contains(cursorPos.range.location)))") -// print("Token info: \(String(describing: tokenSubstring)) Range: \(String(describing: adjustedRange))") -// print("Current cursor position: \(cursorPos.range)") + // Filter through the completion items based on how far the cursor is in the token + if cursorPos.range.location > currentNode.range.location { + let selectedRange = NSRange( + location: currentNode.range.location, + length: cursorPos.range.location - currentNode.range.location + ) + if let tokenSubstring = textView.textStorage?.substring(from: selectedRange) { + currentFilterText = tokenSubstring + suggestionController.items = filterCompletionItems(completionItems) } - } catch { - print("Error getting TreeSitter node: \(error)") } } - func onItemSelect(item: LanguageServerProtocol.CompletionItem) { - - } + func onItemSelect(item: LanguageServerProtocol.CompletionItem) { } func onClose() { currentNode = nil + currentFilterText = "" } } diff --git a/CodeEdit/Features/LSP/Service/LSPService+Events.swift b/CodeEdit/Features/LSP/Service/LSPService+Events.swift index 1d5bee6fe..c6443ddec 100644 --- a/CodeEdit/Features/LSP/Service/LSPService+Events.swift +++ b/CodeEdit/Features/LSP/Service/LSPService+Events.swift @@ -46,50 +46,50 @@ extension LSPService { private func handleRequest(_ request: ServerRequest) { // TODO: Handle Requests - switch request { - case let .workspaceConfiguration(params, _): - print("workspaceConfiguration: \(params)") - case let .workspaceFolders(handler): - print("workspaceFolders: \(String(describing: handler))") - case let .workspaceApplyEdit(params, _): - print("workspaceApplyEdit: \(params)") - case let .clientRegisterCapability(params, _): - print("clientRegisterCapability: \(params)") - case let .clientUnregisterCapability(params, _): - print("clientUnregisterCapability: \(params)") - case let .workspaceCodeLensRefresh(handler): - print("workspaceCodeLensRefresh: \(String(describing: handler))") - case let .workspaceSemanticTokenRefresh(handler): - print("workspaceSemanticTokenRefresh: \(String(describing: handler))") - case let .windowShowMessageRequest(params, _): - print("windowShowMessageRequest: \(params)") - case let .windowShowDocument(params, _): - print("windowShowDocument: \(params)") - case let .windowWorkDoneProgressCreate(params, _): - print("windowWorkDoneProgressCreate: \(params)") - - default: - print() - } +// switch request { +// case let .workspaceConfiguration(params, _): +// print("workspaceConfiguration: \(params)") +// case let .workspaceFolders(handler): +// print("workspaceFolders: \(String(describing: handler))") +// case let .workspaceApplyEdit(params, _): +// print("workspaceApplyEdit: \(params)") +// case let .clientRegisterCapability(params, _): +// print("clientRegisterCapability: \(params)") +// case let .clientUnregisterCapability(params, _): +// print("clientUnregisterCapability: \(params)") +// case let .workspaceCodeLensRefresh(handler): +// print("workspaceCodeLensRefresh: \(String(describing: handler))") +// case let .workspaceSemanticTokenRefresh(handler): +// print("workspaceSemanticTokenRefresh: \(String(describing: handler))") +// case let .windowShowMessageRequest(params, _): +// print("windowShowMessageRequest: \(params)") +// case let .windowShowDocument(params, _): +// print("windowShowDocument: \(params)") +// case let .windowWorkDoneProgressCreate(params, _): +// print("windowWorkDoneProgressCreate: \(params)") +// +// default: +// print() +// } } private func handleNotification(_ notification: ServerNotification) { // TODO: Handle Notifications - switch notification { - case let .windowLogMessage(params): - print("windowLogMessage \(params.type)\n```\n\(params.message)\n```\n") - case let .windowShowMessage(params): - print("windowShowMessage \(params.type)\n```\n\(params.message)\n```\n") - case let .textDocumentPublishDiagnostics(params): - print("textDocumentPublishDiagnostics: \(params)") - case let .telemetryEvent(params): - print("telemetryEvent: \(params)") - case let .protocolCancelRequest(params): - print("protocolCancelRequest: \(params)") - case let .protocolProgress(params): - print("protocolProgress: \(params)") - case let .protocolLogTrace(params): - print("protocolLogTrace: \(params)") - } +// switch notification { +// case let .windowLogMessage(params): +// print("windowLogMessage \(params.type)\n```\n\(params.message)\n```\n") +// case let .windowShowMessage(params): +// print("windowShowMessage \(params.type)\n```\n\(params.message)\n```\n") +// case let .textDocumentPublishDiagnostics(params): +// print("textDocumentPublishDiagnostics: \(params)") +// case let .telemetryEvent(params): +// print("telemetryEvent: \(params)") +// case let .protocolCancelRequest(params): +// print("protocolCancelRequest: \(params)") +// case let .protocolProgress(params): +// print("protocolProgress: \(params)") +// case let .protocolLogTrace(params): +// print("protocolLogTrace: \(params)") +// } } } diff --git a/CodeEdit/Features/LSP/Service/LSPService.swift b/CodeEdit/Features/LSP/Service/LSPService.swift index 9f0cc7c80..01b3dd0ad 100644 --- a/CodeEdit/Features/LSP/Service/LSPService.swift +++ b/CodeEdit/Features/LSP/Service/LSPService.swift @@ -246,7 +246,7 @@ final class LSPService: ObservableObject { do { try await languageServer.openDocument(document) } catch { - let uri = await document.languageServerURI + let uri = document.languageServerURI // swiftlint:disable:next line_length self.logger.error("Failed to close document: \(uri ?? "", privacy: .private), language: \(lspLanguage.rawValue). Error \(error)") } From 336cab08e973f60df07aed508e12112bd9c5783d Mon Sep 17 00:00:00 2001 From: Abe M Date: Sun, 29 Dec 2024 17:12:37 -0800 Subject: [PATCH 7/8] Added CodeSuggestionEntry types to CESE --- .../xcshareddata/swiftpm/Package.resolved | 24 +++++++++---------- .../Editor/AutoCompleteCoordinator.swift | 5 ++-- .../Settings/Views/ExternalLink.swift | 2 +- 3 files changed, 16 insertions(+), 15 deletions(-) diff --git a/CodeEdit.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/CodeEdit.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 5a7e2ce18..3a0778c34 100644 --- a/CodeEdit.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/CodeEdit.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -114,8 +114,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/ChimeHQ/LanguageServerProtocol", "state" : { - "revision" : "ac76fccf0e981c8e30c5ee4de1b15adc1decd697", - "version" : "0.13.2" + "revision" : "d51412945ae88ffcab65ec339ca89aed9c9f0b8a", + "version" : "0.13.3" } }, { @@ -141,8 +141,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/mattmassicotte/Queue", "state" : { - "revision" : "8d6f936097888f97011610ced40313655dc5948d", - "version" : "0.1.4" + "revision" : "6adf359a705e3252742905b413bb8f56401043ca", + "version" : "0.2.0" } }, { @@ -159,8 +159,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/groue/Semaphore", "state" : { - "revision" : "f1c4a0acabeb591068dea6cffdd39660b86dec28", - "version" : "0.0.8" + "revision" : "2543679282aa6f6c8ecf2138acd613ed20790bc2", + "version" : "0.1.0" } }, { @@ -186,8 +186,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-collections.git", "state" : { - "revision" : "ee97538f5b81ae89698fd95938896dec5217b148", - "version" : "1.1.1" + "revision" : "671108c96644956dddcd89dd59c203dcdb36cec7", + "version" : "1.1.4" } }, { @@ -213,8 +213,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/lukepistrol/SwiftLintPlugin", "state" : { - "revision" : "5a65f4074975f811da666dfe31a19850950b1ea4", - "version" : "0.56.2" + "revision" : "87454f5c9ff4d644086aec2a0df1ffba678e7f3c", + "version" : "0.57.1" } }, { @@ -239,8 +239,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/siteline/SwiftUI-Introspect.git", "state" : { - "revision" : "668a65735751432b640260c56dfa621cec568368", - "version" : "1.2.0" + "revision" : "807f73ce09a9b9723f12385e592b4e0aaebd3336", + "version" : "1.3.0" } }, { diff --git a/CodeEdit/Features/Editor/AutoCompleteCoordinator.swift b/CodeEdit/Features/Editor/AutoCompleteCoordinator.swift index e5aa0bb66..8d1a3a43f 100644 --- a/CodeEdit/Features/Editor/AutoCompleteCoordinator.swift +++ b/CodeEdit/Features/Editor/AutoCompleteCoordinator.swift @@ -187,8 +187,9 @@ class AutoCompleteCoordinator: TextViewCoordinator { extension AutoCompleteCoordinator: SuggestionControllerDelegate { /// Takes a `CompletionItem` and modifies the text view with the new string - func applyCompletionItem(item: CompletionItem) { + func applyCompletionItem(item: CodeSuggestionEntry) { guard let cursorPos = textViewController?.cursorPositions.first, + let item = item as? CompletionItem, let textView = textViewController?.textView else { return } @@ -253,7 +254,7 @@ extension AutoCompleteCoordinator: SuggestionControllerDelegate { } } - func onItemSelect(item: LanguageServerProtocol.CompletionItem) { } + func onItemSelect(item: CodeSuggestionEntry) { } func onClose() { currentNode = nil diff --git a/CodeEdit/Features/Settings/Views/ExternalLink.swift b/CodeEdit/Features/Settings/Views/ExternalLink.swift index 9995df553..4a6453184 100644 --- a/CodeEdit/Features/Settings/Views/ExternalLink.swift +++ b/CodeEdit/Features/Settings/Views/ExternalLink.swift @@ -55,7 +55,7 @@ struct ExternalLink: View { @ViewBuilder content: @escaping () -> Content, title: String? = nil, subtitle: String? = nil, - @ViewBuilder icon: @escaping() -> Icon = { EmptyView() } + @ViewBuilder icon: @escaping () -> Icon = { EmptyView() } ) { self.showInFinder = showInFinder self.title = title From 48a7fd64405a598684a27b11d989b7c813343545 Mon Sep 17 00:00:00 2001 From: Abe M Date: Sun, 29 Dec 2024 17:27:07 -0800 Subject: [PATCH 8/8] Remove completion example --- .../Features/LSP/Service/LSPService.swift | 63 ------------------- 1 file changed, 63 deletions(-) diff --git a/CodeEdit/Features/LSP/Service/LSPService.swift b/CodeEdit/Features/LSP/Service/LSPService.swift index 01b3dd0ad..9d07ab18b 100644 --- a/CodeEdit/Features/LSP/Service/LSPService.swift +++ b/CodeEdit/Features/LSP/Service/LSPService.swift @@ -34,69 +34,6 @@ import CodeEditLanguages /// ) /// try await lspService.stopServer(for: .python) /// ``` -/// -/// ## Completion Example -/// -/// ```swift -/// func testCompletion() async throws { -/// do { -/// guard var languageClient = self.languageClient(for: .python) else { -/// print("Failed to get client") -/// throw ServerManagerError.languageClientNotFound -/// } -/// -/// let testFilePathStr = "" -/// let testFileURL = URL(fileURLWithPath: testFilePathStr) -/// -/// // Tell server we opened a document -/// _ = await languageClient.addDocument(testFileURL) -/// -/// // Completion example -/// let textPosition = Position(line: 32, character: 18) // Lines and characters start at 0 -/// let completions = try await languageClient.requestCompletion( -/// document: testFileURL.absoluteString, -/// position: textPosition -/// ) -/// switch completions { -/// case .optionA(let completionItems): -/// // Handle the case where completions is an array of CompletionItem -/// print("\n*******\nCompletion Items:\n*******\n") -/// for item in completionItems { -/// let textEdits = LSPCompletionItemsUtil.getCompletionItemEdits( -/// startPosition: textPosition, -/// item: item -/// ) -/// for edit in textEdits { -/// print(edit) -/// } -/// } -/// -/// case .optionB(let completionList): -/// // Handle the case where completions is a CompletionList -/// print("\n*******\nCompletion Items:\n*******\n") -/// for item in completionList.items { -/// let textEdits = LSPCompletionItemsUtil.getCompletionItemEdits( -/// startPosition: textPosition, -/// item: item -/// ) -/// for edit in textEdits { -/// print(edit) -/// } -/// } -/// -/// print(completionList.items[0]) -/// -/// case .none: -/// print("No completions found") -/// } -/// -/// // Close the document -/// _ = await languageClient.closeDocument(testFilePathStr) -/// } catch { -/// print(error) -/// } -/// } -/// ``` @MainActor final class LSPService: ObservableObject { let logger: Logger = Logger(subsystem: Bundle.main.bundleIdentifier ?? "", category: "LSPService")