Skip to content

Conversation

@devmanuelli
Copy link
Contributor

@devmanuelli devmanuelli commented Nov 26, 2025

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 lsprotocol

test_lsp.py:

#!/usr/bin/env python3
from lsprotocol import types as lsp
from pygls.lsp.server import LanguageServer

server = LanguageServer("test", "v0.1")

@server.feature(lsp.TEXT_DOCUMENT_INLINE_COMPLETION, lsp.InlineCompletionOptions())
def inline_completion(params: lsp.InlineCompletionParams):
    return lsp.InlineCompletionList(items=[
        lsp.InlineCompletionItem(insert_text="# Hello!")
    ])

if __name__ == "__main__":
    server.start_io()

~/.config/helix/languages.toml:

[language-server.test-lsp]
command = "/path/to/.venv/bin/python3"
args = ["/path/to/test_lsp.py"]

[[language]]
name = "python"
language-servers = ["test-lsp"]

~/.config/helix/config.toml:

[editor]
inline-completion-timeout = 150

[keys.insert]
tab = "inline_completion_accept"
C-e = "inline_completion_dismiss"

P.S.: spread the word on reddit!!

@devmanuelli devmanuelli force-pushed the textDocument/inlineCompletion branch 2 times, most recently from 3417d52 to dcde385 Compare November 26, 2025 11:52
@devmanuelli
Copy link
Contributor Author

devmanuelli commented Nov 26, 2025

I'm doing a refactoring simplifying the code
EDIT: done. I'm gonna make a demo video

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
@devmanuelli devmanuelli force-pushed the textDocument/inlineCompletion branch from dcde385 to 166dc56 Compare November 26, 2025 17:04
@taylorplewe
Copy link

Nice!!! Can't wait to clone and try it out!

@cunha
Copy link

cunha commented Nov 26, 2025

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 textDocument/didOpen, textDocument/didChange, and textDocument/didClose, but a quick search through the code indicates we may have these already.)

@devmanuelli
Copy link
Contributor Author

@cunha of course we already have them. Tomorrow I will sign up github copilot and try, But it should work

@devmanuelli
Copy link
Contributor Author

devmanuelli commented Nov 26, 2025

@cunha we have work to do but we are confident 💪
https://github.com/user-attachments/assets/0eb9e5d4-6284-4e46-9e85-c5a8c3c6a168
I've launched first this script copilot_auth.py to login via web interface.
Minimal config is

[[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" }

@devmanuelli
Copy link
Contributor Author

Screen.Recording.-.Made.with.RecordCast.webm

@cunha enjoy ;)

@devmanuelli devmanuelli force-pushed the textDocument/inlineCompletion branch from 991cc39 to 7be883f Compare November 26, 2025 21:57
@synecdokey
Copy link

I would expect the cursor to stay where it is and the ghost text to appear after, no?

@devmanuelli devmanuelli force-pushed the textDocument/inlineCompletion branch from 7be883f to de0038c Compare November 26, 2025 22:20
@devmanuelli
Copy link
Contributor Author

devmanuelli commented Nov 26, 2025

I would expect the cursor to stay where it is and the ghost text to appear after, no?

@synecdokey cursor at end shows where you'd land after accepting, it is not so bad.
Poll: 👍 if u want cursor to stay where it is; 👎 o.w.

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()
@devmanuelli devmanuelli force-pushed the textDocument/inlineCompletion branch from de0038c to 6ebd915 Compare November 26, 2025 23:24
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.
@devmanuelli devmanuelli force-pushed the textDocument/inlineCompletion branch from 78c2a9e to d45fae1 Compare November 27, 2025 00:48
@devmanuelli
Copy link
Contributor Author

Screen.Recording.-.Made.with.RecordCast.webm

@synecdokey

@cunha
Copy link

cunha commented Nov 27, 2025

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 ()) to know when to ask for an inline completion (e.g., manual trigger with autotriggers disabled). However, while working on a draft, I found that there already exists a CompletionEvent for basically the same purpose, and the CompletionHandler::handle_event function has logic that we might want when handling inline completions too (e.g., cancelling ongoing completion requests when the user deletes text).

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 CompletionEvent structure (would require differentiating normal completions from inline completions, possibly touching the existing code for normal completions) or, in a more extreme case, to make inline completions a special case for the existing CompletionHandler to minimize code duplication.

