@@ -1,5 +1,5 @@
use crate::{
- acp::completion_provider::ContextPickerCompletionProvider,
+ acp::completion_provider::{ContextPickerCompletionProvider, SlashCommandCompletion},
context_picker::{ContextPickerAction, fetch_context_picker::fetch_url_content},
};
use acp_thread::{MentionUri, selection_name};
@@ -11,10 +11,10 @@ use assistant_slash_commands::codeblock_fence_for_path;
use collections::{HashMap, HashSet};
use editor::{
Addon, Anchor, AnchorRangeExt, ContextMenuOptions, ContextMenuPlacement, Editor, EditorElement,
- EditorEvent, EditorMode, EditorSnapshot, EditorStyle, ExcerptId, FoldPlaceholder, MultiBuffer,
- ToOffset,
+ EditorEvent, EditorMode, EditorSnapshot, EditorStyle, ExcerptId, FoldPlaceholder, InlayId,
+ MultiBuffer, ToOffset,
actions::Paste,
- display_map::{Crease, CreaseId, FoldId},
+ display_map::{Crease, CreaseId, FoldId, Inlay},
};
use futures::{
FutureExt as _,
@@ -25,10 +25,12 @@ use gpui::{
EventEmitter, FocusHandle, Focusable, Image, ImageFormat, Img, KeyContext, Subscription, Task,
TextStyle, WeakEntity, pulsating_between,
};
-use language::{Buffer, Language};
+use language::{Buffer, Language, language_settings::InlayHintKind};
use language_model::LanguageModelImage;
use postage::stream::Stream as _;
-use project::{CompletionIntent, Project, ProjectItem, ProjectPath, Worktree};
+use project::{
+ CompletionIntent, InlayHint, InlayHintLabel, Project, ProjectItem, ProjectPath, Worktree,
+};
use prompt_store::{PromptId, PromptStore};
use rope::Point;
use settings::Settings;
@@ -62,6 +64,7 @@ pub struct MessageEditor {
history_store: Entity<HistoryStore>,
prompt_store: Option<Entity<PromptStore>>,
prompt_capabilities: Rc<Cell<acp::PromptCapabilities>>,
+ available_commands: Rc<RefCell<Vec<acp::AvailableCommand>>>,
_subscriptions: Vec<Subscription>,
_parse_slash_command_task: Task<()>,
}
@@ -76,6 +79,8 @@ pub enum MessageEditorEvent {
impl EventEmitter<MessageEditorEvent> for MessageEditor {}
+const COMMAND_HINT_INLAY_ID: usize = 0;
+
impl MessageEditor {
pub fn new(
workspace: WeakEntity<Workspace>,
@@ -102,7 +107,7 @@ impl MessageEditor {
history_store.clone(),
prompt_store.clone(),
prompt_capabilities.clone(),
- available_commands,
+ available_commands.clone(),
));
let mention_set = MentionSet::default();
let editor = cx.new(|cx| {
@@ -133,12 +138,33 @@ impl MessageEditor {
})
.detach();
+ let mut has_hint = false;
let mut subscriptions = Vec::new();
+
subscriptions.push(cx.subscribe_in(&editor, window, {
move |this, editor, event, window, cx| {
if let EditorEvent::Edited { .. } = event {
- let snapshot = editor.update(cx, |editor, cx| editor.snapshot(window, cx));
+ let snapshot = editor.update(cx, |editor, cx| {
+ let new_hints = this
+ .command_hint(editor.buffer(), cx)
+ .into_iter()
+ .collect::<Vec<_>>();
+ let has_new_hint = !new_hints.is_empty();
+ editor.splice_inlays(
+ if has_hint {
+ &[InlayId::Hint(COMMAND_HINT_INLAY_ID)]
+ } else {
+ &[]
+ },
+ new_hints,
+ cx,
+ );
+ has_hint = has_new_hint;
+
+ editor.snapshot(window, cx)
+ });
this.mention_set.remove_invalid(snapshot);
+
cx.notify();
}
}
@@ -152,11 +178,55 @@ impl MessageEditor {
history_store,
prompt_store,
prompt_capabilities,
+ available_commands,
_subscriptions: subscriptions,
_parse_slash_command_task: Task::ready(()),
}
}
+ fn command_hint(&self, buffer: &Entity<MultiBuffer>, cx: &App) -> Option<Inlay> {
+ let available_commands = self.available_commands.borrow();
+ if available_commands.is_empty() {
+ return None;
+ }
+
+ let snapshot = buffer.read(cx).snapshot(cx);
+ let parsed_command = SlashCommandCompletion::try_parse(&snapshot.text(), 0)?;
+ if parsed_command.argument.is_some() {
+ return None;
+ }
+
+ let command_name = parsed_command.command?;
+ let available_command = available_commands
+ .iter()
+ .find(|command| command.name == command_name)?;
+
+ let acp::AvailableCommandInput::Unstructured { mut hint } =
+ available_command.input.clone()?;
+
+ let mut hint_pos = parsed_command.source_range.end + 1;
+ if hint_pos > snapshot.len() {
+ hint_pos = snapshot.len();
+ hint.insert(0, ' ');
+ }
+
+ let hint_pos = snapshot.anchor_after(hint_pos);
+
+ Some(Inlay::hint(
+ COMMAND_HINT_INLAY_ID,
+ hint_pos,
+ &InlayHint {
+ position: hint_pos.text_anchor,
+ label: InlayHintLabel::String(hint),
+ kind: Some(InlayHintKind::Parameter),
+ padding_left: false,
+ padding_right: false,
+ tooltip: None,
+ resolve_state: project::ResolveState::Resolved,
+ },
+ ))
+ }
+
pub fn insert_thread_summary(
&mut self,
thread: agent2::DbThreadMetadata,
@@ -1184,6 +1254,7 @@ impl Render for MessageEditor {
local_player: cx.theme().players().local(),
text: text_style,
syntax: cx.theme().syntax().clone(),
+ inlay_hints_style: editor::make_inlay_hints_style(cx),
..Default::default()
},
)
@@ -1639,7 +1710,7 @@ mod tests {
name: "say-hello".to_string(),
description: "Say hello to whoever you want".to_string(),
input: Some(acp::AvailableCommandInput::Unstructured {
- hint: "Who do you want to say hello to?".to_string(),
+ hint: "<name>".to_string(),
}),
},
]));
@@ -1714,7 +1785,7 @@ mod tests {
cx.run_until_parked();
editor.update_in(&mut cx, |editor, window, cx| {
- assert_eq!(editor.text(cx), "/quick-math ");
+ assert_eq!(editor.display_text(cx), "/quick-math ");
assert!(!editor.has_visible_completions_menu());
editor.set_text("", window, cx);
});
@@ -1722,7 +1793,7 @@ mod tests {
cx.simulate_input("/say");
editor.update_in(&mut cx, |editor, _window, cx| {
- assert_eq!(editor.text(cx), "/say");
+ assert_eq!(editor.display_text(cx), "/say");
assert!(editor.has_visible_completions_menu());
assert_eq!(
@@ -1740,6 +1811,7 @@ mod tests {
editor.update_in(&mut cx, |editor, _window, cx| {
assert_eq!(editor.text(cx), "/say-hello ");
+ assert_eq!(editor.display_text(cx), "/say-hello <name>");
assert!(editor.has_visible_completions_menu());
assert_eq!(
@@ -1757,8 +1829,35 @@ mod tests {
cx.run_until_parked();
- editor.update_in(&mut cx, |editor, _window, cx| {
+ editor.update_in(&mut cx, |editor, window, cx| {
assert_eq!(editor.text(cx), "/say-hello GPT5");
+ assert_eq!(editor.display_text(cx), "/say-hello GPT5");
+ assert!(!editor.has_visible_completions_menu());
+
+ // Delete argument
+ for _ in 0..4 {
+ editor.backspace(&editor::actions::Backspace, window, cx);
+ }
+ });
+
+ cx.run_until_parked();
+
+ editor.update_in(&mut cx, |editor, window, cx| {
+ assert_eq!(editor.text(cx), "/say-hello ");
+ // Hint is visible because argument was deleted
+ assert_eq!(editor.display_text(cx), "/say-hello <name>");
+
+ // Delete last command letter
+ editor.backspace(&editor::actions::Backspace, window, cx);
+ editor.backspace(&editor::actions::Backspace, window, cx);
+ });
+
+ cx.run_until_parked();
+
+ editor.update_in(&mut cx, |editor, _window, cx| {
+ // Hint goes away once command no longer matches an available one
+ assert_eq!(editor.text(cx), "/say-hell");
+ assert_eq!(editor.display_text(cx), "/say-hell");
assert!(!editor.has_visible_completions_menu());
});
}