Skip to content

Commit dcde385

Browse files
committed
feat(lsp): add textDocument/inlineCompletion support
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 (Tab) and inline_completion_dismiss (Esc) commands - Auto-trigger on document change with configurable debounce (inline-completion-timeout) - Clear completion on Escape/normal mode
1 parent 24f4574 commit dcde385

File tree

14 files changed

+202
-9
lines changed

14 files changed

+202
-9
lines changed

book/src/generated/static-cmd.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -213,6 +213,8 @@
213213
| `keep_primary_selection` | Keep primary selection | normal: `` , ``, select: `` , `` |
214214
| `remove_primary_selection` | Remove primary selection | normal: `` <A-,> ``, select: `` <A-,> `` |
215215
| `completion` | Invoke completion popup | insert: `` <C-x> `` |
216+
| `inline_completion_accept` | Accept inline completion | |
217+
| `inline_completion_dismiss` | Dismiss inline completion | |
216218
| `hover` | Show docs for item under cursor | normal: `` <space>k ``, select: `` <space>k `` |
217219
| `toggle_comments` | Comment/uncomment selections | normal: `` <C-c> ``, `` <space>c ``, select: `` <C-c> ``, `` <space>c `` |
218220
| `toggle_line_comments` | Line comment/uncomment selections | normal: `` <space><A-c> ``, select: `` <space><A-c> `` |

helix-core/src/syntax/config.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -274,6 +274,7 @@ pub enum LanguageServerFeature {
274274
RenameSymbol,
275275
InlayHints,
276276
DocumentColors,
277+
InlineCompletion,
277278
}
278279

279280
impl Display for LanguageServerFeature {
@@ -299,6 +300,7 @@ impl Display for LanguageServerFeature {
299300
RenameSymbol => "rename-symbol",
300301
InlayHints => "inlay-hints",
301302
DocumentColors => "document-colors",
303+
InlineCompletion => "inline-completion",
302304
};
303305
write!(f, "{feature}",)
304306
}

helix-lsp-types/src/inline_completion.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ pub struct InlineCompletionParams {
5555
pub context: InlineCompletionContext,
5656
}
5757

58-
/// Describes how an [`InlineCompletionItemProvider`] was triggered.
58+
/// Describes how an `InlineCompletionItemProvider` was triggered.
5959
///
6060
/// @since 3.18.0
6161
#[derive(Eq, PartialEq, Clone, Copy, Deserialize, Serialize)]
@@ -137,7 +137,7 @@ pub struct InlineCompletionItem {
137137
/// Is used both for the preview and the accept operation.
138138
pub insert_text: String,
139139
/// A text that is used to decide if this inline completion should be
140-
/// shown. When `falsy` the [`InlineCompletionItem::insertText`] is
140+
/// shown. When `falsy` the [`InlineCompletionItem::insert_text`] is
141141
/// used.
142142
///
143143
/// An inline completion is shown if the text to replace is a prefix of the

helix-lsp-types/src/lib.rs

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -153,9 +153,7 @@ pub use inlay_hint::*;
153153
mod inline_value;
154154
pub use inline_value::*;
155155

156-
#[cfg(feature = "proposed")]
157156
mod inline_completion;
158-
#[cfg(feature = "proposed")]
159157
pub use inline_completion::*;
160158

161159
mod moniker;
@@ -1596,7 +1594,6 @@ pub struct TextDocumentClientCapabilities {
15961594
///
15971595
/// @since 3.18.0
15981596
#[serde(skip_serializing_if = "Option::is_none")]
1599-
#[cfg(feature = "proposed")]
16001597
pub inline_completion: Option<InlineCompletionClientCapabilities>,
16011598
}
16021599

@@ -2060,7 +2057,6 @@ pub struct ServerCapabilities {
20602057
///
20612058
/// @since 3.18.0
20622059
#[serde(skip_serializing_if = "Option::is_none")]
2063-
#[cfg(feature = "proposed")]
20642060
pub inline_completion_provider: Option<OneOf<bool, InlineCompletionOptions>>,
20652061

20662062
/// Experimental server capabilities.

helix-lsp-types/src/request.rs

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -661,10 +661,8 @@ impl Request for PrepareRenameRequest {
661661
}
662662

663663
#[derive(Debug)]
664-
#[cfg(feature = "proposed")]
665664
pub enum InlineCompletionRequest {}
666665

667-
#[cfg(feature = "proposed")]
668666
impl Request for InlineCompletionRequest {
669667
type Params = InlineCompletionParams;
670668
type Result = Option<InlineCompletionResponse>;

helix-lsp/src/client.rs

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -389,6 +389,10 @@ impl Client {
389389
| ColorProviderCapability::Options(_)
390390
)
391391
),
392+
LanguageServerFeature::InlineCompletion => matches!(
393+
capabilities.inline_completion_provider,
394+
Some(OneOf::Left(true) | OneOf::Right(_))
395+
),
392396
}
393397
}
394398