@devmanuelli
Copy link
Contributor Author

devmanuelli commented Nov 27, 2025

I would prefer to disable the automatic triggers for inline completions and instead have a keybind to trigger it on demand.

@cunha
for each request or once for all the requests until u decide to disable it?

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)
@devmanuelli devmanuelli force-pushed the textDocument/inlineCompletion branch from 65237d6 to 5b7d6c1 Compare November 27, 2025 23:21
@devmanuelli
Copy link
Contributor Author

@ljahier I got a simpler way to solve it than yours. Probably InlineAnnotation field is not really needed

@devmanuelli
Copy link
Contributor Author

Screen.Recording.-.Made.with.RecordCast.webm

@ljahier

@cunha
Copy link

cunha commented Nov 28, 2025

I would prefer to disable the automatic triggers for inline completions and instead have a keybind to trigger it on demand.

@cunha for each request or once for all the requests until u decide to disable it?

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 {
Copy link

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

Copy link
Contributor Author

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)

Copy link

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

@thomasaarholt
Copy link
Contributor

This is extremely cool, and I will build this and try it out!
It took me quite a few runs to actually catch what was going on in the screen recording. Could I ask you to zoom in a bit when recording in the future?

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.
@devmanuelli
Copy link
Contributor Author

@ljahier fix little bug (previously, we only took the first suggestion, even though the server could have sent us several ones)

@devmanuelli devmanuelli requested a review from ljahier November 28, 2025 21:19
@cosmincartas
Copy link

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".

@devmanuelli
Copy link
Contributor Author

devmanuelli commented Nov 30, 2025

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 tab

[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 tab was the problem I change pr status. Ty very much!

EDIT: fixed bad indentation in ghost text (tabs vs spaces...)

Screen.Recording.-.Made.with.RecordCast.2.webm

@devmanuelli devmanuelli marked this pull request as draft November 30, 2025 14:46
@the-mikedavis the-mikedavis linked an issue Nov 30, 2025 that may be closed by this pull request
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
@devmanuelli
Copy link
Contributor Author

I'll keep this PR open but it will undergo major refactoring.
I don't give up I will bring Github copilot into Helix!!
The goal is to fully integrate it with other Helix's features like diagnostics (ATM we overwrite them ...)

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
@NSPC911
Copy link

NSPC911 commented Dec 1, 2025

image minor issue... splits don't seem to be handled nicely

@NSPC911
Copy link

NSPC911 commented Dec 1, 2025

@cunha we have work to do but we are confident 💪 github.com/user-attachments/assets/0eb9e5d4-6284-4e46-9e85-c5a8c3c6a168 I've launched first this script copilot_auth.py to login via web interface. Minimal config is

[[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" }

I couldn't get this working, but I used the environment variable GITHUB_COPILOT_TOKEN as suggested in github/copilot-language-server-release#3, and it works. There is a minor issue with this method though.

The Fine Grained Token doesn't work, and Classic Token doesn't use the proper initials (it needs ghu_<token>, not ghp_). Using gh auth token outputs a token with gho_, again, not ghu_. The only way to get the Copilot-supported token is by using helix-gpt.


If possible, I would also like a way to make inline_completion_accept a higher priority than interacting with the completion menu. As a previous VSCode user, the tab button handled the inline completion and is a higher priority than the popup menu

@cunha
Copy link

cunha commented Dec 4, 2025

Just confirming I also get some spill-over onto nearly splits:

image

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

@devmanuelli
Copy link
Contributor Author

devmanuelli commented Dec 4, 2025

u are all right. Im confident the code I will push in the following days will be okay ;) just stay tuned and patient ;)

@MohdShahulMalik
Copy link

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.

@rohitbishnoi
Copy link

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

@MohdShahulMalik
Copy link

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?

Comment on lines 211 to 213
// 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) {
Copy link
Member

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:

#13039 (comment)

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.
@devmanuelli
Copy link
Contributor Author

devmanuelli commented Dec 9, 2025

Screen-Recording.mp4

gonna slim it but we are on the good way

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

LSP: textDocument/inlineCompletion