Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Autocomplete Implementation #1949

Open
wants to merge 10 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 24 additions & 14 deletions CodeEdit.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
archiveVersion = 1;
classes = {
};
objectVersion = 55;
objectVersion = 70;
objects = {

/* Begin PBXBuildFile section */
Expand Down Expand Up @@ -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 */; };
Expand Down Expand Up @@ -474,7 +475,7 @@
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 */; };
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 */; };
Expand Down Expand Up @@ -761,6 +762,7 @@
300051692BBD3A8200A98562 /* ServiceType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServiceType.swift; sourceTree = "<group>"; };
3000516B2BBD3A9500A98562 /* ServiceWrapper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServiceWrapper.swift; sourceTree = "<group>"; };
3026F50E2AC006C80061227E /* InspectorAreaViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InspectorAreaViewModel.swift; sourceTree = "<group>"; };
3046374D2CB15F9200180667 /* AutoCompleteCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AutoCompleteCoordinator.swift; sourceTree = "<group>"; };
30AB4EBA2BF718A100ED4431 /* DeveloperSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeveloperSettings.swift; sourceTree = "<group>"; };
30AB4EBC2BF71CA800ED4431 /* DeveloperSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeveloperSettingsView.swift; sourceTree = "<group>"; };
30AB4EC12BF7253200ED4431 /* KeyValueTable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyValueTable.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -1321,6 +1323,10 @@
EC0870F62A455F6400EB8692 /* ProjectNavigatorViewController+NSMenuDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ProjectNavigatorViewController+NSMenuDelegate.swift"; sourceTree = "<group>"; };
/* End PBXFileReference section */

/* Begin PBXFileSystemSynchronizedRootGroup section */
302EFC1F2CC3C034004A74DF /* Views */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); path = Views; sourceTree = "<group>"; };
/* End PBXFileSystemSynchronizedRootGroup section */