@@ -701,6 +705,9 @@ impl Client {
701705
dynamic_registration: Some(false),
702706
resolve_support: None,
703707
}),
708+
inline_completion: Some(lsp::InlineCompletionClientCapabilities {
709+
dynamic_registration: Some(false),
710+
}),
704711
..Default::default()
705712
}),
706713
window: Some(lsp::WindowClientCapabilities {
@@ -1135,6 +1142,32 @@ impl Client {
11351142
Some(self.call::<lsp::request::InlayHintRequest>(params))
11361143
}
11371144

1145+
pub fn inline_completion(
1146+
&self,
1147+
text_document: lsp::TextDocumentIdentifier,
1148+
position: lsp::Position,
1149+
context: lsp::InlineCompletionContext,
1150+
work_done_token: Option<lsp::ProgressToken>,
1151+
) -> Option<impl Future<Output = Result<Option<lsp::InlineCompletionResponse>>>> {
1152+
let capabilities = self.capabilities.get().unwrap();
1153+
1154+
match capabilities.inline_completion_provider {
1155+
Some(lsp::OneOf::Left(true) | lsp::OneOf::Right(_)) => (),
1156+
_ => return None,
1157+
}
1158+
1159+
let params = lsp::InlineCompletionParams {
1160+
text_document_position: lsp::TextDocumentPositionParams {
1161+
text_document,
1162+
position,
1163+
},
1164+
context,
1165+
work_done_progress_params: lsp::WorkDoneProgressParams { work_done_token },
1166+
};
1167+
1168+
Some(self.call::<lsp::request::InlineCompletionRequest>(params))
1169+
}
1170+
11381171
pub fn text_document_document_color(
11391172
&self,
11401173
text_document: lsp::TextDocumentIdentifier,

helix-term/src/commands.rs

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -516,6 +516,8 @@ impl MappableCommand {
516516
keep_primary_selection, "Keep primary selection",
517517
remove_primary_selection, "Remove primary selection",
518518
completion, "Invoke completion popup",
519+
inline_completion_accept, "Accept inline completion",
520+
inline_completion_dismiss, "Dismiss inline completion",
519521
hover, "Show docs for item under cursor",
520522
toggle_comments, "Comment/uncomment selections",
521523
toggle_line_comments, "Line comment/uncomment selections",
@@ -5262,6 +5264,23 @@ pub fn completion(cx: &mut Context) {
52625264
.trigger_completions(cursor, doc.id(), view.id);
52635265
}
52645266

5267+
pub fn inline_completion_accept(cx: &mut Context) {
5268+
let (view, doc) = current!(cx.editor);
5269+
if let Some(c) = doc.inline_completion.take() {
5270+
doc.apply(
5271+
&Transaction::insert(doc.text(), doc.selection(view.id), c.text),
5272+
view.id,
5273+
);
5274+
}
5275+
}
5276+
5277+
pub fn inline_completion_dismiss(cx: &mut Context) {
5278+
let doc = doc_mut!(cx.editor);
5279+
if doc.inline_completion.take().is_none() {
5280+
normal_mode(cx);
5281+
}
5282+
}
5283+
52655284
// comments
52665285
type CommentTransactionFn = fn(
52675286
line_token: Option<&str>,

helix-term/src/handlers.rs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,25 +13,28 @@ use crate::handlers::signature_help::SignatureHelpHandler;
1313
pub use helix_view::handlers::{word_index, Handlers};
1414

1515
use self::document_colors::DocumentColorsHandler;
16+
use self::inline_completion::InlineCompletionHandler;
1617

1718
mod auto_save;
1819
pub mod completion;
1920
pub mod diagnostics;
2021
mod document_colors;
22+
mod inline_completion;
2123
mod prompt;
2224
mod signature_help;
2325
mod snippet;
2426

2527
pub fn setup(config: Arc<ArcSwap<Config>>) -> Handlers {
2628
events::register();
2729

28-
let event_tx = completion::CompletionHandler::new(config).spawn();
30+
let event_tx = completion::CompletionHandler::new(config.clone()).spawn();
2931
let signature_hints = SignatureHelpHandler::new().spawn();
3032
let auto_save = AutoSaveHandler::new().spawn();
3133
let document_colors = DocumentColorsHandler::default().spawn();
3234
let word_index = word_index::Handler::spawn();
3335
let pull_diagnostics = PullDiagnosticsHandler::default().spawn();
3436
let pull_all_documents_diagnostics = PullAllDocumentsDiagnosticHandler::default().spawn();
37+
let inline_completions = InlineCompletionHandler::new(config.clone()).spawn();
3538

3639
let handlers = Handlers {
3740
completions: helix_view::handlers::completion::CompletionHandler::new(event_tx),
@@ -41,6 +44,7 @@ pub fn setup(config: Arc<ArcSwap<Config>>) -> Handlers {
4144
word_index,
4245
pull_diagnostics,
4346
pull_all_documents_diagnostics,
47+
inline_completions,
4448
};
4549

4650
helix_view::handlers::register_hooks(&handlers);
@@ -50,6 +54,7 @@ pub fn setup(config: Arc<ArcSwap<Config>>) -> Handlers {
5054
diagnostics::register_hooks(&handlers);
5155
snippet::register_hooks(&handlers);
5256
document_colors::register_hooks(&handlers);
57+
inline_completion::register_hooks(&handlers);
5358
prompt::register_hooks(&handlers);
5459
handlers
5560
}
Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
use std::sync::Arc;
2+
3+
use arc_swap::ArcSwap;
4+
use helix_core::{syntax::config::LanguageServerFeature, text_annotations::InlineAnnotation};
5+
use helix_event::{register_hook, send_blocking};
6+
use helix_lsp::lsp;
7+
use helix_view::{
8+
events::DocumentDidChange,
9+
handlers::{lsp::InlineCompletionEvent, Handlers},
10+
DocumentId,
11+
};
12+
13+
use crate::config::Config;
14+
use tokio::time::Instant;
15+
16+
use crate::job;
17+
18+
pub(super) struct InlineCompletionHandler {
19+
pending: Option<(DocumentId, usize)>,
20+
config: Arc<ArcSwap<Config>>,
21+
}
22+
23+
impl InlineCompletionHandler {
24+
pub fn new(config: Arc<ArcSwap<Config>>) -> Self {
25+
Self {
26+
pending: None,
27+
config,
28+
}
29+
}
30+
}
31+
32+
impl helix_event::AsyncHook for InlineCompletionHandler {
33+
type Event = InlineCompletionEvent;
34+
35+
fn handle_event(&mut self, event: Self::Event, _: Option<Instant>) -> Option<Instant> {
36+
self.pending = Some((event.doc, event.cursor));
37+
Some(Instant::now() + self.config.load().editor.inline_completion_timeout)
38+
}
39+
40+
fn finish_debounce(&mut self) {
41+
let Some((doc_id, cursor)) = self.pending.take() else {
42+
return;
43+
};
44+
45+
job::dispatch_blocking(move |editor, _| {
46+
let Some(doc) = editor.document(doc_id) else {
47+
return;
48+
};
49+
50+
let Some(ls) = doc
51+
.language_servers_with_feature(LanguageServerFeature::InlineCompletion)
52+
.next()
53+
else {
54+
return;
55+
};
56+
57+
let offset_encoding = ls.offset_encoding();
58+
let pos = helix_lsp::util::pos_to_lsp_pos(doc.text(), cursor, offset_encoding);
59+
let context = lsp::InlineCompletionContext {
60+
trigger_kind: lsp::InlineCompletionTriggerKind::Automatic,
61+
selected_completion_info: None,
62+
};
63+
64+
let Some(fut) = ls.inline_completion(doc.identifier(), pos, context, None) else {
65+
return;
66+
};
67+
68+
tokio::spawn(async move {
69+
let Ok(Some(resp)) = fut.await else { return };
70+
let items = match resp {
71+
lsp::InlineCompletionResponse::Array(v) => v,
72+
lsp::InlineCompletionResponse::List(l) => l.items,
73+
};
74+
let Some(item) = items.into_iter().next() else {
75+
return;
76+
};
77+
78+
job::dispatch(move |editor, _| {
79+
let Some(doc) = editor.documents.get_mut(&doc_id) else {
80+
return;
81+
};
82+
doc.inline_completion = Some(InlineAnnotation::new(cursor, item.insert_text));
83+
})
84+
.await;
85+
});
86+
});
87+
}
88+
}
89+
90+
pub(super) fn register_hooks(handlers: &Handlers) {
91+
let tx = handlers.inline_completions.clone();
92+
93+
register_hook!(move |event: &mut DocumentDidChange<'_>| {
94+
event.doc.inline_completion = None;
95+
if event.ghost_transaction {
96+
return Ok(());
97+
}
98+
99+
let cursor = event
100+
.doc
101+
.selection(event.view)
102+
.primary()
103+
.cursor(event.doc.text().slice(..));
104+
send_blocking(
105+
&tx,
106+
InlineCompletionEvent {
107+
doc: event.doc.id(),
108+
cursor,
109+
},
110+
);
111+
Ok(())
112+
});
113+
}

helix-view/src/document.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,9 @@ pub struct Document {
154154
/// update from the LSP
155155
pub inlay_hints_oudated: bool,
156156

157+
/// Current inline completion (ghost text) for the document.
158+
pub inline_completion: Option<InlineAnnotation>,
159+
157160
path: Option<PathBuf>,
158161
relative_path: OnceCell<Option<PathBuf>>,
159162
encoding: &'static encoding::Encoding,
@@ -705,6 +708,7 @@ impl Document {
705708
selections: HashMap::default(),
706709
inlay_hints: HashMap::default(),
707710
inlay_hints_oudated: false,
711+
inline_completion: None,
708712
view_data: Default::default(),
709713
indent_style: DEFAULT_INDENT,
710714
editor_config: EditorConfig::default(),

0 commit comments

Comments
 (0)