Skip to content

Commit 32529dc

Browse files
sola-ryuclaudehuacnlee
authored
editor: Add on-demand completion commands (#3411)
## Description Editor completions previously opened only as a side effect of typing. Applications could not request suggestions from a command, menu, or their own shortcut without editing text. Add the Editor-only `ShowCompletions` action and `show_completions` API. There is no default shortcut: hosts choose bindings that fit their platform and avoid input-method shortcuts such as Ctrl+Space. Hosts can scope bindings to `Input && mode == editor`; existing `Input` bindings remain valid. ```rust window.dispatch_action(Box::new(ShowCompletions), cx); editor.update(cx, |state, cx| state.show_completions(window, cx)); ``` The action addresses the focused Editor; the method focuses the specified Editor. Suggestions refine against the identifier prefix before the caret, and an empty prefix still requests suggestions. Prefix scanning walks backward through the rope instead of copying the whole preceding document. Manual requests use LSP `INVOKED` with no trigger character. Disabled/readonly editors, missing providers, and IME preedit issue no request. An already-open completion menu keeps its selected item. A new request cancels the previous overlay request and inline suggestion, closes code actions, and uses the existing completion lifecycle. Responses are checked against the current document revision, caret, focus, editability, and composition state before being applied. English and Chinese Editor documentation describe the command, API, host-owned shortcuts, and lifecycle rules. ## Public API ### gpui-base - `input::ShowCompletions` — Editor action requesting suggestions without typing; no default keybinding. - `InputBaseState<EditorMode>::show_completions(&mut self, window: &mut Window, cx: &mut Context<Self>)` — Request manual completions and focus the Editor, respecting editability, IME, and an existing completion menu. ### gpui-component - `input::ShowCompletions` — Re-export the Base action for applications using `gpui_kit::component::input`. ## Breaking Changes None for existing consumers. The public API is additive and existing `Input` bindings remain supported. ## Validation - `cargo test -p gpui-kit --features 'test-support component' --test input` — 179 passed. Covers explicit and empty-prefix requests, host-configured Editor-only shortcuts, no Ctrl+Space default, menu selection preservation, IME preedit/commit, readonly/disabled/missing-provider guards, request replacement, Escape/caret cancellation, inline suggestion/debounce cancellation, code-action replacement, and API focus from an application command. - `cargo clippy -p gpui-base -p gpui-kit --features 'gpui-kit/test-support gpui-kit/component' --all-targets -- --deny warnings` — passed. - `cargo fmt --all --check` and `git diff --check` — passed. The implementation, tests, and documentation revision were generated with AI assistance. IME coverage uses GPUI's simulated input-handler protocol; it does not drive native candidate windows or verify OS shortcut interception. --------- Co-authored-by: Sola <sola-ryu@users.noreply.github.com> Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Co-authored-by: Jason Lee <huacnlee@gmail.com>
1 parent 2ef9968 commit 32529dc

7 files changed

Lines changed: 418 additions & 27 deletions

File tree

‎crates/base/src/input/base/state.rs‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,7 @@ actions!(
116116
MoveToNextWord,
117117
Escape,
118118
ToggleCodeActions,
119+
ShowCompletions,
119120
Search,
120121
Replace,
121122
GoToDefinition,

‎crates/base/src/input/editor/lsp/completions.rs‎

Lines changed: 96 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ use lsp_types::{
99
use ropey::Rope;
1010
use std::{cell::RefCell, ops::Range, rc::Rc, time::Duration};
1111

12-
use crate::input::InputBaseState;
12+
use crate::input::{InputBaseState, ShowCompletions};
1313

1414
/// Default debounce duration for inline completions.
1515
const DEFAULT_INLINE_COMPLETION_DEBOUNCE: Duration = Duration::from_millis(300);
@@ -183,6 +183,93 @@ impl InputBaseState<EditorMode> {
183183
)
184184
.map(|s| s.trim().to_string())
185185
.unwrap_or_default();
186+
self.request_completions(
187+
provider,
188+
start_offset,
189+
query.clone(),
190+
CompletionContext {
191+
trigger_kind: lsp_types::CompletionTriggerKind::TRIGGER_CHARACTER,
192+
trigger_character: Some(query),
193+
},
194+
window,
195+
cx,
196+
);
197+
}
198+
199+
pub(crate) fn on_action_show_completions(
200+
&mut self,
201+
_: &ShowCompletions,
202+
window: &mut Window,
203+
cx: &mut Context<Self>,
204+
) {
205+
self.show_completions(window, cx);
206+
}
207+
208+
/// Request completions at the caret and focus the editor, without editing text.
209+
///
210+
/// The current identifier prefix is used to refine the menu. An empty
211+
/// prefix still asks the provider for suggestions. There is no default
212+
/// keybinding; applications can dispatch [`ShowCompletions`] in the
213+
/// `Input && mode == editor` context or call this method from a command.
214+
///
215+
/// Disabled/readonly editors, IME preedit, and an already-open completion
216+
/// menu are left alone. A new request cancels the previous overlay request
217+
/// and inline suggestion, and closes the code-action menu.
218+
pub fn show_completions(&mut self, window: &mut Window, cx: &mut Context<Self>) {
219+
if self.disabled
220+
|| self.readonly
221+
|| self.completion_inserting
222+
|| self.ime_marked_range.is_some()
223+
|| self.extras.context_menu_content.completion.open
224+
{
225+
return;
226+
}
227+
let Some(provider) = self.extras.lsp.completion_provider.clone() else {
228+
return;
229+
};
230+
231+
let offset = self.cursor();
232+
let word_len: usize = self
233+
.text
234+
.chars_at(offset)
235+
.reversed()
236+
.take_while(|ch| ch.is_alphanumeric() || *ch == '_')
237+
.map(char::len_utf8)
238+
.sum();
239+
let start_offset = offset - word_len;
240+
let query = self.text.slice(start_offset..offset).to_string();
241+
242+
self.hide_context_menu(cx);
243+
self.clear_inline_completion(cx);
244+
self.focus(window, cx);
245+
246+
self.request_completions(
247+
provider,
248+
start_offset,
249+
query,
250+
CompletionContext {
251+
trigger_kind: lsp_types::CompletionTriggerKind::INVOKED,
252+
trigger_character: None,
253+
},
254+
window,
255+
cx,
256+
);
257+
}
258+
259+
/// Record the word a menu is for and ask `provider` about the caret.
260+
fn request_completions(
261+
&mut self,
262+
provider: Rc<dyn CompletionProvider>,
263+
start_offset: usize,
264+
query: String,
265+
completion_context: CompletionContext,
266+
window: &mut Window,
267+
cx: &mut Context<Self>,
268+
) {
269+
let new_offset = self.cursor();
270+
let revision = self.document_revision;
271+
self.extras.context_menu_task = Task::ready(Ok(()));
272+
self.extras.context_menu_content.code_action.open = false;
186273
self.extras
187274
.context_menu_content
188275
.completion
@@ -193,11 +280,6 @@ impl InputBaseState<EditorMode> {
193280
.query
194281
.clone_from(&query);
195282

196-
let completion_context = CompletionContext {
197-
trigger_kind: lsp_types::CompletionTriggerKind::TRIGGER_CHARACTER,
198-
trigger_character: Some(query),
199-
};
200-
201283
let provider_responses =
202284
provider.completions(&self.text, new_offset, completion_context, window, cx);
203285
self.extras.context_menu_task = cx.spawn_in(window, async move |editor, cx| {
@@ -209,31 +291,21 @@ impl InputBaseState<EditorMode> {
209291
}
210292
}
211293

212-
if completions.is_empty() {
213-
editor.update(cx, |editor, cx| {
214-
editor.extras.context_menu_content.completion.open = false;
215-
editor.extras.context_menu_content.completion.items.clear();
216-
editor.extras.context_menu_content.completion.bump();
217-
cx.notify();
218-
})?;
219-
return Ok(());
220-
}
221-
222294
editor
223295
.update_in(cx, |editor, window, cx| {
224-
if !editor.focus_handle.is_focused(window) {
296+
if !editor.focus_handle.is_focused(window)
297+
|| editor.document_revision != revision
298+
|| editor.cursor() != new_offset
299+
|| editor.ime_marked_range.is_some()
300+
|| editor.disabled
301+
|| editor.readonly
302+
{
225303
return;
226304
}
227305

306+
editor.extras.context_menu_content.completion.open = !completions.is_empty();
228307
editor.extras.context_menu_content.completion.items = completions;
229-
editor.extras.context_menu_content.completion.open = !editor
230-
.extras
231-
.context_menu_content
232-
.completion
233-
.items
234-
.is_empty();
235308
editor.extras.context_menu_content.completion.bump();
236-
237309
cx.notify();
238310
})
239311
.ok();

‎crates/base/src/input/editor/mod.rs‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -186,7 +186,9 @@ impl InputModeKind for EditorMode {
186186
window: &mut Window,
187187
) -> Stateful<Div> {
188188
element
189+
.key_context("Input mode=editor")
189190
.on_action(window.listener_for(entity, InputBaseState::on_action_toggle_code_actions))
191+
.on_action(window.listener_for(entity, InputBaseState::on_action_show_completions))
190192
.on_action(window.listener_for(entity, InputBaseState::on_action_go_to_definition))
191193
}
192194
}

‎crates/component/src/input/mod.rs‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,8 +34,8 @@ pub use gpui_base::input::{
3434
MoveUp, Outdent, OutdentInline, Paste, Point, RangeDecoration, RangeDecorationCollection,
3535
RangeDecorationStyle, Redo, Replace, Rope, RopeExt, RopeLines, Search, SelectAll, SelectToEnd,
3636
SelectToEndOfLine, SelectToNextWordEnd, SelectToPreviousWordStart, SelectToStart,
37-
SelectToStartOfLine, Selection, ShowCharacterPalette, ShowDocumentHandler, TabSize,
38-
TextDecoration, TextDecorationCollection, TextareaState, ToggleCodeActions, Undo,
37+
SelectToStartOfLine, Selection, ShowCharacterPalette, ShowCompletions, ShowDocumentHandler,
38+
TabSize, TextDecoration, TextDecorationCollection, TextareaState, ToggleCodeActions, Undo,
3939
WrappingIndent,
4040
};
4141
pub use gpui_base::input::{EditorMode, InputMode, InputModeKind, TextareaMode};

0 commit comments

Comments
 (0)