agent: LSP tools (#55744)
Adds 4 (technically 5) new tools to the zed agent, corresponding to LSP actions: - `find_references` - `goto_definition` - `rename_symbol` - `get_code_actions` and `apply_code_actions` Notes: - `rename_symbol` skips doing a `prepare_rename`. If there is nothing to rename at the position, it will forward the error to the agent - The code action tools are stateful. The state is stored in the `get_code_actions` tool itself as a `PendingCodeActions`. It is not passed into/out of subagents. Calling `apply_code_actions` without calling `get_code_actions` first is an error, but I've never seen an agent do this Symbols are identified by: - file name - line number - symbol If there is no substring match on that line for the symbol text, it is an error. If there are multiple, it chooses the first. This may not be great if you have a line like: `fn convert(x: foo::Something) -> bar::Something` - the second `Something` is a different symbol, but is inacessible to these tools. Probably fine for now, but we can look into improving Release Notes: - Added: New tools for the Zed Agent for interacting with language servers --------- Co-authored-by: Lukas Wirth <me@lukaswirth.dev>
This commit is contained in:
1 parent
914e7ecb65
commit
1764bfc645
12 files changed
+912
-9
No files matched your search
@@ -1108,13 +1108,18 @@
|
||||
"create_directory": true,
|
||||
"delete_path": true,
|
||||
"diagnostics": true,
|
||||
"apply_code_action": true,
|
||||
"edit_file": true,
|
||||
"fetch": true,
|
||||
"find_path": true,
|
||||
"find_references": true,
|
||||
"get_code_actions": true,
|
||||
"go_to_definition": true,
|
||||
"list_directory": true,
|
||||
"project_notifications": false,
|
||||
"move_path": true,
|
||||
"now": true,
|
||||
"find_path": true,
|
||||
"rename_symbol": true,
|
||||
"read_file": true,
|
||||
"restore_file_from_disk": true,
|
||||
"save_file": true,
|
||||
@@ -1138,6 +1143,9 @@
|
||||
"project_notifications": false,
|
||||
"now": true,
|
||||
"find_path": true,
|
||||
"find_references": true,
|
||||
"get_code_actions": true,
|
||||
"go_to_definition": true,
|
||||
"read_file": true,
|
||||
"open": true,
|
||||
"grep": true,
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
use crate::{
|
||||
ContextServerRegistry, CopyPathTool, CreateDirectoryTool, DbLanguageModel, DbThread,
|
||||
DeletePathTool, DiagnosticsTool, EditFileTool, FetchTool, FindPathTool, GrepTool,
|
||||
ListDirectoryTool, MovePathTool, NowTool, OpenTool, ProjectSnapshot, ReadFileTool,
|
||||
ApplyCodeActionTool, CodeActionStore, ContextServerRegistry, CopyPathTool, CreateDirectoryTool,
|
||||
DbLanguageModel, DbThread, DeletePathTool, DiagnosticsTool, EditFileTool, FetchTool,
|
||||
FindPathTool, FindReferencesTool, GetCodeActionsTool, GoToDefinitionTool, GrepTool,
|
||||
ListDirectoryTool, MovePathTool, NowTool, OpenTool, ProjectSnapshot, ReadFileTool, RenameTool,
|
||||
RestoreFileFromDiskTool, SaveFileTool, SpawnAgentTool, SystemPromptTemplate, Template,
|
||||
Templates, TerminalTool, ToolPermissionDecision, UpdatePlanTool, WebSearchTool,
|
||||
decide_permission_from_settings,
|
||||
};
|
||||
use acp_thread::{MentionUri, UserMessageId};
|
||||
use action_log::ActionLog;
|
||||
use feature_flags::{FeatureFlagAppExt as _, UpdatePlanToolFeatureFlag};
|
||||
use feature_flags::{FeatureFlagAppExt as _, LspToolFeatureFlag, UpdatePlanToolFeatureFlag};
|
||||
|
||||
use agent_client_protocol::schema as acp;
|
||||
use agent_settings::{
|
||||
@@ -1542,7 +1543,6 @@ impl Thread {
|
||||
self.project.clone(),
|
||||
self.action_log.clone(),
|
||||
));
|
||||
self.add_tool(DiagnosticsTool::new(self.project.clone()));
|
||||
self.add_tool(EditFileTool::new(
|
||||
self.project.clone(),
|
||||
cx.weak_entity(),
|
||||
@@ -1569,6 +1569,22 @@ impl Thread {
|
||||
self.add_tool(TerminalTool::new(self.project.clone(), environment.clone()));
|
||||
self.add_tool(WebSearchTool);
|
||||
|
||||
self.add_tool(DiagnosticsTool::new(self.project.clone()));
|
||||
if cx.has_flag::<LspToolFeatureFlag>() {
|
||||
let code_action_store: CodeActionStore = cx.new(|_cx| None);
|
||||
self.add_tool(FindReferencesTool::new(self.project.clone()));
|
||||
self.add_tool(GetCodeActionsTool::new(
|
||||
self.project.clone(),
|
||||
code_action_store.clone(),
|
||||
));
|
||||
self.add_tool(ApplyCodeActionTool::new(
|
||||
self.project.clone(),
|
||||
code_action_store,
|
||||
));
|
||||
self.add_tool(GoToDefinitionTool::new(self.project.clone()));
|
||||
self.add_tool(RenameTool::new(self.project.clone()));
|
||||
}
|
||||
|
||||
if self.depth() < MAX_SUBAGENT_DEPTH {
|
||||
self.add_tool(SpawnAgentTool::new(environment));
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
mod apply_code_action_tool;
|
||||
mod context_server_registry;
|
||||
mod copy_path_tool;
|
||||
mod create_directory_tool;
|
||||
@@ -8,15 +9,20 @@ mod edit_file_tool;
|
||||
mod evals;
|
||||
mod fetch_tool;
|
||||
mod find_path_tool;
|
||||
mod find_references_tool;
|
||||
mod get_code_actions_tool;
|
||||
mod go_to_definition_tool;
|
||||
mod grep_tool;
|
||||
mod list_directory_tool;
|
||||
mod move_path_tool;
|
||||
mod now_tool;
|
||||
mod open_tool;
|
||||
mod read_file_tool;
|
||||
mod rename_tool;
|
||||
mod restore_file_from_disk_tool;
|
||||
mod save_file_tool;
|
||||
mod spawn_agent_tool;
|
||||
mod symbol_locator;
|
||||
mod terminal_tool;
|
||||
mod tool_permissions;
|
||||
mod update_plan_tool;
|
||||
@@ -25,6 +31,7 @@ mod web_search_tool;
|
||||
use crate::AgentTool;
|
||||
use language_model::{LanguageModelRequestTool, LanguageModelToolSchemaFormat};
|
||||
|
||||
pub use apply_code_action_tool::*;
|
||||
pub use context_server_registry::*;
|
||||
pub use copy_path_tool::*;
|
||||
pub use create_directory_tool::*;
|
||||
@@ -33,15 +40,20 @@ pub use diagnostics_tool::*;
|
||||
pub use edit_file_tool::*;
|
||||
pub use fetch_tool::*;
|
||||
pub use find_path_tool::*;
|
||||
pub use find_references_tool::*;
|
||||
pub use get_code_actions_tool::*;
|
||||
pub use go_to_definition_tool::*;
|
||||
pub use grep_tool::*;
|
||||
pub use list_directory_tool::*;
|
||||
pub use move_path_tool::*;
|
||||
pub use now_tool::*;
|
||||
pub use open_tool::*;
|
||||
pub use read_file_tool::*;
|
||||
pub use rename_tool::*;
|
||||
pub use restore_file_from_disk_tool::*;
|
||||
pub use save_file_tool::*;
|
||||
pub use spawn_agent_tool::*;
|
||||
pub use symbol_locator::*;
|
||||
pub use terminal_tool::*;
|
||||
pub use tool_permissions::*;
|
||||
pub use update_plan_tool::*;
|
||||
@@ -116,6 +128,7 @@ macro_rules! tools {
|
||||
}
|
||||
|
||||
tools! {
|
||||
ApplyCodeActionTool,
|
||||
CopyPathTool,
|
||||
CreateDirectoryTool,
|
||||
DeletePathTool,
|
||||
@@ -123,12 +136,16 @@ tools! {
|
||||
EditFileTool,
|
||||
FetchTool,
|
||||
FindPathTool,
|
||||
FindReferencesTool,
|
||||
GetCodeActionsTool,
|
||||
GoToDefinitionTool,
|
||||
GrepTool,
|
||||
ListDirectoryTool,
|
||||
MovePathTool,
|
||||
NowTool,
|
||||
OpenTool,
|
||||
ReadFileTool,
|
||||
RenameTool,
|
||||
RestoreFileFromDiskTool,
|
||||
SaveFileTool,
|
||||
SpawnAgentTool,
|
||||
|
||||
@@ -0,0 +1,145 @@
|
||||
use std::fmt::Write;
|
||||
use std::sync::Arc;
|
||||
|
||||
use agent_client_protocol::schema as acp;
|
||||
use gpui::{App, Entity, SharedString, Task};
|
||||
use project::Project;
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use super::symbol_locator::CodeActionStore;
|
||||
use crate::{AgentTool, ToolCallEventStream, ToolInput};
|
||||
|
||||
/// Applies a code action previously retrieved by get_code_actions.
|
||||
///
|
||||
/// You must call get_code_actions first to get the list of available actions,
|
||||
/// then use the number from that list to choose which action to apply.
|
||||
///
|
||||
/// After applying a code action, the list is cleared. If you want to apply
|
||||
/// another action, call get_code_actions again.
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct ApplyCodeActionToolInput {
|
||||
/// The 1-based index of the code action to apply, from the list
|
||||
/// returned by get_code_actions.
|
||||
pub index: u32,
|
||||
}
|
||||
|
||||
pub struct ApplyCodeActionTool {
|
||||
project: Entity<Project>,
|
||||
code_action_store: CodeActionStore,
|
||||
}
|
||||
|
||||
impl ApplyCodeActionTool {
|
||||
pub fn new(project: Entity<Project>, code_action_store: CodeActionStore) -> Self {
|
||||
Self {
|
||||
project,
|
||||
code_action_store,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl AgentTool for ApplyCodeActionTool {
|
||||
type Input = ApplyCodeActionToolInput;
|
||||
type Output = String;
|
||||
|
||||
const NAME: &'static str = "apply_code_action";
|
||||
|
||||
fn kind() -> acp::ToolKind {
|
||||
acp::ToolKind::Other
|
||||
}
|
||||
|
||||
fn initial_title(
|
||||
&self,
|
||||
input: Result<Self::Input, serde_json::Value>,
|
||||
cx: &mut App,
|
||||
) -> SharedString {
|
||||
if let Ok(input) = input {
|
||||
let title = self
|
||||
.code_action_store
|
||||
.read(cx)
|
||||
.as_ref()
|
||||
.and_then(|pending| {
|
||||
let index = input.index.checked_sub(1)? as usize;
|
||||
Some(pending.actions.get(index)?.lsp_action.title().to_string())
|
||||
});
|
||||
if let Some(title) = title {
|
||||
format!("Apply code action: {title}").into()
|
||||
} else {
|
||||
format!("Apply code action #{}", input.index).into()
|
||||
}
|
||||
} else {
|
||||
"Apply code action".into()
|
||||
}
|
||||
}
|
||||
|
||||
fn run(
|
||||
self: Arc<Self>,
|
||||
input: ToolInput<Self::Input>,
|
||||
_event_stream: ToolCallEventStream,
|
||||
cx: &mut App,
|
||||
) -> Task<Result<String, String>> {
|
||||
let project = self.project.clone();
|
||||
let store = self.code_action_store.clone();
|
||||
cx.spawn(async move |cx| {
|
||||
let input = input
|
||||
.recv()
|
||||
.await
|
||||
.map_err(|e| format!("Failed to receive tool input: {e}"))?;
|
||||
|
||||
let pending = store.update(cx, |store, _cx| store.take()).ok_or_else(|| {
|
||||
"No code actions available. Call get_code_actions first.".to_string()
|
||||
})?;
|
||||
|
||||
let zero_based_index = input
|
||||
.index
|
||||
.checked_sub(1)
|
||||
.ok_or_else(|| "Index must be 1 or greater.".to_string())?;
|
||||
|
||||
let action = pending
|
||||
.actions
|
||||
.get(zero_based_index as usize)
|
||||
.cloned()
|
||||
.ok_or_else(|| {
|
||||
format!(
|
||||
"Index {} is out of range. There were {} code action(s) available.",
|
||||
input.index,
|
||||
pending.actions.len()
|
||||
)
|
||||
})?;
|
||||
|
||||
let title = action.lsp_action.title().to_string();
|
||||
let buffer = pending.buffer.clone();
|
||||
|
||||
let apply_task = project.update(cx, |project, cx| {
|
||||
project.apply_code_action(buffer, action, true, cx)
|
||||
});
|
||||
|
||||
let transaction = apply_task
|
||||
.await
|
||||
.map_err(|e| format!("Failed to apply code action '{title}': {e}"))?;
|
||||
|
||||
if transaction.0.is_empty() {
|
||||
return Ok(format!(
|
||||
"Code action '{title}' was applied but made no changes.",
|
||||
));
|
||||
}
|
||||
|
||||
let mut output = format!(
|
||||
"Applied code action '{title}'. Modified {} file(s):\n",
|
||||
transaction.0.len()
|
||||
);
|
||||
|
||||
for (buffer, _) in &transaction.0 {
|
||||
buffer.read_with(cx, |buffer, cx| {
|
||||
let path = buffer
|
||||
.file()
|
||||
.map(|f| f.full_path(cx).display().to_string())
|
||||
.unwrap_or_else(|| "<untitled>".to_string());
|
||||
writeln!(output, "- {path}").ok();
|
||||
});
|
||||
}
|
||||
|
||||
Ok(output)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
use std::fmt::Write;
|
||||
use std::sync::Arc;
|
||||
|
||||
use super::symbol_locator::{LocationDisplay, SymbolLocator};
|
||||
use crate::{AgentTool, ToolCallEventStream, ToolInput};
|
||||
use agent_client_protocol::schema as acp;
|
||||
use gpui::{App, Entity, SharedString, Task};
|
||||
use project::Project;
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// Finds all references to a symbol across the project using the language server.
|
||||
///
|
||||
/// Returns a list of locations where the symbol is referenced, including file paths,
|
||||
/// line numbers, and code snippets for each reference.
|
||||
///
|
||||
/// Before using this tool, use read_file or grep to find the exact symbol
|
||||
/// name and line number.
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct FindReferencesToolInput {
|
||||
/// The symbol to find references of.
|
||||
pub symbol: SymbolLocator,
|
||||
}
|
||||
|
||||
pub struct FindReferencesTool {
|
||||
project: Entity<Project>,
|
||||
}
|
||||
|
||||
impl FindReferencesTool {
|
||||
pub fn new(project: Entity<Project>) -> Self {
|
||||
Self { project }
|
||||
}
|
||||
}
|
||||
|
||||
impl AgentTool for FindReferencesTool {
|
||||
type Input = FindReferencesToolInput;
|
||||
type Output = String;
|
||||
|
||||
const NAME: &'static str = "find_references";
|
||||
|
||||
fn kind() -> acp::ToolKind {
|
||||
acp::ToolKind::Search
|
||||
}
|
||||
|
||||
fn initial_title(
|
||||
&self,
|
||||
input: Result<Self::Input, serde_json::Value>,
|
||||
_cx: &mut App,
|
||||
) -> SharedString {
|
||||
if let Ok(input) = input {
|
||||
format!("Find references to `{}`", input.symbol.symbol_name).into()
|
||||
} else {
|
||||
"Find references".into()
|
||||
}
|
||||
}
|
||||
|
||||
fn run(
|
||||
self: Arc<Self>,
|
||||
input: ToolInput<Self::Input>,
|
||||
_event_stream: ToolCallEventStream,
|
||||
cx: &mut App,
|
||||
) -> Task<Result<String, String>> {
|
||||
let project = self.project.clone();
|
||||
cx.spawn(async move |cx| {
|
||||
let input = input
|
||||
.recv()
|
||||
.await
|
||||
.map_err(|e| format!("Failed to receive tool input: {e}"))?;
|
||||
|
||||
let resolved = input.symbol.resolve(&project, cx).await?;
|
||||
|
||||
let references_task = project.update(cx, |project, cx| {
|
||||
project.references(&resolved.buffer, resolved.position, cx)
|
||||
});
|
||||
|
||||
let references = references_task
|
||||
.await
|
||||
.map_err(|e| format!("Find references failed: {e}"))?
|
||||
.unwrap_or_default();
|
||||
|
||||
if references.is_empty() {
|
||||
return Ok(format!(
|
||||
"No references found for '{}'.",
|
||||
input.symbol.symbol_name
|
||||
));
|
||||
}
|
||||
|
||||
let mut output = format!(
|
||||
"Found {} references to `{}`:\n",
|
||||
references.len(),
|
||||
input.symbol.symbol_name
|
||||
);
|
||||
|
||||
for location in &references {
|
||||
let display = location
|
||||
.buffer
|
||||
.read_with(cx, |_, cx| LocationDisplay::from_location(location, cx));
|
||||
write!(output, "\n## {display}\n").ok();
|
||||
}
|
||||
|
||||
Ok(output)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
use std::fmt::Write;
|
||||
use std::sync::Arc;
|
||||
|
||||
use agent_client_protocol::schema as acp;
|
||||
use gpui::{App, Entity, SharedString, Task};
|
||||
use project::Project;
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use super::symbol_locator::{CodeActionStore, PendingCodeActions, SymbolLocator};
|
||||
use crate::{AgentTool, ToolCallEventStream, ToolInput};
|
||||
|
||||
/// Gets the list of available code actions at a symbol location from the language server.
|
||||
///
|
||||
/// Code actions include quick fixes, refactorings, and other automated transformations
|
||||
/// suggested by the language server (e.g. "Add missing import", "Extract to function").
|
||||
///
|
||||
/// Returns a numbered list of available actions. Use apply_code_action with the
|
||||
/// corresponding number to apply one.
|
||||
///
|
||||
/// Before using this tool, use read_file or grep to find the exact symbol
|
||||
/// name and line number.
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct GetCodeActionsToolInput {
|
||||
/// The symbol to get code actions for.
|
||||
pub symbol: SymbolLocator,
|
||||
}
|
||||
|
||||
pub struct GetCodeActionsTool {
|
||||
project: Entity<Project>,
|
||||
code_action_store: CodeActionStore,
|
||||
}
|
||||
|
||||
impl GetCodeActionsTool {
|
||||
pub fn new(project: Entity<Project>, code_action_store: CodeActionStore) -> Self {
|
||||
Self {
|
||||
project,
|
||||
code_action_store,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl AgentTool for GetCodeActionsTool {
|
||||
type Input = GetCodeActionsToolInput;
|
||||
type Output = String;
|
||||
|
||||
const NAME: &'static str = "get_code_actions";
|
||||
|
||||
fn kind() -> acp::ToolKind {
|
||||
acp::ToolKind::Search
|
||||
}
|
||||
|
||||
fn initial_title(
|
||||
&self,
|
||||
input: Result<Self::Input, serde_json::Value>,
|
||||
_cx: &mut App,
|
||||
) -> SharedString {
|
||||
if let Ok(input) = input {
|
||||
format!("Get code actions for `{}`", input.symbol.symbol_name).into()
|
||||
} else {
|
||||
"Get code actions".into()
|
||||
}
|
||||
}
|
||||
|
||||
fn run(
|
||||
self: Arc<Self>,
|
||||
input: ToolInput<Self::Input>,
|
||||
_event_stream: ToolCallEventStream,
|
||||
cx: &mut App,
|
||||
) -> Task<Result<String, String>> {
|
||||
let project = self.project.clone();
|
||||
let store = self.code_action_store.clone();
|
||||
cx.spawn(async move |cx| {
|
||||
let input = input
|
||||
.recv()
|
||||
.await
|
||||
.map_err(|e| format!("Failed to receive tool input: {e}"))?;
|
||||
|
||||
let resolved = input.symbol.resolve(&project, cx).await?;
|
||||
|
||||
let actions_task = project.update(cx, |project, cx| {
|
||||
let range = resolved.position..resolved.position;
|
||||
project.code_actions(&resolved.buffer, range, None, cx)
|
||||
});
|
||||
|
||||
let actions = actions_task
|
||||
.await
|
||||
.map_err(|e| format!("Failed to get code actions: {e}"))?
|
||||
.unwrap_or_default();
|
||||
|
||||
if actions.is_empty() {
|
||||
store.update(cx, |store, _cx| *store = None);
|
||||
return Ok(format!(
|
||||
"No code actions available for '{}' at this location.",
|
||||
input.symbol.symbol_name
|
||||
));
|
||||
}
|
||||
|
||||
let mut output = format!("Found {} code action(s):\n", actions.len());
|
||||
for (i, action) in actions.iter().enumerate() {
|
||||
writeln!(output, "{}. {}", i + 1, action.lsp_action.title()).ok();
|
||||
}
|
||||
write!(
|
||||
output,
|
||||
"\nUse apply_code_action with the number of the action you want to apply."
|
||||
)
|
||||
.ok();
|
||||
|
||||
store.update(cx, |store, _cx| {
|
||||
*store = Some(PendingCodeActions {
|
||||
actions,
|
||||
buffer: resolved.buffer,
|
||||
});
|
||||
});
|
||||
|
||||
Ok(output)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
use std::fmt::Write;
|
||||
use std::sync::Arc;
|
||||
|
||||
use super::symbol_locator::{LocationDisplay, SymbolLocator};
|
||||
use crate::{AgentTool, ToolCallEventStream, ToolInput};
|
||||
use agent_client_protocol::schema as acp;
|
||||
use gpui::{App, Entity, SharedString, Task};
|
||||
use project::Project;
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// Jumps to the definition of a symbol using the language server.
|
||||
///
|
||||
/// Returns the file path and line number of the symbol's definition,
|
||||
/// along with a snippet of the source code at that location.
|
||||
///
|
||||
/// Before using this tool, use read_file or grep to find the exact symbol
|
||||
/// name and line number of a usage you want to navigate from.
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct GoToDefinitionToolInput {
|
||||
/// The symbol to find the definition of.
|
||||
pub symbol: SymbolLocator,
|
||||
}
|
||||
|
||||
pub struct GoToDefinitionTool {
|
||||
project: Entity<Project>,
|
||||
}
|
||||
|
||||
impl GoToDefinitionTool {
|
||||
pub fn new(project: Entity<Project>) -> Self {
|
||||
Self { project }
|
||||
}
|
||||
}
|
||||
|
||||
impl AgentTool for GoToDefinitionTool {
|
||||
type Input = GoToDefinitionToolInput;
|
||||
type Output = String;
|
||||
|
||||
const NAME: &'static str = "go_to_definition";
|
||||
|
||||
fn kind() -> acp::ToolKind {
|
||||
acp::ToolKind::Search
|
||||
}
|
||||
|
||||
fn initial_title(
|
||||
&self,
|
||||
input: Result<Self::Input, serde_json::Value>,
|
||||
_cx: &mut App,
|
||||
) -> SharedString {
|
||||
if let Ok(input) = input {
|
||||
format!("Go to definition of `{}`", input.symbol.symbol_name).into()
|
||||
} else {
|
||||
"Go to definition".into()
|
||||
}
|
||||
}
|
||||
|
||||
fn run(
|
||||
self: Arc<Self>,
|
||||
input: ToolInput<Self::Input>,
|
||||
_event_stream: ToolCallEventStream,
|
||||
cx: &mut App,
|
||||
) -> Task<Result<String, String>> {
|
||||
let project = self.project.clone();
|
||||
cx.spawn(async move |cx| {
|
||||
let input = input
|
||||
.recv()
|
||||
.await
|
||||
.map_err(|e| format!("Failed to receive tool input: {e}"))?;
|
||||
|
||||
let resolved = input.symbol.resolve(&project, cx).await?;
|
||||
|
||||
let definitions_task = project.update(cx, |project, cx| {
|
||||
project.definitions(&resolved.buffer, resolved.position, cx)
|
||||
});
|
||||
|
||||
let definitions = definitions_task
|
||||
.await
|
||||
.map_err(|e| format!("Go to definition failed: {e}"))?
|
||||
.unwrap_or_default();
|
||||
|
||||
if definitions.is_empty() {
|
||||
return Ok(format!(
|
||||
"No definition found for '{}'.",
|
||||
input.symbol.symbol_name
|
||||
));
|
||||
}
|
||||
|
||||
let mut output = String::new();
|
||||
|
||||
if definitions.len() == 1 {
|
||||
write!(output, "Definition of `{}`:\n", input.symbol.symbol_name).ok();
|
||||
} else {
|
||||
write!(
|
||||
output,
|
||||
"Found {} definitions of `{}`:\n",
|
||||
definitions.len(),
|
||||
input.symbol.symbol_name
|
||||
)
|
||||
.ok();
|
||||
}
|
||||
|
||||
for link in &definitions {
|
||||
let display = link
|
||||
.target
|
||||
.buffer
|
||||
.read_with(cx, |_, cx| LocationDisplay::from_location(&link.target, cx));
|
||||
write!(output, "\n## {display}\n").ok();
|
||||
}
|
||||
|
||||
Ok(output)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
use std::fmt::Write;
|
||||
use std::sync::Arc;
|
||||
|
||||
use agent_client_protocol::schema as acp;
|
||||
use gpui::{App, Entity, SharedString, Task};
|
||||
use project::Project;
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use super::symbol_locator::SymbolLocator;
|
||||
use crate::{AgentTool, ToolCallEventStream, ToolInput};
|
||||
|
||||
/// Renames a symbol across the project using the language server.
|
||||
///
|
||||
/// This performs a semantic rename, updating all references to the symbol
|
||||
/// across all files in the project. The language server determines which
|
||||
/// occurrences to rename based on the symbol's type and scope.
|
||||
///
|
||||
/// Before using this tool, use read_file or grep to find the exact symbol
|
||||
/// name and line number.
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct RenameToolInput {
|
||||
/// The symbol to rename.
|
||||
pub symbol: SymbolLocator,
|
||||
|
||||
/// The new name for the symbol.
|
||||
pub new_name: String,
|
||||
}
|
||||
|
||||
pub struct RenameTool {
|
||||
project: Entity<Project>,
|
||||
}
|
||||
|
||||
impl RenameTool {
|
||||
pub fn new(project: Entity<Project>) -> Self {
|
||||
Self { project }
|
||||
}
|
||||
}
|
||||
|
||||
impl AgentTool for RenameTool {
|
||||
type Input = RenameToolInput;
|
||||
type Output = String;
|
||||
|
||||
const NAME: &'static str = "rename_symbol";
|
||||
|
||||
fn kind() -> acp::ToolKind {
|
||||
acp::ToolKind::Other
|
||||
}
|
||||
|
||||
fn initial_title(
|
||||
&self,
|
||||
input: Result<Self::Input, serde_json::Value>,
|
||||
_cx: &mut App,
|
||||
) -> SharedString {
|
||||
if let Ok(input) = input {
|
||||
format!(
|
||||
"Rename `{}` to `{}`",
|
||||
input.symbol.symbol_name, input.new_name
|
||||
)
|
||||
.into()
|
||||
} else {
|
||||
"Rename symbol".into()
|
||||
}
|
||||
}
|
||||
|
||||
fn run(
|
||||
self: Arc<Self>,
|
||||
input: ToolInput<Self::Input>,
|
||||
_event_stream: ToolCallEventStream,
|
||||
cx: &mut App,
|
||||
) -> Task<Result<String, String>> {
|
||||
let project = self.project.clone();
|
||||
cx.spawn(async move |cx| {
|
||||
let input = input
|
||||
.recv()
|
||||
.await
|
||||
.map_err(|e| format!("Failed to receive tool input: {e}"))?;
|
||||
|
||||
let resolved = input.symbol.resolve(&project, cx).await?;
|
||||
|
||||
let rename_task = project.update(cx, |project, cx| {
|
||||
project.perform_rename(
|
||||
resolved.buffer.clone(),
|
||||
resolved.position,
|
||||
input.new_name.clone(),
|
||||
cx,
|
||||
)
|
||||
});
|
||||
|
||||
let transaction = rename_task
|
||||
.await
|
||||
.map_err(|e| format!("Rename failed: {e}"))?;
|
||||
|
||||
if transaction.0.is_empty() {
|
||||
return Ok(format!(
|
||||
"No changes were made. The language server could not rename '{}'.",
|
||||
input.symbol.symbol_name
|
||||
));
|
||||
}
|
||||
|
||||
let mut output = format!(
|
||||
"Renamed `{}` to `{}` in {} file(s):\n",
|
||||
input.symbol.symbol_name,
|
||||
input.new_name,
|
||||
transaction.0.len()
|
||||
);
|
||||
|
||||
for (buffer, _) in &transaction.0 {
|
||||
buffer.read_with(cx, |buffer, cx| {
|
||||
let path = buffer
|
||||
.file()
|
||||
.map(|f| f.full_path(cx).display().to_string())
|
||||
.unwrap_or_else(|| "<untitled>".to_string());
|
||||
writeln!(output, "- {path}").ok();
|
||||
});
|
||||
}
|
||||
|
||||
Ok(output)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,239 @@
|
||||
use std::collections::VecDeque;
|
||||
use std::fmt;
|
||||
|
||||
use gpui::{App, AsyncApp, Entity};
|
||||
use language::{Buffer, Location};
|
||||
use project::{CodeAction, Project};
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use text::ToPoint as _;
|
||||
use text::{Anchor, Point};
|
||||
|
||||
/// Identifies a specific symbol (declaration or usage) in the source code.
|
||||
///
|
||||
/// Use the file path, line number, and symbol name from file outlines, grep results,
|
||||
/// or other tool outputs to populate these fields.
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct SymbolLocator {
|
||||
/// The relative path of the file containing the symbol
|
||||
/// (e.g. "crates/editor/src/editor.rs").
|
||||
pub file_path: String,
|
||||
|
||||
/// The 1-based line number where the symbol appears.
|
||||
/// Use the line numbers from file outlines or grep results.
|
||||
pub line: u32,
|
||||
|
||||
/// The name of the symbol (function name, type name, variable name, etc.)
|
||||
pub symbol_name: String,
|
||||
}
|
||||
|
||||
pub struct PendingCodeActions {
|
||||
pub actions: Vec<CodeAction>,
|
||||
pub buffer: Entity<Buffer>,
|
||||
}
|
||||
|
||||
pub type CodeActionStore = Entity<Option<PendingCodeActions>>;
|
||||
|
||||
pub struct ResolvedSymbol {
|
||||
pub buffer: Entity<Buffer>,
|
||||
pub position: Anchor,
|
||||
pub line_text: String,
|
||||
pub truncated: bool,
|
||||
}
|
||||
|
||||
pub const MAX_LINE_DISPLAY_LEN: usize = 200;
|
||||
|
||||
pub struct LocationDisplay {
|
||||
pub path: String,
|
||||
pub start_line: u32,
|
||||
pub end_line: u32,
|
||||
pub snippet: String,
|
||||
pub truncated: bool,
|
||||
}
|
||||
|
||||
impl LocationDisplay {
|
||||
pub fn from_location(location: &Location, cx: &App) -> Self {
|
||||
let snapshot = location.buffer.read(cx).snapshot();
|
||||
let range =
|
||||
location.range.start.to_point(&snapshot)..location.range.end.to_point(&snapshot);
|
||||
let path = location
|
||||
.buffer
|
||||
.read(cx)
|
||||
.file()
|
||||
.map(|f| f.full_path(cx).display().to_string())
|
||||
.unwrap_or_else(|| "<untitled>".to_string());
|
||||
|
||||
let start_line = range.start.row + 1;
|
||||
let end_line = range.end.row + 1;
|
||||
|
||||
let line_len = snapshot.line_len(range.start.row);
|
||||
let truncated = line_len as usize > MAX_LINE_DISPLAY_LEN;
|
||||
let snippet: String = snapshot
|
||||
.text_for_range(Point::new(range.start.row, 0)..Point::new(range.start.row, line_len))
|
||||
.flat_map(|chunk| chunk.chars())
|
||||
.skip_while(|c| c.is_whitespace())
|
||||
.take(MAX_LINE_DISPLAY_LEN)
|
||||
.collect::<String>();
|
||||
let snippet = snippet.trim_end().to_string();
|
||||
|
||||
Self {
|
||||
path,
|
||||
start_line,
|
||||
end_line,
|
||||
snippet,
|
||||
truncated,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for LocationDisplay {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
let truncated_label = if self.truncated { " (truncated)" } else { "" };
|
||||
if self.start_line == self.end_line {
|
||||
writeln!(f, "{}#L{}{truncated_label}", self.path, self.start_line)?;
|
||||
} else {
|
||||
writeln!(
|
||||
f,
|
||||
"{}#L{}-{}{truncated_label}",
|
||||
self.path, self.start_line, self.end_line
|
||||
)?;
|
||||
}
|
||||
writeln!(f, "```")?;
|
||||
writeln!(f, "{}", self.snippet)?;
|
||||
write!(f, "```")
|
||||
}
|
||||
}
|
||||
|
||||
/// Searches for `needle` in a char iterator, returning the byte offset of the
|
||||
/// first occurrence without collecting the full iterator into a string.
|
||||
///
|
||||
/// Equivalent to [`str::find`]
|
||||
fn find_in_char_iter(chars: impl Iterator<Item = char>, needle: &str) -> Option<usize> {
|
||||
let needle_chars: Vec<char> = needle.chars().collect();
|
||||
if needle_chars.is_empty() {
|
||||
return Some(0);
|
||||
}
|
||||
|
||||
let mut window: VecDeque<char> = VecDeque::with_capacity(needle_chars.len());
|
||||
let mut byte_offsets: VecDeque<usize> = VecDeque::with_capacity(needle_chars.len());
|
||||
let mut byte_offset = 0usize;
|
||||
|
||||
for ch in chars {
|
||||
window.push_back(ch);
|
||||
byte_offsets.push_back(byte_offset);
|
||||
byte_offset += ch.len_utf8();
|
||||
|
||||
if window.len() > needle_chars.len() {
|
||||
window.pop_front();
|
||||
byte_offsets.pop_front();
|
||||
}
|
||||
|
||||
if window.len() == needle_chars.len()
|
||||
&& window.iter().zip(needle_chars.iter()).all(|(a, b)| a == b)
|
||||
{
|
||||
return byte_offsets.front().copied();
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
impl SymbolLocator {
|
||||
/// Resolves this locator into a concrete buffer and position.
|
||||
///
|
||||
/// Opens the file at `file_path`, then searches for `symbol_name` on the
|
||||
/// specified `line`. Returns an error if the file can't be found, the line
|
||||
/// is out of range, or the symbol name doesn't appear on that line.
|
||||
/// If the symbol name appears multiple times on the line, uses the first
|
||||
/// occurrence.
|
||||
pub async fn resolve(
|
||||
&self,
|
||||
project: &Entity<Project>,
|
||||
cx: &mut AsyncApp,
|
||||
) -> Result<ResolvedSymbol, String> {
|
||||
let Self {
|
||||
file_path,
|
||||
line,
|
||||
symbol_name,
|
||||
} = self;
|
||||
|
||||
let open_buffer_task = project.update(cx, |project, cx| {
|
||||
let Some(project_path) = project.find_project_path(file_path, cx) else {
|
||||
return Err(format!("Could not find path '{file_path}' in project",));
|
||||
};
|
||||
Ok(project.open_buffer(project_path, cx))
|
||||
})?;
|
||||
|
||||
let buffer = open_buffer_task
|
||||
.await
|
||||
.map_err(|e| format!("Failed to open '{}': {e}", self.file_path))?;
|
||||
|
||||
let (position, line_text, truncated) = buffer.read_with(cx, |buffer, _cx| {
|
||||
let snapshot = buffer.snapshot();
|
||||
let row = line.saturating_sub(1);
|
||||
|
||||
if row > snapshot.max_point().row {
|
||||
let line_count = snapshot.max_point().row + 1;
|
||||
return Err(format!(
|
||||
"Line {line} is beyond the end of '{file_path}' (file has {line_count} lines)",
|
||||
));
|
||||
}
|
||||
|
||||
let line_len = snapshot.line_len(row);
|
||||
let truncated = line_len as usize > MAX_LINE_DISPLAY_LEN;
|
||||
let line_start = Point::new(row, 0);
|
||||
let line_end = Point::new(row, line_len);
|
||||
let line_chars = || {
|
||||
snapshot
|
||||
.text_for_range(line_start..line_end)
|
||||
.flat_map(|chunk| chunk.chars())
|
||||
};
|
||||
|
||||
let byte_offset = find_in_char_iter(line_chars(), symbol_name).ok_or_else(|| {
|
||||
let preview: String = line_chars()
|
||||
.skip_while(|c| c.is_whitespace())
|
||||
.take(MAX_LINE_DISPLAY_LEN)
|
||||
.collect();
|
||||
format!(
|
||||
"Symbol '{symbol_name}' not found on line {line} of '{file_path}'. \
|
||||
Line content: '{}'",
|
||||
preview.trim_end()
|
||||
)
|
||||
})?;
|
||||
|
||||
let position = snapshot.anchor_before(Point::new(row, byte_offset as u32));
|
||||
let display_text: String = line_chars()
|
||||
.skip_while(|c| c.is_whitespace())
|
||||
.take(MAX_LINE_DISPLAY_LEN)
|
||||
.collect::<String>();
|
||||
let display_text = display_text.trim_end().to_string();
|
||||
|
||||
Ok((position, display_text, truncated))
|
||||
})?;
|
||||
|
||||
Ok(ResolvedSymbol {
|
||||
buffer,
|
||||
position,
|
||||
line_text,
|
||||
truncated,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use gpui::proptest::prelude::*;
|
||||
|
||||
#[gpui::property_test]
|
||||
fn find_in_char_iter_test(
|
||||
// limited character sets to increase odds of finding matches
|
||||
#[strategy = "[abcd]{100,1000}"] haystack: String,
|
||||
#[strategy = "[abcd]{1,5}"] needle: String,
|
||||
) -> Result<(), TestCaseError> {
|
||||
let expected = haystack.find(&needle);
|
||||
let actual = find_in_char_iter(haystack.chars(), &needle);
|
||||
prop_assert_eq!(actual, expected);
|
||||
Ok::<_, TestCaseError>(())
|
||||
}
|
||||
}
|
||||
@@ -59,6 +59,18 @@ impl FeatureFlag for UpdatePlanToolFeatureFlag {
|
||||
}
|
||||
register_feature_flag!(UpdatePlanToolFeatureFlag);
|
||||
|
||||
pub struct LspToolFeatureFlag;
|
||||
|
||||
impl FeatureFlag for LspToolFeatureFlag {
|
||||
const NAME: &'static str = "lsp-tool";
|
||||
type Value = PresenceFlag;
|
||||
|
||||
fn enabled_for_staff() -> bool {
|
||||
false
|
||||
}
|
||||
}
|
||||
register_feature_flag!(LspToolFeatureFlag);
|
||||
|
||||
pub struct ProjectPanelUndoRedoFeatureFlag;
|
||||
|
||||
impl FeatureFlag for ProjectPanelUndoRedoFeatureFlag {
|
||||
|
||||
@@ -71,16 +71,20 @@ pub fn apply_seed_to_proptest_config(
|
||||
///
|
||||
/// Doesn't support many features of [`run_test`], since these are provided by
|
||||
/// proptest.
|
||||
pub fn run_test_once(seed: u64, test_fn: Box<dyn UnwindSafe + FnOnce(TestDispatcher)>) {
|
||||
pub fn run_test_once<R>(
|
||||
seed: u64,
|
||||
test_fn: Box<dyn UnwindSafe + FnOnce(TestDispatcher) -> R>,
|
||||
) -> R {
|
||||
let result = panic::catch_unwind(|| {
|
||||
let dispatcher = TestDispatcher::new(seed);
|
||||
let scheduler = dispatcher.scheduler().clone();
|
||||
test_fn(dispatcher);
|
||||
let res = test_fn(dispatcher);
|
||||
scheduler.end_test();
|
||||
res
|
||||
});
|
||||
|
||||
match result {
|
||||
Ok(()) => {}
|
||||
Ok(r) => r,
|
||||
Err(e) => panic::resume_unwind(e),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1407,13 +1407,18 @@ mod tests {
|
||||
// 2. Add it to this list with a comment explaining why it's excluded.
|
||||
const EXCLUDED_TOOLS: &[&str] = &[
|
||||
// Read-only / low-risk tools that don't call decide_permission_from_settings
|
||||
"apply_code_action",
|
||||
"diagnostics",
|
||||
"find_path",
|
||||
"find_references",
|
||||
"get_code_actions",
|
||||
"go_to_definition",
|
||||
"grep",
|
||||
"list_directory",
|
||||
"now",
|
||||
"open",
|
||||
"read_file",
|
||||
"rename_symbol",
|
||||
"thinking",
|
||||
// streaming_edit_file uses "edit_file" for permission lookups,
|
||||
// so its rules are configured under the edit_file entry.
|
||||
|
||||
Reference in new issue
Block a user