/* Begin PBXFrameworksBuildPhase section */
2BE487E928245162003F3F64 /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
Expand All @@ -1338,8 +1344,7 @@
58F2EB1E292FB954004A9BDE /* Sparkle in Frameworks */,
6C147C4529A329350089B630 /* OrderedCollections in Frameworks */,
6CE21E872C650D2C0031B056 /* SwiftTerm in Frameworks */,
6CC00A8B2CBEF150004E8134 /* CodeEditSourceEditor in Frameworks */,
6CD3CA552C8B508200D83DCD /* CodeEditSourceEditor in Frameworks */,
6CD3CA552C8B508200D83DCD /* (null) in Frameworks */,
6C0617D62BDB4432008C9C42 /* LogStream in Frameworks */,
6CC17B4F2C432AE000834E2C /* CodeEditSourceEditor in Frameworks */,
30CB64912C16CA8100CC8A9E /* LanguageServerProtocol in Frameworks */,
Expand All @@ -1349,6 +1354,7 @@
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 */,
Expand Down Expand Up @@ -1600,6 +1606,7 @@
6C3B4CD22D0E2C5400C6759E /* Editor */,
6CD26C732C8EA71F00ADBA38 /* LanguageServer */,
6CD26C742C8EA79100ADBA38 /* Service */,
302EFC1F2CC3C034004A74DF /* Views */,
30B087FA2C0D53080063A882 /* LSPUtil.swift */,
);
path = LSP;
Expand Down Expand Up @@ -2868,6 +2875,7 @@
287776EB27E350BA00D46668 /* TabBar */,
B67660642AA970ED00CD56B0 /* Models */,
B67660632AA970E300CD56B0 /* Views */,
3046374D2CB15F9200180667 /* AutoCompleteCoordinator.swift */,
);
path = Editor;
sourceTree = "<group>";
Expand Down Expand Up @@ -3747,6 +3755,9 @@
6C7B1C762A1D57CE005CBBFC /* PBXTargetDependency */,
2BE487F328245162003F3F64 /* PBXTargetDependency */,
);
fileSystemSynchronizedGroups = (
302EFC1F2CC3C034004A74DF /* Views */,
);
name = CodeEdit;
packageProductDependencies = (
2816F593280CF50500DD548B /* CodeEditSymbols */,
Expand All @@ -3764,7 +3775,6 @@
6C0824A02C5C0C9700A0751E /* SwiftTerm */,
6CE21E862C650D2C0031B056 /* SwiftTerm */,
6C4E37FB2C73E00700AEE7B5 /* SwiftTerm */,
6CD3CA542C8B508200D83DCD /* CodeEditSourceEditor */,
6CB94D022CA1205100E8651C /* AsyncAlgorithms */,
6CC00A8A2CBEF150004E8134 /* CodeEditSourceEditor */,
6C05CF9D2CDE8699006AAECD /* CodeEditSourceEditor */,
Expand Down Expand Up @@ -3865,7 +3875,7 @@
303E88462C276FD600EEA8D9 /* XCRemoteSwiftPackageReference "LanguageServerProtocol" */,
6C4E37FA2C73E00700AEE7B5 /* XCRemoteSwiftPackageReference "SwiftTerm" */,
6CB94D012CA1205100E8651C /* XCRemoteSwiftPackageReference "swift-async-algorithms" */,
6C05CF9C2CDE8699006AAECD /* XCRemoteSwiftPackageReference "CodeEditSourceEditor" */,
302EFBFB2CC3284D004A74DF /* XCLocalSwiftPackageReference "../CodeEditSourceEditor" */,
);
productRefGroup = B658FB2D27DA9E0F00EA4DBD /* Products */;
projectDirPath = "";
Expand Down Expand Up @@ -4247,6 +4257,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 */,
Expand Down Expand Up @@ -5654,6 +5665,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;
Expand Down Expand Up @@ -5890,14 +5908,6 @@
isa = XCSwiftPackageProductDependency;
productName = CodeEditSourceEditor;
};
6CC17B4E2C432AE000834E2C /* CodeEditSourceEditor */ = {
isa = XCSwiftPackageProductDependency;
productName = CodeEditSourceEditor;
};
6CD3CA542C8B508200D83DCD /* CodeEditSourceEditor */ = {
isa = XCSwiftPackageProductDependency;
productName = CodeEditSourceEditor;
};
6CE21E862C650D2C0031B056 /* SwiftTerm */ = {
isa = XCSwiftPackageProductDependency;
productName = SwiftTerm;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"originHash" : "ac57a6899925c3e4ac6d43aed791c845c6fc24a4441b6a10297a207d951b7836",
"originHash" : "bb72acfad31b288599b6721256b508d8209ba1bc1d7ab0fff6a358d49a1deae0",
"pins" : [
{
"identity" : "anycodable",
Expand All @@ -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"
Expand All @@ -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",
Expand All @@ -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",
Expand Down Expand Up @@ -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"
}
},
{
Expand All @@ -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"
}
},
{
Expand Down Expand Up @@ -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"
}
},
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ import Foundation
import SwiftUI
import UniformTypeIdentifiers
import CodeEditSourceEditor
import CodeEditTextView
import CodeEditLanguages
import Combine
import OSLog
Expand Down
156 changes: 156 additions & 0 deletions CodeEdit/Features/Editor/AutoCompleteCoordinator.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
//
// AutoCompleteCoordinator.swift
// CodeEdit
//
// Created by Abe Malla on 9/20/24.
//

import AppKit
import CodeEditTextView
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) {
self.file = file
}

func prepareCoordinator(controller: TextViewController) {
itemBoxController = ItemBoxWindowController()
itemBoxController?.delegate = self
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 == " " {
Task {
await self.showAutocompleteWindow()
}
return nil
}
return event
}
}

/// Will query the language server for autocomplete suggestions and then display the window.
@MainActor
func showAutocompleteWindow() {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I mentioned it in the review of the CETV pr, but this function should be moved to CESE. That should simplify this type, as you could instead add a parameter to CESE for an optional ItemBoxDelegate, and ignore the unnecessary TextViewCoordinator conformance. Then make the ItemBoxDelegate pass the text controller when it inserts items so you can still make that range conversion on line 111.

That does make it a little harder to get this object down to be visible to the CodeFileView, but check how it works with the content coordinator. I tried to make it so it's extendable for new types like this.

  • Add a published property to CodeFileDocument
  • Extend the type in LanguageServerFileMap to contain this object
  • When opening a document, set this object in the CodeFileDocument's new published property

guard let cursorPos = textViewController?.cursorPositions.first,
let textView = textViewController?.textView,
let window = NSApplication.shared.keyWindow,
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 []
}

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 {
itemBoxController?.close()
if let localEventMonitor = localEventMonitor {
NSEvent.removeMonitor(localEventMonitor)
self.localEventMonitor = nil
}
}
}

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
}

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
)
}
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 {
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
// }
}
}
Loading
Loading