Prevent sending slash commands in CC threads (#36453)
Highlight them as errors in the editor, and add a leading space when sending them so users don't hit the odd behavior when sending these commands to the SDK. Release Notes: - N/A
This commit is contained in:
parent
7bcea7dc2c
commit
d30b017d1f
8 changed files with 263 additions and 16 deletions
|
@ -1,4 +1,4 @@
|
||||||
use std::{path::Path, rc::Rc, sync::Arc};
|
use std::{any::Any, path::Path, rc::Rc, sync::Arc};
|
||||||
|
|
||||||
use agent_servers::AgentServer;
|
use agent_servers::AgentServer;
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
|
@ -66,4 +66,8 @@ impl AgentServer for NativeAgentServer {
|
||||||
Ok(Rc::new(connection) as Rc<dyn acp_thread::AgentConnection>)
|
Ok(Rc::new(connection) as Rc<dyn acp_thread::AgentConnection>)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn into_any(self: Rc<Self>) -> Rc<dyn Any> {
|
||||||
|
self
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,6 +18,7 @@ use project::Project;
|
||||||
use schemars::JsonSchema;
|
use schemars::JsonSchema;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use std::{
|
use std::{
|
||||||
|
any::Any,
|
||||||
path::{Path, PathBuf},
|
path::{Path, PathBuf},
|
||||||
rc::Rc,
|
rc::Rc,
|
||||||
sync::Arc,
|
sync::Arc,
|
||||||
|
@ -40,6 +41,14 @@ pub trait AgentServer: Send {
|
||||||
project: &Entity<Project>,
|
project: &Entity<Project>,
|
||||||
cx: &mut App,
|
cx: &mut App,
|
||||||
) -> Task<Result<Rc<dyn AgentConnection>>>;
|
) -> Task<Result<Rc<dyn AgentConnection>>>;
|
||||||
|
|
||||||
|
fn into_any(self: Rc<Self>) -> Rc<dyn Any>;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl dyn AgentServer {
|
||||||
|
pub fn downcast<T: 'static + AgentServer + Sized>(self: Rc<Self>) -> Option<Rc<T>> {
|
||||||
|
self.into_any().downcast().ok()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl std::fmt::Debug for AgentServerCommand {
|
impl std::fmt::Debug for AgentServerCommand {
|
||||||
|
|
|
@ -65,6 +65,10 @@ impl AgentServer for ClaudeCode {
|
||||||
|
|
||||||
Task::ready(Ok(Rc::new(connection) as _))
|
Task::ready(Ok(Rc::new(connection) as _))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn into_any(self: Rc<Self>) -> Rc<dyn Any> {
|
||||||
|
self
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct ClaudeAgentConnection {
|
struct ClaudeAgentConnection {
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
use std::path::Path;
|
|
||||||
use std::rc::Rc;
|
use std::rc::Rc;
|
||||||
|
use std::{any::Any, path::Path};
|
||||||
|
|
||||||
use crate::{AgentServer, AgentServerCommand};
|
use crate::{AgentServer, AgentServerCommand};
|
||||||
use acp_thread::{AgentConnection, LoadError};
|
use acp_thread::{AgentConnection, LoadError};
|
||||||
|
@ -86,6 +86,10 @@ impl AgentServer for Gemini {
|
||||||
result
|
result
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn into_any(self: Rc<Self>) -> Rc<dyn Any> {
|
||||||
|
self
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
|
|
|
@ -24,6 +24,7 @@ pub struct EntryViewState {
|
||||||
thread_store: Entity<ThreadStore>,
|
thread_store: Entity<ThreadStore>,
|
||||||
text_thread_store: Entity<TextThreadStore>,
|
text_thread_store: Entity<TextThreadStore>,
|
||||||
entries: Vec<Entry>,
|
entries: Vec<Entry>,
|
||||||
|
prevent_slash_commands: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl EntryViewState {
|
impl EntryViewState {
|
||||||
|
@ -32,6 +33,7 @@ impl EntryViewState {
|
||||||
project: Entity<Project>,
|
project: Entity<Project>,
|
||||||
thread_store: Entity<ThreadStore>,
|
thread_store: Entity<ThreadStore>,
|
||||||
text_thread_store: Entity<TextThreadStore>,
|
text_thread_store: Entity<TextThreadStore>,
|
||||||
|
prevent_slash_commands: bool,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
Self {
|
Self {
|
||||||
workspace,
|
workspace,
|
||||||
|
@ -39,6 +41,7 @@ impl EntryViewState {
|
||||||
thread_store,
|
thread_store,
|
||||||
text_thread_store,
|
text_thread_store,
|
||||||
entries: Vec::new(),
|
entries: Vec::new(),
|
||||||
|
prevent_slash_commands,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -77,6 +80,7 @@ impl EntryViewState {
|
||||||
self.thread_store.clone(),
|
self.thread_store.clone(),
|
||||||
self.text_thread_store.clone(),
|
self.text_thread_store.clone(),
|
||||||
"Edit message - @ to include context",
|
"Edit message - @ to include context",
|
||||||
|
self.prevent_slash_commands,
|
||||||
editor::EditorMode::AutoHeight {
|
editor::EditorMode::AutoHeight {
|
||||||
min_lines: 1,
|
min_lines: 1,
|
||||||
max_lines: None,
|
max_lines: None,
|
||||||
|
@ -382,6 +386,7 @@ mod tests {
|
||||||
project.clone(),
|
project.clone(),
|
||||||
thread_store,
|
thread_store,
|
||||||
text_thread_store,
|
text_thread_store,
|
||||||
|
false,
|
||||||
)
|
)
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -10,7 +10,8 @@ use assistant_slash_commands::codeblock_fence_for_path;
|
||||||
use collections::{HashMap, HashSet};
|
use collections::{HashMap, HashSet};
|
||||||
use editor::{
|
use editor::{
|
||||||
Anchor, AnchorRangeExt, ContextMenuOptions, ContextMenuPlacement, Editor, EditorElement,
|
Anchor, AnchorRangeExt, ContextMenuOptions, ContextMenuPlacement, Editor, EditorElement,
|
||||||
EditorMode, EditorStyle, ExcerptId, FoldPlaceholder, MultiBuffer, ToOffset,
|
EditorEvent, EditorMode, EditorStyle, ExcerptId, FoldPlaceholder, MultiBuffer,
|
||||||
|
SemanticsProvider, ToOffset,
|
||||||
actions::Paste,
|
actions::Paste,
|
||||||
display_map::{Crease, CreaseId, FoldId},
|
display_map::{Crease, CreaseId, FoldId},
|
||||||
};
|
};
|
||||||
|
@ -19,8 +20,9 @@ use futures::{
|
||||||
future::{Shared, join_all, try_join_all},
|
future::{Shared, join_all, try_join_all},
|
||||||
};
|
};
|
||||||
use gpui::{
|
use gpui::{
|
||||||
AppContext, ClipboardEntry, Context, Entity, EventEmitter, FocusHandle, Focusable, Image,
|
AppContext, ClipboardEntry, Context, Entity, EventEmitter, FocusHandle, Focusable,
|
||||||
ImageFormat, Img, Task, TextStyle, WeakEntity,
|
HighlightStyle, Image, ImageFormat, Img, Subscription, Task, TextStyle, UnderlineStyle,
|
||||||
|
WeakEntity,
|
||||||
};
|
};
|
||||||
use language::{Buffer, Language};
|
use language::{Buffer, Language};
|
||||||
use language_model::LanguageModelImage;
|
use language_model::LanguageModelImage;
|
||||||
|
@ -28,26 +30,30 @@ use project::{CompletionIntent, Project, ProjectPath, Worktree};
|
||||||
use rope::Point;
|
use rope::Point;
|
||||||
use settings::Settings;
|
use settings::Settings;
|
||||||
use std::{
|
use std::{
|
||||||
|
cell::Cell,
|
||||||
ffi::OsStr,
|
ffi::OsStr,
|
||||||
fmt::{Display, Write},
|
fmt::{Display, Write},
|
||||||
ops::Range,
|
ops::Range,
|
||||||
path::{Path, PathBuf},
|
path::{Path, PathBuf},
|
||||||
rc::Rc,
|
rc::Rc,
|
||||||
sync::Arc,
|
sync::Arc,
|
||||||
|
time::Duration,
|
||||||
};
|
};
|
||||||
use text::OffsetRangeExt;
|
use text::{OffsetRangeExt, ToOffset as _};
|
||||||
use theme::ThemeSettings;
|
use theme::ThemeSettings;
|
||||||
use ui::{
|
use ui::{
|
||||||
ActiveTheme, AnyElement, App, ButtonCommon, ButtonLike, ButtonStyle, Color, Icon, IconName,
|
ActiveTheme, AnyElement, App, ButtonCommon, ButtonLike, ButtonStyle, Color, Icon, IconName,
|
||||||
IconSize, InteractiveElement, IntoElement, Label, LabelCommon, LabelSize, ParentElement,
|
IconSize, InteractiveElement, IntoElement, Label, LabelCommon, LabelSize, ParentElement,
|
||||||
Render, SelectableButton, SharedString, Styled, TextSize, TintColor, Toggleable, Window, div,
|
Render, SelectableButton, SharedString, Styled, TextSize, TintColor, Toggleable, Window, div,
|
||||||
h_flex,
|
h_flex, px,
|
||||||
};
|
};
|
||||||
use url::Url;
|
use url::Url;
|
||||||
use util::ResultExt;
|
use util::ResultExt;
|
||||||
use workspace::{Workspace, notifications::NotifyResultExt as _};
|
use workspace::{Workspace, notifications::NotifyResultExt as _};
|
||||||
use zed_actions::agent::Chat;
|
use zed_actions::agent::Chat;
|
||||||
|
|
||||||
|
const PARSE_SLASH_COMMAND_DEBOUNCE: Duration = Duration::from_millis(50);
|
||||||
|
|
||||||
pub struct MessageEditor {
|
pub struct MessageEditor {
|
||||||
mention_set: MentionSet,
|
mention_set: MentionSet,
|
||||||
editor: Entity<Editor>,
|
editor: Entity<Editor>,
|
||||||
|
@ -55,6 +61,9 @@ pub struct MessageEditor {
|
||||||
workspace: WeakEntity<Workspace>,
|
workspace: WeakEntity<Workspace>,
|
||||||
thread_store: Entity<ThreadStore>,
|
thread_store: Entity<ThreadStore>,
|
||||||
text_thread_store: Entity<TextThreadStore>,
|
text_thread_store: Entity<TextThreadStore>,
|
||||||
|
prevent_slash_commands: bool,
|
||||||
|
_subscriptions: Vec<Subscription>,
|
||||||
|
_parse_slash_command_task: Task<()>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Copy)]
|
#[derive(Clone, Copy)]
|
||||||
|
@ -73,6 +82,7 @@ impl MessageEditor {
|
||||||
thread_store: Entity<ThreadStore>,
|
thread_store: Entity<ThreadStore>,
|
||||||
text_thread_store: Entity<TextThreadStore>,
|
text_thread_store: Entity<TextThreadStore>,
|
||||||
placeholder: impl Into<Arc<str>>,
|
placeholder: impl Into<Arc<str>>,
|
||||||
|
prevent_slash_commands: bool,
|
||||||
mode: EditorMode,
|
mode: EditorMode,
|
||||||
window: &mut Window,
|
window: &mut Window,
|
||||||
cx: &mut Context<Self>,
|
cx: &mut Context<Self>,
|
||||||
|
@ -90,6 +100,9 @@ impl MessageEditor {
|
||||||
text_thread_store.downgrade(),
|
text_thread_store.downgrade(),
|
||||||
cx.weak_entity(),
|
cx.weak_entity(),
|
||||||
);
|
);
|
||||||
|
let semantics_provider = Rc::new(SlashCommandSemanticsProvider {
|
||||||
|
range: Cell::new(None),
|
||||||
|
});
|
||||||
let mention_set = MentionSet::default();
|
let mention_set = MentionSet::default();
|
||||||
let editor = cx.new(|cx| {
|
let editor = cx.new(|cx| {
|
||||||
let buffer = cx.new(|cx| Buffer::local("", cx).with_language(Arc::new(language), cx));
|
let buffer = cx.new(|cx| Buffer::local("", cx).with_language(Arc::new(language), cx));
|
||||||
|
@ -106,6 +119,9 @@ impl MessageEditor {
|
||||||
max_entries_visible: 12,
|
max_entries_visible: 12,
|
||||||
placement: Some(ContextMenuPlacement::Above),
|
placement: Some(ContextMenuPlacement::Above),
|
||||||
});
|
});
|
||||||
|
if prevent_slash_commands {
|
||||||
|
editor.set_semantics_provider(Some(semantics_provider.clone()));
|
||||||
|
}
|
||||||
editor
|
editor
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -114,6 +130,24 @@ impl MessageEditor {
|
||||||
})
|
})
|
||||||
.detach();
|
.detach();
|
||||||
|
|
||||||
|
let mut subscriptions = Vec::new();
|
||||||
|
if prevent_slash_commands {
|
||||||
|
subscriptions.push(cx.subscribe_in(&editor, window, {
|
||||||
|
let semantics_provider = semantics_provider.clone();
|
||||||
|
move |this, editor, event, window, cx| match event {
|
||||||
|
EditorEvent::Edited { .. } => {
|
||||||
|
this.highlight_slash_command(
|
||||||
|
semantics_provider.clone(),
|
||||||
|
editor.clone(),
|
||||||
|
window,
|
||||||
|
cx,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
editor,
|
editor,
|
||||||
project,
|
project,
|
||||||
|
@ -121,6 +155,9 @@ impl MessageEditor {
|
||||||
thread_store,
|
thread_store,
|
||||||
text_thread_store,
|
text_thread_store,
|
||||||
workspace,
|
workspace,
|
||||||
|
prevent_slash_commands,
|
||||||
|
_subscriptions: subscriptions,
|
||||||
|
_parse_slash_command_task: Task::ready(()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -590,6 +627,7 @@ impl MessageEditor {
|
||||||
self.mention_set
|
self.mention_set
|
||||||
.contents(self.project.clone(), self.thread_store.clone(), window, cx);
|
.contents(self.project.clone(), self.thread_store.clone(), window, cx);
|
||||||
let editor = self.editor.clone();
|
let editor = self.editor.clone();
|
||||||
|
let prevent_slash_commands = self.prevent_slash_commands;
|
||||||
|
|
||||||
cx.spawn(async move |_, cx| {
|
cx.spawn(async move |_, cx| {
|
||||||
let contents = contents.await?;
|
let contents = contents.await?;
|
||||||
|
@ -612,7 +650,15 @@ impl MessageEditor {
|
||||||
|
|
||||||
let crease_range = crease.range().to_offset(&snapshot.buffer_snapshot);
|
let crease_range = crease.range().to_offset(&snapshot.buffer_snapshot);
|
||||||
if crease_range.start > ix {
|
if crease_range.start > ix {
|
||||||
chunks.push(text[ix..crease_range.start].into());
|
let chunk = if prevent_slash_commands
|
||||||
|
&& ix == 0
|
||||||
|
&& parse_slash_command(&text[ix..]).is_some()
|
||||||
|
{
|
||||||
|
format!(" {}", &text[ix..crease_range.start]).into()
|
||||||
|
} else {
|
||||||
|
text[ix..crease_range.start].into()
|
||||||
|
};
|
||||||
|
chunks.push(chunk);
|
||||||
}
|
}
|
||||||
let chunk = match mention {
|
let chunk = match mention {
|
||||||
Mention::Text { uri, content } => {
|
Mention::Text { uri, content } => {
|
||||||
|
@ -644,7 +690,14 @@ impl MessageEditor {
|
||||||
}
|
}
|
||||||
|
|
||||||
if ix < text.len() {
|
if ix < text.len() {
|
||||||
let last_chunk = text[ix..].trim_end();
|
let last_chunk = if prevent_slash_commands
|
||||||
|
&& ix == 0
|
||||||
|
&& parse_slash_command(&text[ix..]).is_some()
|
||||||
|
{
|
||||||
|
format!(" {}", text[ix..].trim_end())
|
||||||
|
} else {
|
||||||
|
text[ix..].trim_end().to_owned()
|
||||||
|
};
|
||||||
if !last_chunk.is_empty() {
|
if !last_chunk.is_empty() {
|
||||||
chunks.push(last_chunk.into());
|
chunks.push(last_chunk.into());
|
||||||
}
|
}
|
||||||
|
@ -990,6 +1043,48 @@ impl MessageEditor {
|
||||||
cx.notify();
|
cx.notify();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn highlight_slash_command(
|
||||||
|
&mut self,
|
||||||
|
semantics_provider: Rc<SlashCommandSemanticsProvider>,
|
||||||
|
editor: Entity<Editor>,
|
||||||
|
window: &mut Window,
|
||||||
|
cx: &mut Context<Self>,
|
||||||
|
) {
|
||||||
|
struct InvalidSlashCommand;
|
||||||
|
|
||||||
|
self._parse_slash_command_task = cx.spawn_in(window, async move |_, cx| {
|
||||||
|
cx.background_executor()
|
||||||
|
.timer(PARSE_SLASH_COMMAND_DEBOUNCE)
|
||||||
|
.await;
|
||||||
|
editor
|
||||||
|
.update_in(cx, |editor, window, cx| {
|
||||||
|
let snapshot = editor.snapshot(window, cx);
|
||||||
|
let range = parse_slash_command(&editor.text(cx));
|
||||||
|
semantics_provider.range.set(range);
|
||||||
|
if let Some((start, end)) = range {
|
||||||
|
editor.highlight_text::<InvalidSlashCommand>(
|
||||||
|
vec![
|
||||||
|
snapshot.buffer_snapshot.anchor_after(start)
|
||||||
|
..snapshot.buffer_snapshot.anchor_before(end),
|
||||||
|
],
|
||||||
|
HighlightStyle {
|
||||||
|
underline: Some(UnderlineStyle {
|
||||||
|
thickness: px(1.),
|
||||||
|
color: Some(gpui::red()),
|
||||||
|
wavy: true,
|
||||||
|
}),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
cx,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
editor.clear_highlights::<InvalidSlashCommand>(cx);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.ok();
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
pub fn set_text(&mut self, text: &str, window: &mut Window, cx: &mut Context<Self>) {
|
pub fn set_text(&mut self, text: &str, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
self.editor.update(cx, |editor, cx| {
|
self.editor.update(cx, |editor, cx| {
|
||||||
|
@ -1416,6 +1511,118 @@ impl MentionSet {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
struct SlashCommandSemanticsProvider {
|
||||||
|
range: Cell<Option<(usize, usize)>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SemanticsProvider for SlashCommandSemanticsProvider {
|
||||||
|
fn hover(
|
||||||
|
&self,
|
||||||
|
buffer: &Entity<Buffer>,
|
||||||
|
position: text::Anchor,
|
||||||
|
cx: &mut App,
|
||||||
|
) -> Option<Task<Vec<project::Hover>>> {
|
||||||
|
let snapshot = buffer.read(cx).snapshot();
|
||||||
|
let offset = position.to_offset(&snapshot);
|
||||||
|
let (start, end) = self.range.get()?;
|
||||||
|
if !(start..end).contains(&offset) {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let range = snapshot.anchor_after(start)..snapshot.anchor_after(end);
|
||||||
|
return Some(Task::ready(vec![project::Hover {
|
||||||
|
contents: vec![project::HoverBlock {
|
||||||
|
text: "Slash commands are not supported".into(),
|
||||||
|
kind: project::HoverBlockKind::PlainText,
|
||||||
|
}],
|
||||||
|
range: Some(range),
|
||||||
|
language: None,
|
||||||
|
}]));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn inline_values(
|
||||||
|
&self,
|
||||||
|
_buffer_handle: Entity<Buffer>,
|
||||||
|
_range: Range<text::Anchor>,
|
||||||
|
_cx: &mut App,
|
||||||
|
) -> Option<Task<anyhow::Result<Vec<project::InlayHint>>>> {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
fn inlay_hints(
|
||||||
|
&self,
|
||||||
|
_buffer_handle: Entity<Buffer>,
|
||||||
|
_range: Range<text::Anchor>,
|
||||||
|
_cx: &mut App,
|
||||||
|
) -> Option<Task<anyhow::Result<Vec<project::InlayHint>>>> {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
fn resolve_inlay_hint(
|
||||||
|
&self,
|
||||||
|
_hint: project::InlayHint,
|
||||||
|
_buffer_handle: Entity<Buffer>,
|
||||||
|
_server_id: lsp::LanguageServerId,
|
||||||
|
_cx: &mut App,
|
||||||
|
) -> Option<Task<anyhow::Result<project::InlayHint>>> {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
fn supports_inlay_hints(&self, _buffer: &Entity<Buffer>, _cx: &mut App) -> bool {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
fn document_highlights(
|
||||||
|
&self,
|
||||||
|
_buffer: &Entity<Buffer>,
|
||||||
|
_position: text::Anchor,
|
||||||
|
_cx: &mut App,
|
||||||
|
) -> Option<Task<Result<Vec<project::DocumentHighlight>>>> {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
fn definitions(
|
||||||
|
&self,
|
||||||
|
_buffer: &Entity<Buffer>,
|
||||||
|
_position: text::Anchor,
|
||||||
|
_kind: editor::GotoDefinitionKind,
|
||||||
|
_cx: &mut App,
|
||||||
|
) -> Option<Task<Result<Vec<project::LocationLink>>>> {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
fn range_for_rename(
|
||||||
|
&self,
|
||||||
|
_buffer: &Entity<Buffer>,
|
||||||
|
_position: text::Anchor,
|
||||||
|
_cx: &mut App,
|
||||||
|
) -> Option<Task<Result<Option<Range<text::Anchor>>>>> {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
fn perform_rename(
|
||||||
|
&self,
|
||||||
|
_buffer: &Entity<Buffer>,
|
||||||
|
_position: text::Anchor,
|
||||||
|
_new_name: String,
|
||||||
|
_cx: &mut App,
|
||||||
|
) -> Option<Task<Result<project::ProjectTransaction>>> {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_slash_command(text: &str) -> Option<(usize, usize)> {
|
||||||
|
if let Some(remainder) = text.strip_prefix('/') {
|
||||||
|
let pos = remainder
|
||||||
|
.find(char::is_whitespace)
|
||||||
|
.unwrap_or(remainder.len());
|
||||||
|
let command = &remainder[..pos];
|
||||||
|
if !command.is_empty() && command.chars().all(char::is_alphanumeric) {
|
||||||
|
return Some((0, 1 + command.len()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use std::{ops::Range, path::Path, sync::Arc};
|
use std::{ops::Range, path::Path, sync::Arc};
|
||||||
|
@ -1463,6 +1670,7 @@ mod tests {
|
||||||
thread_store.clone(),
|
thread_store.clone(),
|
||||||
text_thread_store.clone(),
|
text_thread_store.clone(),
|
||||||
"Test",
|
"Test",
|
||||||
|
false,
|
||||||
EditorMode::AutoHeight {
|
EditorMode::AutoHeight {
|
||||||
min_lines: 1,
|
min_lines: 1,
|
||||||
max_lines: None,
|
max_lines: None,
|
||||||
|
@ -1661,6 +1869,7 @@ mod tests {
|
||||||
thread_store.clone(),
|
thread_store.clone(),
|
||||||
text_thread_store.clone(),
|
text_thread_store.clone(),
|
||||||
"Test",
|
"Test",
|
||||||
|
false,
|
||||||
EditorMode::AutoHeight {
|
EditorMode::AutoHeight {
|
||||||
max_lines: None,
|
max_lines: None,
|
||||||
min_lines: 1,
|
min_lines: 1,
|
||||||
|
|
|
@ -7,7 +7,7 @@ use acp_thread::{AgentConnection, Plan};
|
||||||
use action_log::ActionLog;
|
use action_log::ActionLog;
|
||||||
use agent::{TextThreadStore, ThreadStore};
|
use agent::{TextThreadStore, ThreadStore};
|
||||||
use agent_client_protocol::{self as acp};
|
use agent_client_protocol::{self as acp};
|
||||||
use agent_servers::AgentServer;
|
use agent_servers::{AgentServer, ClaudeCode};
|
||||||
use agent_settings::{AgentProfileId, AgentSettings, CompletionMode, NotifyWhenAgentWaiting};
|
use agent_settings::{AgentProfileId, AgentSettings, CompletionMode, NotifyWhenAgentWaiting};
|
||||||
use anyhow::bail;
|
use anyhow::bail;
|
||||||
use audio::{Audio, Sound};
|
use audio::{Audio, Sound};
|
||||||
|
@ -160,6 +160,7 @@ impl AcpThreadView {
|
||||||
window: &mut Window,
|
window: &mut Window,
|
||||||
cx: &mut Context<Self>,
|
cx: &mut Context<Self>,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
|
let prevent_slash_commands = agent.clone().downcast::<ClaudeCode>().is_some();
|
||||||
let message_editor = cx.new(|cx| {
|
let message_editor = cx.new(|cx| {
|
||||||
MessageEditor::new(
|
MessageEditor::new(
|
||||||
workspace.clone(),
|
workspace.clone(),
|
||||||
|
@ -167,6 +168,7 @@ impl AcpThreadView {
|
||||||
thread_store.clone(),
|
thread_store.clone(),
|
||||||
text_thread_store.clone(),
|
text_thread_store.clone(),
|
||||||
"Message the agent - @ to include context",
|
"Message the agent - @ to include context",
|
||||||
|
prevent_slash_commands,
|
||||||
editor::EditorMode::AutoHeight {
|
editor::EditorMode::AutoHeight {
|
||||||
min_lines: MIN_EDITOR_LINES,
|
min_lines: MIN_EDITOR_LINES,
|
||||||
max_lines: Some(MAX_EDITOR_LINES),
|
max_lines: Some(MAX_EDITOR_LINES),
|
||||||
|
@ -184,6 +186,7 @@ impl AcpThreadView {
|
||||||
project.clone(),
|
project.clone(),
|
||||||
thread_store.clone(),
|
thread_store.clone(),
|
||||||
text_thread_store.clone(),
|
text_thread_store.clone(),
|
||||||
|
prevent_slash_commands,
|
||||||
)
|
)
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -3925,6 +3928,10 @@ pub(crate) mod tests {
|
||||||
) -> Task<gpui::Result<Rc<dyn AgentConnection>>> {
|
) -> Task<gpui::Result<Rc<dyn AgentConnection>>> {
|
||||||
Task::ready(Ok(Rc::new(self.connection.clone())))
|
Task::ready(Ok(Rc::new(self.connection.clone())))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn into_any(self: Rc<Self>) -> Rc<dyn Any> {
|
||||||
|
self
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
|
|
|
@ -167,7 +167,8 @@ pub fn hover_at_inlay(
|
||||||
|
|
||||||
let language_registry = project.read_with(cx, |p, _| p.languages().clone())?;
|
let language_registry = project.read_with(cx, |p, _| p.languages().clone())?;
|
||||||
let blocks = vec![inlay_hover.tooltip];
|
let blocks = vec![inlay_hover.tooltip];
|
||||||
let parsed_content = parse_blocks(&blocks, &language_registry, None, cx).await;
|
let parsed_content =
|
||||||
|
parse_blocks(&blocks, Some(&language_registry), None, cx).await;
|
||||||
|
|
||||||
let scroll_handle = ScrollHandle::new();
|
let scroll_handle = ScrollHandle::new();
|
||||||
|
|
||||||
|
@ -251,7 +252,9 @@ fn show_hover(
|
||||||
|
|
||||||
let (excerpt_id, _, _) = editor.buffer().read(cx).excerpt_containing(anchor, cx)?;
|
let (excerpt_id, _, _) = editor.buffer().read(cx).excerpt_containing(anchor, cx)?;
|
||||||
|
|
||||||
let language_registry = editor.project()?.read(cx).languages().clone();
|
let language_registry = editor
|
||||||
|
.project()
|
||||||
|
.map(|project| project.read(cx).languages().clone());
|
||||||
let provider = editor.semantics_provider.clone()?;
|
let provider = editor.semantics_provider.clone()?;
|
||||||
|
|
||||||
if !ignore_timeout {
|
if !ignore_timeout {
|
||||||
|
@ -443,7 +446,8 @@ fn show_hover(
|
||||||
text: format!("Unicode character U+{:02X}", invisible as u32),
|
text: format!("Unicode character U+{:02X}", invisible as u32),
|
||||||
kind: HoverBlockKind::PlainText,
|
kind: HoverBlockKind::PlainText,
|
||||||
}];
|
}];
|
||||||
let parsed_content = parse_blocks(&blocks, &language_registry, None, cx).await;
|
let parsed_content =
|
||||||
|
parse_blocks(&blocks, language_registry.as_ref(), None, cx).await;
|
||||||
let scroll_handle = ScrollHandle::new();
|
let scroll_handle = ScrollHandle::new();
|
||||||
let subscription = this
|
let subscription = this
|
||||||
.update(cx, |_, cx| {
|
.update(cx, |_, cx| {
|
||||||
|
@ -493,7 +497,8 @@ fn show_hover(
|
||||||
|
|
||||||
let blocks = hover_result.contents;
|
let blocks = hover_result.contents;
|
||||||
let language = hover_result.language;
|
let language = hover_result.language;
|
||||||
let parsed_content = parse_blocks(&blocks, &language_registry, language, cx).await;
|
let parsed_content =
|
||||||
|
parse_blocks(&blocks, language_registry.as_ref(), language, cx).await;
|
||||||
let scroll_handle = ScrollHandle::new();
|
let scroll_handle = ScrollHandle::new();
|
||||||
hover_highlights.push(range.clone());
|
hover_highlights.push(range.clone());
|
||||||
let subscription = this
|
let subscription = this
|
||||||
|
@ -583,7 +588,7 @@ fn same_diagnostic_hover(editor: &Editor, snapshot: &EditorSnapshot, anchor: Anc
|
||||||
|
|
||||||
async fn parse_blocks(
|
async fn parse_blocks(
|
||||||
blocks: &[HoverBlock],
|
blocks: &[HoverBlock],
|
||||||
language_registry: &Arc<LanguageRegistry>,
|
language_registry: Option<&Arc<LanguageRegistry>>,
|
||||||
language: Option<Arc<Language>>,
|
language: Option<Arc<Language>>,
|
||||||
cx: &mut AsyncWindowContext,
|
cx: &mut AsyncWindowContext,
|
||||||
) -> Option<Entity<Markdown>> {
|
) -> Option<Entity<Markdown>> {
|
||||||
|
@ -603,7 +608,7 @@ async fn parse_blocks(
|
||||||
.new_window_entity(|_window, cx| {
|
.new_window_entity(|_window, cx| {
|
||||||
Markdown::new(
|
Markdown::new(
|
||||||
combined_text.into(),
|
combined_text.into(),
|
||||||
Some(language_registry.clone()),
|
language_registry.cloned(),
|
||||||
language.map(|language| language.name()),
|
language.map(|language| language.name()),
|
||||||
cx,
|
cx,
|
||||||
)
|
)
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue