-
-
Notifications
You must be signed in to change notification settings - Fork 3.2k
feat(lsp): add textDocument/inlineCompletion support #14876
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
base: master
Are you sure you want to change the base?
feat(lsp): add textDocument/inlineCompletion support #14876
Conversation
3417d52 to
dcde385
Compare
|
I'm doing a refactoring simplifying the code |
Implement LSP 3.18 inline completion (ghost text) feature: - Add InlineCompletion to LanguageServerFeature enum - Remove 'proposed' feature gate from lsp-types (official in LSP 3.18) - Add inline_completion client capability and request method - Store inline completion as InlineAnnotation on Document - Render ghost text via text_annotations system - Add inline_completion_accept and inline_completion_dismiss commands - Auto-trigger on document change with configurable debounce (inline_completion_timeout) - Clear completion on entering normal mode
dcde385 to
166dc56
Compare
|
Nice!!! Can't wait to clone and try it out! |
|
Is this sufficient to get copilot-language-server working? If so, if someone configures it successfully, it would be useful to get a "bootstrap" config posted here. (It seems copilot-language-server also requires other endpoints like |
|
@cunha of course we already have them. Tomorrow I will sign up github copilot and try, But it should work |
|
@cunha we have work to do but we are confident 💪 [[language]]
name = "python"
language-servers = ["copilot"]
[language-server.copilot]
command = "copilot-language-server"
args = ["--stdio"]
[language-server.copilot.config]
editorInfo = { name = "Helix", version = "25.01" }
editorPluginInfo = { name = "helix-copilot", version = "0.1.0" } |
Screen.Recording.-.Made.with.RecordCast.webm@cunha enjoy ;) |
991cc39 to
7be883f
Compare
|
I would expect the cursor to stay where it is and the ghost text to appear after, no? |
7be883f to
de0038c
Compare
@synecdokey cursor at end shows where you'd land after accepting, it is not so bad. EDIT: done @synecdokey |
- Add InlineCompletion struct to store display text, full insert text, and replace range - Calculate offset from LSP range to show only new text as ghost text - Use helix_core::Range directly instead of std::ops::Range - Handle replace range on accept for proper text replacement - Use safe slicing with get().unwrap_or_default()
de0038c to
6ebd915
Compare
Render inline completion ghost text directly at cursor position instead of using InlineAnnotation, which was causing the cursor to shift to the end of the ghost text. Also handles multiline ghost text by rendering each line separately.
78c2a9e to
d45fae1
Compare
Screen.Recording.-.Made.with.RecordCast.webm |
|
Rocking this locally. Works smoothly. Also went through the code and don't have any suggestions. Feature request: I would prefer to disable the automatic triggers for inline completions and instead have a keybind to trigger it on demand. I looked into this. It seems that the more involved part is passing the type of completion (autotrigger vs manual) to the event handler (the current code passes in Given I have very limited understanding of the overall architecture, I'm not sure what is the best approach. I'm happy to work on it and submit a PR to https://github.com/devmanuelli/helix/tree/textDocument/inlineCompletion with some guidance. I'm not even sure these are the right things to think about, but we would need to decide on whether to reuse the existing |
@cunha of course u can make PRs ;) now I'm gonna push last commit where u can cycle among different completions from different LSPs |
- Query all LSP servers with inline completion capability, not just first - Add InlineCompletions container with push/current/next/take_and_clear - Add inline_completion_next command to cycle through completions - Simplify accept logic using Transaction::change for both range cases - Fix cursor position race by computing fresh cursor in response handler - Add stale completion detection (discard if cursor moved past range)
65237d6 to
5b7d6c1
Compare
|
@ljahier I got a simpler way to solve it than yours. Probably InlineAnnotation field is not really needed |
Screen.Recording.-.Made.with.RecordCast.webm |
I had a toggle in mind. This would allow disabing the automated triggers, but still allow for manual triggers. (The reasoning is that I find the frequent suggestions distracting, but would still like the autocomplete when there's some formulaic code to be written, or after typing out a comment describing what the next lines should do.) |
| }; | ||
|
|
||
| let offset_encoding = ls.offset_encoding(); | ||
| tokio::spawn(async move { |
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 might be misunderstanding, but it looks like this handler spawns async tasks without any cancellation or coordination. We could end up with multiple on the fly requests completing out of order, and each one will push its own completion. That means stale requests might still update the UI or accumulate completions even though newer input has already been sent
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.
No it cannot happen that "stale requests might still update the UI" (look at lines 76-79 and 91-94)
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.
You're right ! I see the version check prevents stale completions after document changes. take_and_clear() is called synchronously before triggering new request
|
This is extremely cool, and I will build this and try it out! |
Process all items returned by the LSP server instead of only the first one, allowing users to cycle through multiple suggestions with next/prev commands.
|
@ljahier fix little bug (previously, we only took the first suggestion, even though the server could have sent us several ones) |
|
I've tested it on my side and it looks good if the only LSP used is copilot. I'm getting some weird behavior when combining it with other LSPs such: "typescript-language-server" or "vue-language-server". |
@cosmincartas do they also have inlineCompletion capability? EDIT: u are right thank u very much for ur time testing my pr. Changed this pr into a draft. I will fix it. If someone finds the bug before me, pls make a pr to my repo EDIT: @cosmincartas try using this config - maybe the problem is [keys.insert]
C-a = "inline_completion_accept"
C-e = "inline_completion_dismiss"
C-n = "inline_completion_next"
C-p = "inline_completion_prev"
C-space = "inline_completion_trigger"If u find sth strange pls report it recording a video. Now I'm testing gopls and copilot together EDIT: video demo with copilot and with last hx release Screen.Recording.-.Made.with.RecordCast.webm// W/OUT COPILOT STD RELEASE Screen.Recording.-.Made.with.RecordCast.1.webm@cosmincartas as soon as u tell me it EDIT: fixed bad indentation in ghost text (tabs vs spaces...) Screen.Recording.-.Made.with.RecordCast.2.webm |
Switch inline completion ghost text from direct surface drawing to using the text annotation and decoration systems. This allows proper coexistence with diagnostics and other virtual text. Changes: - Mid-line: Use Overlay (first char) + InlineAnnotation (rest) to keep cursor in place while shifting diagnostics - EOL: Use Decoration to render ghost text without shifting cursor, return column offset so diagnostics still shift - Multi-line: Use LineAnnotation to reserve virtual lines + Decoration to render additional lines - Add OnModeSwitch hook to clear completions when leaving insert mode - Add documentation in docs/inline-completion-implementation.md
|
I'll keep this PR open but it will undergo major refactoring. Screen.Recording.-.Made.with.RecordCast.3.webm |
- Fix ghost text rendering one column to the right of cursor at EOL - Root cause: virt_off.col includes newline width (1), but cursor is ON the newline cell, not after it - Solution: subtract 1 from virt_off.col for EOL ghost text positioning - Also apply cursor style to first ghost character so it appears "on" the block cursor (matching mid-line behavior) - Update documentation with fix details
I couldn't get this working, but I used the environment variable The Fine Grained Token doesn't work, and Classic Token doesn't use the proper initials (it needs If possible, I would also like a way to make |
|
Just confirming I also get some spill-over onto nearly splits:
Haven't done much testing recently, but will likely be able to over the next few days. Some info for people coming in and wanting to disable the auto triggers, the option is: (@devmanuelli unclear if we're updating this PR, but it seems documentation for this is missing.) [editor]
inline-completion-auto-trigger = false |
|
u are all right. Im confident the code I will push in the following days will be okay ;) just stay tuned and patient ;) |
|
Hey, do this with work lsp-ai language server as well? I tried to set up the inline completions with the lsp-ai language server but it doesn't seem to work. May someone help me with this please. |
lsp-ai doesn't seems to be maintained anymore |
|
Then can you please suggest me some lsp that would work with this product apart from copilot as it's free tier has very limited tab completes. Or it's the only choice? |
helix-term/src/ui/editor.rs
Outdated
| // Render ghost text at cursor position | ||
| if let Some(completion) = doc.inline_completions.current() { | ||
| if let Some(cursor_pos) = editor.cursor_cache.get(view, doc) { |
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.
Thanks for starting on this! To echo the comment on the linked issue, rather than modifying the rendering code we should be leveraging a mix of LineAnnotation (to modify the current line) and Decoration to display the virtual lines:
Replace InlineAnnotation with multiple Overlays for mid-line inline completion display. This prevents the cursor from shifting when ghost text is shown, providing a better editing experience. - Use Overlay per character position (cursor to EOL) for mid-line case - Add overflow text rendering via Decoration for chars beyond line end - Apply suffix trimming only for display, keep ghost_text intact for accept - Simplify Document cache to single overlay vector
Clear ghost text when selection changes (e.g., arrow keys) to match expected editor behavior where completions dismiss on cursor movement.
For multi-line inline completions when cursor is mid-line: - First line shown via overlays at cursor (no cursor shift) - Additional lines shown as virtual lines below - Original rest-of-line content pushed to bottom of ghost text Single-line mid-line completions unchanged (overlay + overflow).
Don't append rest_of_line to the last ghost text line if it already appears anywhere in the ghost text (indicating it's part of the replacement content rather than content to preserve).
When displaying multi-line inline completions mid-line, the first line preview was only creating overlays for as many characters as the ghost text contained. If the ghost text first line was shorter than the rest-of-line content, trailing characters would show through. Fix by padding the preview with spaces to cover the full rest-of-line length, ensuring all original content is blanked out (since it's being pushed down to the last ghost text line). Also removes the no-longer-needed implementation docs file.
Screen-Recording.mp4gonna slim it but we are on the good way |


Implements LSP 3.18
textDocument/inlineCompletion(ghost text).Demo
Screen.Recording.-.Made.with.RecordCast.webm
Test
python3 -m venv .venv source .venv/bin/activate pip install pygls lsprotocoltest_lsp.py:
~/.config/helix/languages.toml:
~/.config/helix/config.toml:
P.S.: spread the word on reddit!!