Repository navigation
feat: inline completion (textDocument/inlineCompletion) as ghost text - #1645
Draft
joaodinissf wants to merge 21 commits into
Draft
joaodinissf wants to merge 21 commits into
joaodinissf wants to merge 21 commits into
Conversation
Add the parts of an inline completion (ghost text) client that do not depend on how suggestions are requested and rendered: - the org.eclipse.lsp4e.inlineCompletion extension point with event listeners (the events of requested, shown, accepted and dismissed suggestions), an enablement and per content type word detectors - read-only access to the suggestion currently shown - the inline completion status with a spinner frames animation and a status menu item - word and replace range computations, the suggestion index popup and a command opening the Keys preferences filtered to inline completion The inline completion engine itself (requesting, rendering, cycling, accepting and dismissing suggestions with its commands, key bindings, preferences and status bar widget) is a placeholder, InlineCompletionController.
Requests are often registered with execute() from a language server's thread, as when it is called in a LanguageServers.collectAll() or computeAll() lambda, while cancel() is called from the UI thread or from the next request. The futures were kept in a plain ArrayList and the cancelled flag was not volatile, so cancel() could miss a future registered concurrently or fail while the list was changed, and a request registered just after cancel() was never cancelled. Guard the list with a lock, take the futures to cancel under it, make the flag volatile, and cancel a future registered after cancel() right away: a support is cancelled once. Completion therefore takes a new support for the trigger characters of each document instead of reusing the cancelled one, and keeps the support of a completion request before sending the request, so that a cancellation meanwhile reaches it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ExecuteCommandOptions.commands is required by the specification, but a server leaving it out made executing a completion item's command fail with a NullPointerException instead of running the command on the client. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Extract computeTextEdit from apply, working on a copy of the item's range instead of mutating it, and expose getEffectiveEdit and expandSnippet so that inline completion can tell what a proposal inserts. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…t text Advertise the LSP 3.18 client capability with dynamic registration. Request inline completions automatically while typing, after a configurable delay, or explicitly (Alt+\, Ctrl+\ on macOS), and show the current one as ghost text. JFace code minings draw it: its first line at the caret, further lines above the next visible line, or below the last line at the end of the document, where the editor's bottom margin makes room for them since JFace reserves none. This requires jface.text 3.28. Tab accepts (snippets enter linked mode, commands run), Esc dismisses, Alt+] and Alt+[ (Ctrl on macOS) cycle. Typing through the suggestion or deleting back within its range updates it without a new request; other edits, moving the caret or losing focus dismiss it. Automatic requests stop while the editor does not have the focus. Requests are sent one at a time, which suits servers answering in 200 ms as well as in several seconds. While the user keeps typing at the caret the request is kept, and its reply is shown if it still fits what was typed meanwhile; otherwise a new request follows. Moving the caret, Esc, losing focus and edits elsewhere cancel it. An explicit request always replaces it, and one without a reply for 10 s no longer holds back new ones. A failing server is logged once, and a reply that the content was modified, usual while typing, not at all. Code minings can only add text, so a suggestion that would overwrite different text after the caret is not shown, and a multi-line one in the middle of a line is previewed before the rest of the line. The engine goes into the internal package, where its controller replaces the placeholder. Asking it for the current suggestion still answers that there is none. The mock server answers inline completions from a configurable responder, can hold back its replies so that tests control what happens during a request, and tells which requests were cancelled. Co-Authored-By: Ruben Porras Campo <ruben.porrascampo@avaloq.com> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
JFace merges code minings at the same position into one annotation and lets header minings of the same line fight over its vertical indent, so an inlay hint at the caret or a code lens above the line the ghost text continues on would be drawn into the ghost text. LSP4E's inlay hint, code lens and color providers leave such minings out while ghost text is shown; they come back when it is dismissed. Minings of other plugins are not affected. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… popup While the completion popup is open, requests carry the selected proposal as selectedCompletionInfo and, as the specification requires, only suggestions extending it are shown, on their first line only since the popup covers the lines below. Accepting the proposal keeps what the suggestions of all servers add to it; closing the popup or selecting another proposal drops them, also while they are still being requested. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Accepting a multi-line suggestion in the middle of a line moves the rest of the line after its last line, while the preview left it on the first line. Code minings can only add text, so a text presentation listener hides the rest of the line where it is, with zero-width glyph metrics, and the last row of the ghost text draws it in the editor's own colors and font styles. The first line is then drawn on the line delimiter. JFace resets the style of the character a removed line content mining covered, which can be part of the hidden text, so it is hidden again when that happens, a few times at most. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
With word wrap on, rows ran off the right edge. Break them where the editor will break the lines once accepted: a first line too long for the visual line goes on below it, which moves the rest of the line as a further line would, and the layout is computed again when the font or the width of the editor changes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…t text JFace can only refresh all providers' minings, so showing or hiding ghost text sent inlay hint, code lens and color requests each time, for a document that did not change. When a refresh is made only for ghost text, LSP4E's providers show copies of what they computed for the current document version, unless a server asked for a refresh since. Any other refresh asks the servers as before. Copies are needed because JFace disposes the minings it replaces. Resolving is unchanged: servers with a resolve provider still get their codeLens/resolve and inlayHint/resolve requests for the copies. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Events are to report which request they belong to, and a session must remember that once its request is no longer in flight. Replace the sequence numbers of requests by an object that holds a UUID and the document's URI, kept when more suggestions are requested for the same position, and when a session continues after the proposal selected in the completion popup is applied. No change in behaviour. Co-Authored-By: Ruben Porras Campo <ruben.porrascampo@avaloq.com> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ving Ask the contributed InlineCompletionEnablement before every request, so that a product can switch inline completion off. A disabled client still advertises the capability, since servers are told once and the enablement can change at any time. Do not request inline completions for edits made while the document is being saved, such as formatting on save: the user did not type them. Co-Authored-By: Ruben Porras Campo <ruben.porrascampo@avaloq.com> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Make InlineCompletions.getCurrentCompletionInfo() answer with the request and the whole text of the suggestion shown in the active editor, instead of always answering that there is none. Co-Authored-By: Ruben Porras Campo <ruben.porrascampo@avaloq.com> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Tell the contributed listeners about requests for a new position or for more suggestions, the first time the result of a request is drawn, and when what is shown is accepted, dismissed by the user, or goes away because the editor loses the focus or closes. Nothing is reported when a suggestion just stops applying, such as after moving the caret. Since ending what is shown was also how accepting, losing the focus and other cases went about their work, only dismissing by the user reports a dismissal now. Only requests that reach a server with the capability are reported. The events are declared provisional, as they follow what one consumer needed so far and may still change. Co-Authored-By: Ruben Porras Campo <ruben.porrascampo@avaloq.com> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A quoted string was searched for anywhere after the offset, so that a string not closed made the word as long as a later string. It could also span lines, starting at an apostrophe in a comment, and only knew escaped quotes. Long strings could overflow the stack, and a surrogate pair could be split. Match at the offset only and on one line, with any backslash escape, and treat a surrogate pair as one character. Co-Authored-By: Ruben Porras Campo <ruben.porrascampo@avaloq.com> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Add commands to accept a suggestion piece by piece, bound while one is shown to Alt+; and Alt+' (Ctrl on macOS). What a word is depends on the word detector contributed for the document's content type. A piece is inserted as if typed, so that what follows keeps showing as it does when typing through a suggestion. The last piece is accepted like a whole suggestion, since a snippet is only expanded and the item's command only run then. A filter text says what typing may match, so it no longer hides the rest of a suggestion once a piece was accepted. Co-Authored-By: Ruben Porras Campo <ruben.porrascampo@avaloq.com> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Tell the listeners when the user typed exactly what was suggested, rather than accepting it. Text after the caret that the suggestion would keep, such as an auto-closed parenthesis, counts as typed. Nearly matching text is not reported: what is typed differently ends the session on the first character, so reporting it needs candidates to be followed after they stop applying. Co-Authored-By: Ruben Porras Campo <ruben.porrascampo@avaloq.com> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Set the global status from the active editor: initializing until a server for its document is known to have the capability, ready once one is, fetching while a request that reached such a server is in flight, and an error after a failed request until the next one succeeds. Cancelled and outdated requests are no failures. Several workbench windows can follow the status now, and listeners only hear of actual changes. Co-Authored-By: Ruben Porras Campo <ruben.porrascampo@avaloq.com> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Show which of the suggestions is shown, such as 2/3, in a short-lived popup above the caret when cycling through them, in addition to the status line. There is one popup at a time, so that cycling quickly does not pile them up, and it goes away with the suggestion. It is placed by the offset in the widget, which differs from the offset in the document when text is folded. Co-Authored-By: Ruben Porras Campo <ruben.porrascampo@avaloq.com> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Show the inline completion status in the status bar of the workbench window: an icon, animated while a request is pending, with the state as its tooltip. It is hidden while inline completion is disabled or the active editor has no server with the capability. Clicking it switches between requesting automatically and only when asked to, which is also a new command, bound to Alt+Shift+\ (Ctrl+Shift+\ on macOS). Its context menu shows the state and offers that switch, the preferences and the key bindings. The tooltip and the menu tell the two trigger modes apart. Co-Authored-By: Ruben Porras Campo <ruben.porrascampo@avaloq.com> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
What a suggestion replaces is the range the server sends: the protocol leaves it to the server, so there is nothing to compute from the text after the caret. Without a range, the suggestion is inserted at the caret. Remove the helper computing such a range and the unused one returning the modification stamp of an editor's document. Co-Authored-By: Ruben Porras Campo <ruben.porrascampo@avaloq.com> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
| public CompletableFuture<Either<List<InlineCompletionItem>, InlineCompletionList>> inlineCompletion( | ||
| InlineCompletionParams params) { | ||
| inlineCompletionRequests.add(params); | ||
| return inlineCompletionGate.thenCompose(released -> futureFactory(inlineCompletionResponder.apply(params))); |
| return new CompletionSnippetParser(document, snippet, insertionOffset, this::getVariableValue).parse(); | ||
| } | ||
|
|
||
| protected void apply(IDocument document, char trigger, int stateMask, int offset) { |
| return new CompletionSnippetParser(document, snippet, insertionOffset, this::getVariableValue).parse(); | ||
| } | ||
|
|
||
| protected void apply(IDocument document, char trigger, int stateMask, int offset) { |
rubenporras
reviewed
Nov 9, 2026
| * 'textDocument/completion' when completion is retriggered | ||
| */ | ||
| private CancellationSupport completionCancellationSupport; | ||
| private volatile CancellationSupport completionCancellationSupport; |
Contributor
There was a problem hiding this comment.
there was a small bug in this file, so have commit it myself in a new PR of my own.
Contributor
There was a problem hiding this comment.
therefore it can be removed from this commit
rubenporras
reviewed
Nov 9, 2026
| private final List<CompletableFuture<?>> futuresToCancel; | ||
|
|
||
| private boolean cancelled; | ||
| private volatile boolean cancelled; |
Contributor
There was a problem hiding this comment.
I agree with this changes. Could you move it to a different PR, as it is very unrelated to the inlineCompletion topic?
This file contains hidden or 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
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
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.
Note
This is a first draft and still needs work. I am opening it to get feedback on the direction and on the open questions below, not for merging as it stands. Manual testing has not been done yet.
What this does
Adds LSP 3.18 inline completions (
textDocument/inlineCompletion) to LSP4E. They appear as ghost text in the editor, drawn with JFace code minings.inlineCompletionextension point and listener API let other plugins provide enablement and word detection, and observe events.Where it comes from
The series combines two pieces of work. The first commit is Ruben Porras Campo's, unchanged: the API, the extension point, the status states and the word logic. The rest is an engine I developed independently and then adapted to those building blocks. Ruben is credited as co-author on the commits where the two come together.
How to read it
It is large: 21 commits, about 9,400 added lines including tests. Each commit builds and passes the related tests on its own, so reading in order should work.
feat: add building blocks for inline completion.CancellationSupportthread safety, the execute command guard, and computing a proposal's edit without applying it.feat: show inline completions ... as ghost text. This is the commit to read most carefully.Feedback wanted
ACCEPT_*text is the inserted remainder,DISMISStext is the whole item, andVIEWfires per fetched result and restarts elapsed time afterFETCH_ADDITIONAL.rangea server sends. Without one, the suggestion is inserted at the caret, and nothing is guessed from the text after it.jface.text3.28.Known limitations
Not in this PR
ACCEPT_EDIT; only an exact match is reported.Testing
testKeyword,testDocumentEditAndUndo(not every run) andtestDiagnosticsOnExternalFile. I still have to check whether they also fail in CI or onmainlocally.LanguageServersTestis occasionally flaky, onmaintoo.AI assistance
This work was done with AI assistance from Claude, as the commit trailers show.
🤖 Generated with Claude Code