-
-
Notifications
You must be signed in to change notification settings - Fork 1k
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
FastestMolasses
wants to merge
10
commits into
main
Choose a base branch
from
itembox
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from 6 commits
Commits
Show all changes
10 commits
Select commit
Hold shift + click to select a range
ab126d5
Autocomplete Coordinator
FastestMolasses 4d24153
Merge branch 'main' into itembox
FastestMolasses 02fb3b5
ItemBox updates
FastestMolasses 6f80528
Merge branch 'main' into itembox
FastestMolasses a2de090
Autocomplete updates
FastestMolasses a432688
Update cursor positioning
FastestMolasses e81ac20
UX updates
FastestMolasses 8e29a0c
Added item filtering based on cursor position
FastestMolasses 336cab0
Added CodeSuggestionEntry types to CESE
FastestMolasses 48a7fd6
Remove completion example
FastestMolasses File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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() { | ||
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 | ||
// } | ||
} | ||
} |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
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 unnecessaryTextViewCoordinator
conformance. Then make theItemBoxDelegate
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.LanguageServerFileMap
to contain this objectCodeFileDocument
's new published property