Delete obsolete tools (#29808)
Release Notes: - Removed some obsolete tools: batch_tool, code_actions, code_symbols, contents, symbol_info, rename Co-authored-by: Cole Miller <m@cole-miller.net>
This commit is contained in:
parent
9147f89257
commit
e6b0d8e48b
8 changed files with 0 additions and 1720 deletions
|
@ -683,10 +683,6 @@
|
||||||
"name": "Write",
|
"name": "Write",
|
||||||
"enable_all_context_servers": true,
|
"enable_all_context_servers": true,
|
||||||
"tools": {
|
"tools": {
|
||||||
"batch_tool": false,
|
|
||||||
"code_actions": false,
|
|
||||||
"code_symbols": false,
|
|
||||||
"contents": false,
|
|
||||||
"copy_path": false,
|
"copy_path": false,
|
||||||
"create_file": true,
|
"create_file": true,
|
||||||
"delete_path": false,
|
"delete_path": false,
|
||||||
|
@ -699,8 +695,6 @@
|
||||||
"find_path": true,
|
"find_path": true,
|
||||||
"read_file": true,
|
"read_file": true,
|
||||||
"grep": true,
|
"grep": true,
|
||||||
"rename": false,
|
|
||||||
"symbol_info": false,
|
|
||||||
"terminal": true,
|
"terminal": true,
|
||||||
"thinking": true,
|
"thinking": true,
|
||||||
"web_search": true
|
"web_search": true
|
||||||
|
|
|
@ -1,7 +1,3 @@
|
||||||
mod batch_tool;
|
|
||||||
mod code_action_tool;
|
|
||||||
mod code_symbols_tool;
|
|
||||||
mod contents_tool;
|
|
||||||
mod copy_path_tool;
|
mod copy_path_tool;
|
||||||
mod create_directory_tool;
|
mod create_directory_tool;
|
||||||
mod create_file_tool;
|
mod create_file_tool;
|
||||||
|
@ -17,11 +13,9 @@ mod move_path_tool;
|
||||||
mod now_tool;
|
mod now_tool;
|
||||||
mod open_tool;
|
mod open_tool;
|
||||||
mod read_file_tool;
|
mod read_file_tool;
|
||||||
mod rename_tool;
|
|
||||||
mod replace;
|
mod replace;
|
||||||
mod schema;
|
mod schema;
|
||||||
mod streaming_edit_file_tool;
|
mod streaming_edit_file_tool;
|
||||||
mod symbol_info_tool;
|
|
||||||
mod templates;
|
mod templates;
|
||||||
mod terminal_tool;
|
mod terminal_tool;
|
||||||
mod thinking_tool;
|
mod thinking_tool;
|
||||||
|
@ -43,10 +37,6 @@ use web_search_tool::WebSearchTool;
|
||||||
|
|
||||||
pub(crate) use templates::*;
|
pub(crate) use templates::*;
|
||||||
|
|
||||||
use crate::batch_tool::BatchTool;
|
|
||||||
use crate::code_action_tool::CodeActionTool;
|
|
||||||
use crate::code_symbols_tool::CodeSymbolsTool;
|
|
||||||
use crate::contents_tool::ContentsTool;
|
|
||||||
use crate::create_directory_tool::CreateDirectoryTool;
|
use crate::create_directory_tool::CreateDirectoryTool;
|
||||||
use crate::delete_path_tool::DeletePathTool;
|
use crate::delete_path_tool::DeletePathTool;
|
||||||
use crate::diagnostics_tool::DiagnosticsTool;
|
use crate::diagnostics_tool::DiagnosticsTool;
|
||||||
|
@ -56,9 +46,7 @@ use crate::grep_tool::GrepTool;
|
||||||
use crate::list_directory_tool::ListDirectoryTool;
|
use crate::list_directory_tool::ListDirectoryTool;
|
||||||
use crate::now_tool::NowTool;
|
use crate::now_tool::NowTool;
|
||||||
use crate::read_file_tool::ReadFileTool;
|
use crate::read_file_tool::ReadFileTool;
|
||||||
use crate::rename_tool::RenameTool;
|
|
||||||
use crate::streaming_edit_file_tool::StreamingEditFileTool;
|
use crate::streaming_edit_file_tool::StreamingEditFileTool;
|
||||||
use crate::symbol_info_tool::SymbolInfoTool;
|
|
||||||
use crate::thinking_tool::ThinkingTool;
|
use crate::thinking_tool::ThinkingTool;
|
||||||
|
|
||||||
pub use create_file_tool::{CreateFileTool, CreateFileToolInput};
|
pub use create_file_tool::{CreateFileTool, CreateFileToolInput};
|
||||||
|
@ -73,23 +61,17 @@ pub fn init(http_client: Arc<HttpClientWithUrl>, cx: &mut App) {
|
||||||
|
|
||||||
let registry = ToolRegistry::global(cx);
|
let registry = ToolRegistry::global(cx);
|
||||||
registry.register_tool(TerminalTool);
|
registry.register_tool(TerminalTool);
|
||||||
registry.register_tool(BatchTool);
|
|
||||||
registry.register_tool(CreateDirectoryTool);
|
registry.register_tool(CreateDirectoryTool);
|
||||||
registry.register_tool(CopyPathTool);
|
registry.register_tool(CopyPathTool);
|
||||||
registry.register_tool(DeletePathTool);
|
registry.register_tool(DeletePathTool);
|
||||||
registry.register_tool(SymbolInfoTool);
|
|
||||||
registry.register_tool(CodeActionTool);
|
|
||||||
registry.register_tool(MovePathTool);
|
registry.register_tool(MovePathTool);
|
||||||
registry.register_tool(DiagnosticsTool);
|
registry.register_tool(DiagnosticsTool);
|
||||||
registry.register_tool(ListDirectoryTool);
|
registry.register_tool(ListDirectoryTool);
|
||||||
registry.register_tool(NowTool);
|
registry.register_tool(NowTool);
|
||||||
registry.register_tool(OpenTool);
|
registry.register_tool(OpenTool);
|
||||||
registry.register_tool(CodeSymbolsTool);
|
|
||||||
registry.register_tool(ContentsTool);
|
|
||||||
registry.register_tool(FindPathTool);
|
registry.register_tool(FindPathTool);
|
||||||
registry.register_tool(ReadFileTool);
|
registry.register_tool(ReadFileTool);
|
||||||
registry.register_tool(GrepTool);
|
registry.register_tool(GrepTool);
|
||||||
registry.register_tool(RenameTool);
|
|
||||||
registry.register_tool(ThinkingTool);
|
registry.register_tool(ThinkingTool);
|
||||||
registry.register_tool(FetchTool::new(http_client));
|
registry.register_tool(FetchTool::new(http_client));
|
||||||
|
|
||||||
|
|
|
@ -1,314 +0,0 @@
|
||||||
use crate::schema::json_schema_for;
|
|
||||||
use anyhow::{Result, anyhow};
|
|
||||||
use assistant_tool::{ActionLog, Tool, ToolResult, ToolWorkingSet};
|
|
||||||
use futures::future::join_all;
|
|
||||||
use gpui::{AnyWindowHandle, App, AppContext, Entity, Task};
|
|
||||||
use language_model::{LanguageModelRequestMessage, LanguageModelToolSchemaFormat};
|
|
||||||
use project::Project;
|
|
||||||
use schemars::JsonSchema;
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
use std::sync::Arc;
|
|
||||||
use ui::IconName;
|
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
|
|
||||||
pub struct ToolInvocation {
|
|
||||||
/// The name of the tool to invoke
|
|
||||||
pub name: String,
|
|
||||||
|
|
||||||
/// The input to the tool in JSON format
|
|
||||||
pub input: serde_json::Value,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
|
|
||||||
pub struct BatchToolInput {
|
|
||||||
/// The tool invocations to run as a batch. These tools will be run either sequentially
|
|
||||||
/// or concurrently depending on the `run_tools_concurrently` flag.
|
|
||||||
///
|
|
||||||
/// <example>
|
|
||||||
/// Basic file operations (concurrent)
|
|
||||||
///
|
|
||||||
/// ```json
|
|
||||||
/// {
|
|
||||||
/// "invocations": [
|
|
||||||
/// {
|
|
||||||
/// "name": "read_file",
|
|
||||||
/// "input": {
|
|
||||||
/// "path": "src/main.rs"
|
|
||||||
/// }
|
|
||||||
/// },
|
|
||||||
/// {
|
|
||||||
/// "name": "list_directory",
|
|
||||||
/// "input": {
|
|
||||||
/// "path": "src/lib"
|
|
||||||
/// }
|
|
||||||
/// },
|
|
||||||
/// {
|
|
||||||
/// "name": "grep",
|
|
||||||
/// "input": {
|
|
||||||
/// "regex": "fn run\\("
|
|
||||||
/// }
|
|
||||||
/// }
|
|
||||||
/// ],
|
|
||||||
/// "run_tools_concurrently": true
|
|
||||||
/// }
|
|
||||||
/// ```
|
|
||||||
/// </example>
|
|
||||||
///
|
|
||||||
/// <example>
|
|
||||||
/// Multiple find-replace operations on the same file (sequential)
|
|
||||||
///
|
|
||||||
/// ```json
|
|
||||||
/// {
|
|
||||||
/// "invocations": [
|
|
||||||
/// {
|
|
||||||
/// "name": "find_replace_file",
|
|
||||||
/// "input": {
|
|
||||||
/// "path": "src/config.rs",
|
|
||||||
/// "display_description": "Update default timeout value",
|
|
||||||
/// "find": "pub const DEFAULT_TIMEOUT: u64 = 30;\n\npub const MAX_RETRIES: u32 = 3;\n\npub const SERVER_URL: &str = \"https://api.example.com\";",
|
|
||||||
/// "replace": "pub const DEFAULT_TIMEOUT: u64 = 60;\n\npub const MAX_RETRIES: u32 = 3;\n\npub const SERVER_URL: &str = \"https://api.example.com\";"
|
|
||||||
/// }
|
|
||||||
/// },
|
|
||||||
/// {
|
|
||||||
/// "name": "find_replace_file",
|
|
||||||
/// "input": {
|
|
||||||
/// "path": "src/config.rs",
|
|
||||||
/// "display_description": "Update API endpoint URL",
|
|
||||||
/// "find": "pub const MAX_RETRIES: u32 = 3;\n\npub const SERVER_URL: &str = \"https://api.example.com\";\n\npub const API_VERSION: &str = \"v1\";",
|
|
||||||
/// "replace": "pub const MAX_RETRIES: u32 = 3;\n\npub const SERVER_URL: &str = \"https://api.newdomain.com\";\n\npub const API_VERSION: &str = \"v1\";"
|
|
||||||
/// }
|
|
||||||
/// }
|
|
||||||
/// ],
|
|
||||||
/// "run_tools_concurrently": false
|
|
||||||
/// }
|
|
||||||
/// ```
|
|
||||||
/// </example>
|
|
||||||
///
|
|
||||||
/// <example>
|
|
||||||
/// Searching and analyzing code (concurrent)
|
|
||||||
///
|
|
||||||
/// ```json
|
|
||||||
/// {
|
|
||||||
/// "invocations": [
|
|
||||||
/// {
|
|
||||||
/// "name": "grep",
|
|
||||||
/// "input": {
|
|
||||||
/// "regex": "impl Database"
|
|
||||||
/// }
|
|
||||||
/// },
|
|
||||||
/// {
|
|
||||||
/// "name": "find_path",
|
|
||||||
/// "input": {
|
|
||||||
/// "glob": "**/*test*.rs"
|
|
||||||
/// }
|
|
||||||
/// }
|
|
||||||
/// ],
|
|
||||||
/// "run_tools_concurrently": true
|
|
||||||
/// }
|
|
||||||
/// ```
|
|
||||||
/// </example>
|
|
||||||
///
|
|
||||||
/// <example>
|
|
||||||
/// Multi-file refactoring (concurrent)
|
|
||||||
///
|
|
||||||
/// ```json
|
|
||||||
/// {
|
|
||||||
/// "invocations": [
|
|
||||||
/// {
|
|
||||||
/// "name": "find_replace_file",
|
|
||||||
/// "input": {
|
|
||||||
/// "path": "src/models/user.rs",
|
|
||||||
/// "display_description": "Add email field to User struct",
|
|
||||||
/// "find": "pub struct User {\n pub id: u64,\n pub username: String,\n pub created_at: DateTime<Utc>,\n}",
|
|
||||||
/// "replace": "pub struct User {\n pub id: u64,\n pub username: String,\n pub email: String,\n pub created_at: DateTime<Utc>,\n}"
|
|
||||||
/// }
|
|
||||||
/// },
|
|
||||||
/// {
|
|
||||||
/// "name": "find_replace_file",
|
|
||||||
/// "input": {
|
|
||||||
/// "path": "src/db/queries.rs",
|
|
||||||
/// "display_description": "Update user insertion query",
|
|
||||||
/// "find": "pub async fn insert_user(conn: &mut Connection, user: &User) -> Result<(), DbError> {\n conn.execute(\n \"INSERT INTO users (id, username, created_at) VALUES ($1, $2, $3)\",\n &[&user.id, &user.username, &user.created_at],\n ).await?;\n \n Ok(())\n}",
|
|
||||||
/// "replace": "pub async fn insert_user(conn: &mut Connection, user: &User) -> Result<(), DbError> {\n conn.execute(\n \"INSERT INTO users (id, username, email, created_at) VALUES ($1, $2, $3, $4)\",\n &[&user.id, &user.username, &user.email, &user.created_at],\n ).await?;\n \n Ok(())\n}"
|
|
||||||
/// }
|
|
||||||
/// }
|
|
||||||
/// ],
|
|
||||||
/// "run_tools_concurrently": true
|
|
||||||
/// }
|
|
||||||
/// ```
|
|
||||||
/// </example>
|
|
||||||
pub invocations: Vec<ToolInvocation>,
|
|
||||||
|
|
||||||
/// Whether to run the tools in this batch concurrently. If this is false (the default), the tools will run sequentially.
|
|
||||||
#[serde(default)]
|
|
||||||
pub run_tools_concurrently: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct BatchTool;
|
|
||||||
|
|
||||||
impl Tool for BatchTool {
|
|
||||||
fn name(&self) -> String {
|
|
||||||
"batch_tool".into()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn needs_confirmation(&self, input: &serde_json::Value, cx: &App) -> bool {
|
|
||||||
serde_json::from_value::<BatchToolInput>(input.clone())
|
|
||||||
.map(|input| {
|
|
||||||
let working_set = ToolWorkingSet::default();
|
|
||||||
input.invocations.iter().any(|invocation| {
|
|
||||||
working_set
|
|
||||||
.tool(&invocation.name, cx)
|
|
||||||
.map_or(false, |tool| tool.needs_confirmation(&invocation.input, cx))
|
|
||||||
})
|
|
||||||
})
|
|
||||||
.unwrap_or(false)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn description(&self) -> String {
|
|
||||||
include_str!("./batch_tool/description.md").into()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn icon(&self) -> IconName {
|
|
||||||
IconName::Cog
|
|
||||||
}
|
|
||||||
|
|
||||||
fn input_schema(&self, format: LanguageModelToolSchemaFormat) -> Result<serde_json::Value> {
|
|
||||||
json_schema_for::<BatchToolInput>(format)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn ui_text(&self, input: &serde_json::Value) -> String {
|
|
||||||
match serde_json::from_value::<BatchToolInput>(input.clone()) {
|
|
||||||
Ok(input) => {
|
|
||||||
let count = input.invocations.len();
|
|
||||||
let mode = if input.run_tools_concurrently {
|
|
||||||
"concurrently"
|
|
||||||
} else {
|
|
||||||
"sequentially"
|
|
||||||
};
|
|
||||||
|
|
||||||
let first_tool_name = input
|
|
||||||
.invocations
|
|
||||||
.first()
|
|
||||||
.map(|inv| inv.name.clone())
|
|
||||||
.unwrap_or_default();
|
|
||||||
|
|
||||||
let all_same = input
|
|
||||||
.invocations
|
|
||||||
.iter()
|
|
||||||
.all(|invocation| invocation.name == first_tool_name);
|
|
||||||
|
|
||||||
if all_same {
|
|
||||||
format!(
|
|
||||||
"Run `{}` {} times {}",
|
|
||||||
first_tool_name,
|
|
||||||
input.invocations.len(),
|
|
||||||
mode
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
format!("Run {} tools {}", count, mode)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Err(_) => "Batch tools".to_string(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn run(
|
|
||||||
self: Arc<Self>,
|
|
||||||
input: serde_json::Value,
|
|
||||||
messages: &[LanguageModelRequestMessage],
|
|
||||||
project: Entity<Project>,
|
|
||||||
action_log: Entity<ActionLog>,
|
|
||||||
window: Option<AnyWindowHandle>,
|
|
||||||
cx: &mut App,
|
|
||||||
) -> ToolResult {
|
|
||||||
let input = match serde_json::from_value::<BatchToolInput>(input) {
|
|
||||||
Ok(input) => input,
|
|
||||||
Err(err) => return Task::ready(Err(anyhow!(err))).into(),
|
|
||||||
};
|
|
||||||
|
|
||||||
if input.invocations.is_empty() {
|
|
||||||
return Task::ready(Err(anyhow!("No tool invocations provided"))).into();
|
|
||||||
}
|
|
||||||
|
|
||||||
let run_tools_concurrently = input.run_tools_concurrently;
|
|
||||||
|
|
||||||
let foreground_task = {
|
|
||||||
let working_set = ToolWorkingSet::default();
|
|
||||||
let invocations = input.invocations;
|
|
||||||
let messages = messages.to_vec();
|
|
||||||
|
|
||||||
cx.spawn(async move |cx| {
|
|
||||||
let mut tasks = Vec::new();
|
|
||||||
let mut tool_names = Vec::new();
|
|
||||||
|
|
||||||
for invocation in invocations {
|
|
||||||
let tool_name = invocation.name.clone();
|
|
||||||
tool_names.push(tool_name.clone());
|
|
||||||
|
|
||||||
let tool = cx
|
|
||||||
.update(|cx| working_set.tool(&tool_name, cx))
|
|
||||||
.map_err(|err| {
|
|
||||||
anyhow!("Failed to look up tool '{}': {}", tool_name, err)
|
|
||||||
})?;
|
|
||||||
|
|
||||||
let Some(tool) = tool else {
|
|
||||||
return Err(anyhow!("Tool '{}' not found", tool_name));
|
|
||||||
};
|
|
||||||
|
|
||||||
let project = project.clone();
|
|
||||||
let action_log = action_log.clone();
|
|
||||||
let messages = messages.clone();
|
|
||||||
let tool_result = cx
|
|
||||||
.update(|cx| {
|
|
||||||
tool.run(invocation.input, &messages, project, action_log, window, cx)
|
|
||||||
})
|
|
||||||
.map_err(|err| anyhow!("Failed to start tool '{}': {}", tool_name, err))?;
|
|
||||||
|
|
||||||
tasks.push(tool_result.output);
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok((tasks, tool_names))
|
|
||||||
})
|
|
||||||
};
|
|
||||||
|
|
||||||
cx.background_spawn(async move {
|
|
||||||
let (tasks, tool_names) = foreground_task.await?;
|
|
||||||
let mut results = Vec::with_capacity(tasks.len());
|
|
||||||
|
|
||||||
if run_tools_concurrently {
|
|
||||||
results.extend(join_all(tasks).await)
|
|
||||||
} else {
|
|
||||||
for task in tasks {
|
|
||||||
results.push(task.await);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut formatted_results = String::new();
|
|
||||||
let mut error_occurred = false;
|
|
||||||
|
|
||||||
for (i, result) in results.into_iter().enumerate() {
|
|
||||||
let tool_name = &tool_names[i];
|
|
||||||
|
|
||||||
match result {
|
|
||||||
Ok(output) => {
|
|
||||||
formatted_results
|
|
||||||
.push_str(&format!("Tool '{}' result:\n{}\n\n", tool_name, output));
|
|
||||||
}
|
|
||||||
Err(err) => {
|
|
||||||
error_occurred = true;
|
|
||||||
formatted_results
|
|
||||||
.push_str(&format!("Tool '{}' error: {}\n\n", tool_name, err));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if error_occurred {
|
|
||||||
formatted_results
|
|
||||||
.push_str("Note: Some tool invocations failed. See individual results above.");
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(formatted_results.trim().to_string())
|
|
||||||
})
|
|
||||||
.into()
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,388 +0,0 @@
|
||||||
use anyhow::{Context as _, Result, anyhow};
|
|
||||||
use assistant_tool::{ActionLog, Tool, ToolResult};
|
|
||||||
use gpui::{AnyWindowHandle, App, Entity, Task};
|
|
||||||
use language::{self, Anchor, Buffer, ToPointUtf16};
|
|
||||||
use language_model::{LanguageModelRequestMessage, LanguageModelToolSchemaFormat};
|
|
||||||
use project::{self, LspAction, Project};
|
|
||||||
use regex::Regex;
|
|
||||||
use schemars::JsonSchema;
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
use std::{ops::Range, sync::Arc};
|
|
||||||
use ui::IconName;
|
|
||||||
|
|
||||||
use crate::schema::json_schema_for;
|
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
|
|
||||||
pub struct CodeActionToolInput {
|
|
||||||
/// The relative path to the file containing the text range.
|
|
||||||
///
|
|
||||||
/// WARNING: you MUST start this path with one of the project's root directories.
|
|
||||||
pub path: String,
|
|
||||||
|
|
||||||
/// The specific code action to execute.
|
|
||||||
///
|
|
||||||
/// If this field is provided, the tool will execute the specified action.
|
|
||||||
/// If omitted, the tool will list all available code actions for the text range.
|
|
||||||
///
|
|
||||||
/// Here are some actions that are commonly supported (but may not be for this particular
|
|
||||||
/// text range; you can omit this field to list all the actions, if you want to know
|
|
||||||
/// what your options are, or you can just try an action and if it fails I'll tell you
|
|
||||||
/// what the available actions were instead):
|
|
||||||
/// - "quickfix.all" - applies all available quick fixes in the range
|
|
||||||
/// - "source.organizeImports" - sorts and cleans up import statements
|
|
||||||
/// - "source.fixAll" - applies all available auto fixes
|
|
||||||
/// - "refactor.extract" - extracts selected code into a new function or variable
|
|
||||||
/// - "refactor.inline" - inlines a variable by replacing references with its value
|
|
||||||
/// - "refactor.rewrite" - general code rewriting operations
|
|
||||||
/// - "source.addMissingImports" - adds imports for references that lack them
|
|
||||||
/// - "source.removeUnusedImports" - removes imports that aren't being used
|
|
||||||
/// - "source.implementInterface" - generates methods required by an interface/trait
|
|
||||||
/// - "source.generateAccessors" - creates getter/setter methods
|
|
||||||
/// - "source.convertToAsyncFunction" - converts callback-style code to async/await
|
|
||||||
///
|
|
||||||
/// Also, there is a special case: if you specify exactly "textDocument/rename" as the action,
|
|
||||||
/// then this will rename the symbol to whatever string you specified for the `arguments` field.
|
|
||||||
pub action: Option<String>,
|
|
||||||
|
|
||||||
/// Optional arguments to pass to the code action.
|
|
||||||
///
|
|
||||||
/// For rename operations (when action="textDocument/rename"), this should contain the new name.
|
|
||||||
/// For other code actions, these arguments may be passed to the language server.
|
|
||||||
pub arguments: Option<serde_json::Value>,
|
|
||||||
|
|
||||||
/// The text that comes immediately before the text range in the file.
|
|
||||||
pub context_before_range: String,
|
|
||||||
|
|
||||||
/// The text range. This text must appear in the file right between `context_before_range`
|
|
||||||
/// and `context_after_range`.
|
|
||||||
///
|
|
||||||
/// The file must contain exactly one occurrence of `context_before_range` followed by
|
|
||||||
/// `text_range` followed by `context_after_range`. If the file contains zero occurrences,
|
|
||||||
/// or if it contains more than one occurrence, the tool will fail, so it is absolutely
|
|
||||||
/// critical that you verify ahead of time that the string is unique. You can search
|
|
||||||
/// the file's contents to verify this ahead of time.
|
|
||||||
///
|
|
||||||
/// To make the string more likely to be unique, include a minimum of 1 line of context
|
|
||||||
/// before the text range, as well as a minimum of 1 line of context after the text range.
|
|
||||||
/// If these lines of context are not enough to obtain a string that appears only once
|
|
||||||
/// in the file, then double the number of context lines until the string becomes unique.
|
|
||||||
/// (Start with 1 line before and 1 line after though, because too much context is
|
|
||||||
/// needlessly costly.)
|
|
||||||
///
|
|
||||||
/// Do not alter the context lines of code in any way, and make sure to preserve all
|
|
||||||
/// whitespace and indentation for all lines of code. The combined string must be exactly
|
|
||||||
/// as it appears in the file, or else this tool call will fail.
|
|
||||||
pub text_range: String,
|
|
||||||
|
|
||||||
/// The text that comes immediately after the text range in the file.
|
|
||||||
pub context_after_range: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct CodeActionTool;
|
|
||||||
|
|
||||||
impl Tool for CodeActionTool {
|
|
||||||
fn name(&self) -> String {
|
|
||||||
"code_actions".into()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn needs_confirmation(&self, _input: &serde_json::Value, _cx: &App) -> bool {
|
|
||||||
false
|
|
||||||
}
|
|
||||||
|
|
||||||
fn description(&self) -> String {
|
|
||||||
include_str!("./code_action_tool/description.md").into()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn icon(&self) -> IconName {
|
|
||||||
IconName::Wand
|
|
||||||
}
|
|
||||||
|
|
||||||
fn input_schema(&self, format: LanguageModelToolSchemaFormat) -> Result<serde_json::Value> {
|
|
||||||
json_schema_for::<CodeActionToolInput>(format)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn ui_text(&self, input: &serde_json::Value) -> String {
|
|
||||||
match serde_json::from_value::<CodeActionToolInput>(input.clone()) {
|
|
||||||
Ok(input) => {
|
|
||||||
if let Some(action) = &input.action {
|
|
||||||
if action == "textDocument/rename" {
|
|
||||||
let new_name = match &input.arguments {
|
|
||||||
Some(serde_json::Value::String(new_name)) => new_name.clone(),
|
|
||||||
Some(value) => {
|
|
||||||
if let Ok(new_name) =
|
|
||||||
serde_json::from_value::<String>(value.clone())
|
|
||||||
{
|
|
||||||
new_name
|
|
||||||
} else {
|
|
||||||
"invalid name".to_string()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
None => "missing name".to_string(),
|
|
||||||
};
|
|
||||||
format!("Rename '{}' to '{}'", input.text_range, new_name)
|
|
||||||
} else {
|
|
||||||
format!(
|
|
||||||
"Execute code action '{}' for '{}'",
|
|
||||||
action, input.text_range
|
|
||||||
)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
format!("List available code actions for '{}'", input.text_range)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Err(_) => "Perform code action".to_string(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn run(
|
|
||||||
self: Arc<Self>,
|
|
||||||
input: serde_json::Value,
|
|
||||||
_messages: &[LanguageModelRequestMessage],
|
|
||||||
project: Entity<Project>,
|
|
||||||
action_log: Entity<ActionLog>,
|
|
||||||
_window: Option<AnyWindowHandle>,
|
|
||||||
cx: &mut App,
|
|
||||||
) -> ToolResult {
|
|
||||||
let input = match serde_json::from_value::<CodeActionToolInput>(input) {
|
|
||||||
Ok(input) => input,
|
|
||||||
Err(err) => return Task::ready(Err(anyhow!(err))).into(),
|
|
||||||
};
|
|
||||||
|
|
||||||
cx.spawn(async move |cx| {
|
|
||||||
let buffer = {
|
|
||||||
let project_path = project.read_with(cx, |project, cx| {
|
|
||||||
project
|
|
||||||
.find_project_path(&input.path, cx)
|
|
||||||
.context("Path not found in project")
|
|
||||||
})??;
|
|
||||||
|
|
||||||
project.update(cx, |project, cx| project.open_buffer(project_path, cx))?.await?
|
|
||||||
};
|
|
||||||
|
|
||||||
action_log.update(cx, |action_log, cx| {
|
|
||||||
action_log.track_buffer(buffer.clone(), cx);
|
|
||||||
})?;
|
|
||||||
|
|
||||||
let range = {
|
|
||||||
let Some(range) = buffer.read_with(cx, |buffer, _cx| {
|
|
||||||
find_text_range(&buffer, &input.context_before_range, &input.text_range, &input.context_after_range)
|
|
||||||
})? else {
|
|
||||||
return Err(anyhow!(
|
|
||||||
"Failed to locate the text specified by context_before_range, text_range, and context_after_range. Make sure context_before_range and context_after_range each match exactly once in the file."
|
|
||||||
));
|
|
||||||
};
|
|
||||||
|
|
||||||
range
|
|
||||||
};
|
|
||||||
|
|
||||||
if let Some(action_type) = &input.action {
|
|
||||||
// Special-case the `rename` operation
|
|
||||||
let response = if action_type == "textDocument/rename" {
|
|
||||||
let Some(new_name) = input.arguments.and_then(|args| serde_json::from_value::<String>(args).ok()) else {
|
|
||||||
return Err(anyhow!("For rename operations, 'arguments' must be a string containing the new name"));
|
|
||||||
};
|
|
||||||
|
|
||||||
let position = buffer.read_with(cx, |buffer, _| {
|
|
||||||
range.start.to_point_utf16(&buffer.snapshot())
|
|
||||||
})?;
|
|
||||||
|
|
||||||
project
|
|
||||||
.update(cx, |project, cx| {
|
|
||||||
project.perform_rename(buffer.clone(), position, new_name.clone(), cx)
|
|
||||||
})?
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
format!("Renamed '{}' to '{}'", input.text_range, new_name)
|
|
||||||
} else {
|
|
||||||
// Get code actions for the range
|
|
||||||
let actions = project
|
|
||||||
.update(cx, |project, cx| {
|
|
||||||
project.code_actions(&buffer, range.clone(), None, cx)
|
|
||||||
})?
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
if actions.is_empty() {
|
|
||||||
return Err(anyhow!("No code actions available for this range"));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Find all matching actions
|
|
||||||
let regex = match Regex::new(action_type) {
|
|
||||||
Ok(regex) => regex,
|
|
||||||
Err(err) => return Err(anyhow!("Invalid regex pattern: {}", err)),
|
|
||||||
};
|
|
||||||
let mut matching_actions = actions
|
|
||||||
.into_iter()
|
|
||||||
.filter(|action| { regex.is_match(action.lsp_action.title()) });
|
|
||||||
|
|
||||||
let Some(action) = matching_actions.next() else {
|
|
||||||
return Err(anyhow!("No code actions match the pattern: {}", action_type));
|
|
||||||
};
|
|
||||||
|
|
||||||
// There should have been exactly one matching action.
|
|
||||||
if let Some(second) = matching_actions.next() {
|
|
||||||
let mut all_matches = vec![action, second];
|
|
||||||
|
|
||||||
all_matches.extend(matching_actions);
|
|
||||||
|
|
||||||
return Err(anyhow!(
|
|
||||||
"Pattern '{}' matches multiple code actions: {}",
|
|
||||||
action_type,
|
|
||||||
all_matches.into_iter().map(|action| action.lsp_action.title().to_string()).collect::<Vec<_>>().join(", ")
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
let title = action.lsp_action.title().to_string();
|
|
||||||
|
|
||||||
project
|
|
||||||
.update(cx, |project, cx| {
|
|
||||||
project.apply_code_action(buffer.clone(), action, true, cx)
|
|
||||||
})?
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
format!("Completed code action: {}", title)
|
|
||||||
};
|
|
||||||
|
|
||||||
project
|
|
||||||
.update(cx, |project, cx| project.save_buffer(buffer.clone(), cx))?
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
action_log.update(cx, |log, cx| {
|
|
||||||
log.buffer_edited(buffer.clone(), cx)
|
|
||||||
})?;
|
|
||||||
|
|
||||||
Ok(response)
|
|
||||||
} else {
|
|
||||||
// No action specified, so list the available ones.
|
|
||||||
let (position_start, position_end) = buffer.read_with(cx, |buffer, _| {
|
|
||||||
let snapshot = buffer.snapshot();
|
|
||||||
(
|
|
||||||
range.start.to_point_utf16(&snapshot),
|
|
||||||
range.end.to_point_utf16(&snapshot)
|
|
||||||
)
|
|
||||||
})?;
|
|
||||||
|
|
||||||
// Convert position to display coordinates (1-based)
|
|
||||||
let position_start_display = language::Point {
|
|
||||||
row: position_start.row + 1,
|
|
||||||
column: position_start.column + 1,
|
|
||||||
};
|
|
||||||
|
|
||||||
let position_end_display = language::Point {
|
|
||||||
row: position_end.row + 1,
|
|
||||||
column: position_end.column + 1,
|
|
||||||
};
|
|
||||||
|
|
||||||
// Get code actions for the range
|
|
||||||
let actions = project
|
|
||||||
.update(cx, |project, cx| {
|
|
||||||
project.code_actions(&buffer, range.clone(), None, cx)
|
|
||||||
})?
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
let mut response = format!(
|
|
||||||
"Available code actions for text range '{}' at position {}:{} to {}:{} (UTF-16 coordinates):\n\n",
|
|
||||||
input.text_range,
|
|
||||||
position_start_display.row, position_start_display.column,
|
|
||||||
position_end_display.row, position_end_display.column
|
|
||||||
);
|
|
||||||
|
|
||||||
if actions.is_empty() {
|
|
||||||
response.push_str("No code actions available for this range.");
|
|
||||||
} else {
|
|
||||||
for (i, action) in actions.iter().enumerate() {
|
|
||||||
let title = match &action.lsp_action {
|
|
||||||
LspAction::Action(code_action) => code_action.title.as_str(),
|
|
||||||
LspAction::Command(command) => command.title.as_str(),
|
|
||||||
LspAction::CodeLens(code_lens) => {
|
|
||||||
if let Some(cmd) = &code_lens.command {
|
|
||||||
cmd.title.as_str()
|
|
||||||
} else {
|
|
||||||
"Unknown code lens"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
let kind = match &action.lsp_action {
|
|
||||||
LspAction::Action(code_action) => {
|
|
||||||
if let Some(kind) = &code_action.kind {
|
|
||||||
kind.as_str()
|
|
||||||
} else {
|
|
||||||
"unknown"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
LspAction::Command(_) => "command",
|
|
||||||
LspAction::CodeLens(_) => "code_lens",
|
|
||||||
};
|
|
||||||
|
|
||||||
response.push_str(&format!("{}. {title} ({kind})\n", i + 1));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(response)
|
|
||||||
}
|
|
||||||
}).into()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Finds the range of the text in the buffer, if it appears between context_before_range
|
|
||||||
/// and context_after_range, and if that combined string has one unique result in the buffer.
|
|
||||||
///
|
|
||||||
/// If an exact match fails, it tries adding a newline to the end of context_before_range and
|
|
||||||
/// to the beginning of context_after_range to accommodate line-based context matching.
|
|
||||||
fn find_text_range(
|
|
||||||
buffer: &Buffer,
|
|
||||||
context_before_range: &str,
|
|
||||||
text_range: &str,
|
|
||||||
context_after_range: &str,
|
|
||||||
) -> Option<Range<Anchor>> {
|
|
||||||
let snapshot = buffer.snapshot();
|
|
||||||
let text = snapshot.text();
|
|
||||||
|
|
||||||
// First try with exact match
|
|
||||||
let search_string = format!("{context_before_range}{text_range}{context_after_range}");
|
|
||||||
let mut positions = text.match_indices(&search_string);
|
|
||||||
let position_result = positions.next();
|
|
||||||
|
|
||||||
if let Some(position) = position_result {
|
|
||||||
// Check if the matched string is unique
|
|
||||||
if positions.next().is_none() {
|
|
||||||
let range_start = position.0 + context_before_range.len();
|
|
||||||
let range_end = range_start + text_range.len();
|
|
||||||
let range_start_anchor = snapshot.anchor_before(snapshot.offset_to_point(range_start));
|
|
||||||
let range_end_anchor = snapshot.anchor_before(snapshot.offset_to_point(range_end));
|
|
||||||
|
|
||||||
return Some(range_start_anchor..range_end_anchor);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// If exact match fails or is not unique, try with line-based context
|
|
||||||
// Add a newline to the end of before context and beginning of after context
|
|
||||||
let line_based_before = if context_before_range.ends_with('\n') {
|
|
||||||
context_before_range.to_string()
|
|
||||||
} else {
|
|
||||||
format!("{context_before_range}\n")
|
|
||||||
};
|
|
||||||
|
|
||||||
let line_based_after = if context_after_range.starts_with('\n') {
|
|
||||||
context_after_range.to_string()
|
|
||||||
} else {
|
|
||||||
format!("\n{context_after_range}")
|
|
||||||
};
|
|
||||||
|
|
||||||
let line_search_string = format!("{line_based_before}{text_range}{line_based_after}");
|
|
||||||
let mut line_positions = text.match_indices(&line_search_string);
|
|
||||||
let line_position = line_positions.next()?;
|
|
||||||
|
|
||||||
// The line-based search string must also appear exactly once
|
|
||||||
if line_positions.next().is_some() {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
|
|
||||||
let line_range_start = line_position.0 + line_based_before.len();
|
|
||||||
let line_range_end = line_range_start + text_range.len();
|
|
||||||
let line_range_start_anchor =
|
|
||||||
snapshot.anchor_before(snapshot.offset_to_point(line_range_start));
|
|
||||||
let line_range_end_anchor = snapshot.anchor_before(snapshot.offset_to_point(line_range_end));
|
|
||||||
|
|
||||||
Some(line_range_start_anchor..line_range_end_anchor)
|
|
||||||
}
|
|
|
@ -1,247 +0,0 @@
|
||||||
use std::fmt::Write;
|
|
||||||
use std::path::PathBuf;
|
|
||||||
use std::sync::Arc;
|
|
||||||
|
|
||||||
use crate::schema::json_schema_for;
|
|
||||||
use anyhow::{Result, anyhow};
|
|
||||||
use assistant_tool::outline;
|
|
||||||
use assistant_tool::{ActionLog, Tool, ToolResult};
|
|
||||||
use collections::IndexMap;
|
|
||||||
use gpui::{AnyWindowHandle, App, AsyncApp, Entity, Task};
|
|
||||||
use language_model::{LanguageModelRequestMessage, LanguageModelToolSchemaFormat};
|
|
||||||
use project::{Project, Symbol};
|
|
||||||
use regex::{Regex, RegexBuilder};
|
|
||||||
use schemars::JsonSchema;
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
use ui::IconName;
|
|
||||||
use util::markdown::MarkdownInlineCode;
|
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
|
|
||||||
pub struct CodeSymbolsInput {
|
|
||||||
/// The relative path of the source code file to read and get the symbols for.
|
|
||||||
/// This tool should only be used on source code files, never on any other type of file.
|
|
||||||
///
|
|
||||||
/// This path should never be absolute, and the first component
|
|
||||||
/// of the path should always be a root directory in a project.
|
|
||||||
///
|
|
||||||
/// If no path is specified, this tool returns a flat list of all symbols in the project
|
|
||||||
/// instead of a hierarchical outline of a specific file.
|
|
||||||
///
|
|
||||||
/// <example>
|
|
||||||
/// If the project has the following root directories:
|
|
||||||
///
|
|
||||||
/// - directory1
|
|
||||||
/// - directory2
|
|
||||||
///
|
|
||||||
/// If you want to access `file.md` in `directory1`, you should use the path `directory1/file.md`.
|
|
||||||
/// If you want to access `file.md` in `directory2`, you should use the path `directory2/file.md`.
|
|
||||||
/// </example>
|
|
||||||
#[serde(default)]
|
|
||||||
pub path: Option<String>,
|
|
||||||
|
|
||||||
/// Optional regex pattern to filter symbols by name.
|
|
||||||
/// When provided, only symbols whose names match this pattern will be included in the results.
|
|
||||||
///
|
|
||||||
/// <example>
|
|
||||||
/// To find only symbols that contain the word "test", use the regex pattern "test".
|
|
||||||
/// To find methods that start with "get_", use the regex pattern "^get_".
|
|
||||||
/// </example>
|
|
||||||
#[serde(default)]
|
|
||||||
pub regex: Option<String>,
|
|
||||||
|
|
||||||
/// Whether the regex is case-sensitive. Defaults to false (case-insensitive).
|
|
||||||
///
|
|
||||||
/// <example>
|
|
||||||
/// Set to `true` to make regex matching case-sensitive.
|
|
||||||
/// </example>
|
|
||||||
#[serde(default)]
|
|
||||||
pub case_sensitive: bool,
|
|
||||||
|
|
||||||
/// Optional starting position for paginated results (0-based).
|
|
||||||
/// When not provided, starts from the beginning.
|
|
||||||
#[serde(default)]
|
|
||||||
pub offset: u32,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl CodeSymbolsInput {
|
|
||||||
/// Which page of search results this is.
|
|
||||||
pub fn page(&self) -> u32 {
|
|
||||||
1 + (self.offset / RESULTS_PER_PAGE)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const RESULTS_PER_PAGE: u32 = 2000;
|
|
||||||
|
|
||||||
pub struct CodeSymbolsTool;
|
|
||||||
|
|
||||||
impl Tool for CodeSymbolsTool {
|
|
||||||
fn name(&self) -> String {
|
|
||||||
"code_symbols".into()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn needs_confirmation(&self, _: &serde_json::Value, _: &App) -> bool {
|
|
||||||
false
|
|
||||||
}
|
|
||||||
|
|
||||||
fn description(&self) -> String {
|
|
||||||
include_str!("./code_symbols_tool/description.md").into()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn icon(&self) -> IconName {
|
|
||||||
IconName::Code
|
|
||||||
}
|
|
||||||
|
|
||||||
fn input_schema(&self, format: LanguageModelToolSchemaFormat) -> Result<serde_json::Value> {
|
|
||||||
json_schema_for::<CodeSymbolsInput>(format)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn ui_text(&self, input: &serde_json::Value) -> String {
|
|
||||||
match serde_json::from_value::<CodeSymbolsInput>(input.clone()) {
|
|
||||||
Ok(input) => {
|
|
||||||
let page = input.page();
|
|
||||||
|
|
||||||
match &input.path {
|
|
||||||
Some(path) => {
|
|
||||||
let path = MarkdownInlineCode(path);
|
|
||||||
if page > 1 {
|
|
||||||
format!("List page {page} of code symbols for {path}")
|
|
||||||
} else {
|
|
||||||
format!("List code symbols for {path}")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
None => {
|
|
||||||
if page > 1 {
|
|
||||||
format!("List page {page} of project symbols")
|
|
||||||
} else {
|
|
||||||
"List all project symbols".to_string()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Err(_) => "List code symbols".to_string(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn run(
|
|
||||||
self: Arc<Self>,
|
|
||||||
input: serde_json::Value,
|
|
||||||
_messages: &[LanguageModelRequestMessage],
|
|
||||||
project: Entity<Project>,
|
|
||||||
action_log: Entity<ActionLog>,
|
|
||||||
_window: Option<AnyWindowHandle>,
|
|
||||||
cx: &mut App,
|
|
||||||
) -> ToolResult {
|
|
||||||
let input = match serde_json::from_value::<CodeSymbolsInput>(input) {
|
|
||||||
Ok(input) => input,
|
|
||||||
Err(err) => return Task::ready(Err(anyhow!(err))).into(),
|
|
||||||
};
|
|
||||||
|
|
||||||
let regex = match input.regex {
|
|
||||||
Some(regex_str) => match RegexBuilder::new(®ex_str)
|
|
||||||
.case_insensitive(!input.case_sensitive)
|
|
||||||
.build()
|
|
||||||
{
|
|
||||||
Ok(regex) => Some(regex),
|
|
||||||
Err(err) => return Task::ready(Err(anyhow!("Invalid regex: {err}"))).into(),
|
|
||||||
},
|
|
||||||
None => None,
|
|
||||||
};
|
|
||||||
|
|
||||||
cx.spawn(async move |cx| match input.path {
|
|
||||||
Some(path) => outline::file_outline(project, path, action_log, regex, cx).await,
|
|
||||||
None => project_symbols(project, regex, input.offset, cx).await,
|
|
||||||
})
|
|
||||||
.into()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn project_symbols(
|
|
||||||
project: Entity<Project>,
|
|
||||||
regex: Option<Regex>,
|
|
||||||
offset: u32,
|
|
||||||
cx: &mut AsyncApp,
|
|
||||||
) -> anyhow::Result<String> {
|
|
||||||
let symbols = project
|
|
||||||
.update(cx, |project, cx| project.symbols("", cx))?
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
if symbols.is_empty() {
|
|
||||||
return Err(anyhow!("No symbols found in project."));
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut symbols_by_path: IndexMap<PathBuf, Vec<&Symbol>> = IndexMap::default();
|
|
||||||
|
|
||||||
for symbol in symbols
|
|
||||||
.iter()
|
|
||||||
.filter(|symbol| {
|
|
||||||
if let Some(regex) = ®ex {
|
|
||||||
regex.is_match(&symbol.name)
|
|
||||||
} else {
|
|
||||||
true
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.skip(offset as usize)
|
|
||||||
// Take 1 more than RESULTS_PER_PAGE so we can tell if there are more results.
|
|
||||||
.take((RESULTS_PER_PAGE as usize).saturating_add(1))
|
|
||||||
{
|
|
||||||
if let Some(worktree_path) = project.read_with(cx, |project, cx| {
|
|
||||||
project
|
|
||||||
.worktree_for_id(symbol.path.worktree_id, cx)
|
|
||||||
.map(|worktree| PathBuf::from(worktree.read(cx).root_name()))
|
|
||||||
})? {
|
|
||||||
let path = worktree_path.join(&symbol.path.path);
|
|
||||||
symbols_by_path.entry(path).or_default().push(symbol);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// If no symbols matched the filter, return early
|
|
||||||
if symbols_by_path.is_empty() {
|
|
||||||
return Err(anyhow!("No symbols found matching the criteria."));
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut symbols_rendered = 0;
|
|
||||||
let mut has_more_symbols = false;
|
|
||||||
let mut output = String::new();
|
|
||||||
|
|
||||||
'outer: for (file_path, file_symbols) in symbols_by_path {
|
|
||||||
if symbols_rendered > 0 {
|
|
||||||
output.push('\n');
|
|
||||||
}
|
|
||||||
|
|
||||||
writeln!(&mut output, "{}", file_path.display()).ok();
|
|
||||||
|
|
||||||
for symbol in file_symbols {
|
|
||||||
if symbols_rendered >= RESULTS_PER_PAGE {
|
|
||||||
has_more_symbols = true;
|
|
||||||
break 'outer;
|
|
||||||
}
|
|
||||||
|
|
||||||
write!(&mut output, " {} ", symbol.label.text()).ok();
|
|
||||||
|
|
||||||
// Convert to 1-based line numbers for display
|
|
||||||
let start_line = symbol.range.start.0.row as usize + 1;
|
|
||||||
let end_line = symbol.range.end.0.row as usize + 1;
|
|
||||||
|
|
||||||
if start_line == end_line {
|
|
||||||
writeln!(&mut output, "[L{}]", start_line).ok();
|
|
||||||
} else {
|
|
||||||
writeln!(&mut output, "[L{}-{}]", start_line, end_line).ok();
|
|
||||||
}
|
|
||||||
|
|
||||||
symbols_rendered += 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(if symbols_rendered == 0 {
|
|
||||||
"No symbols found in the requested page.".to_string()
|
|
||||||
} else if has_more_symbols {
|
|
||||||
format!(
|
|
||||||
"{output}\nShowing symbols {}-{} (more symbols were found; use offset: {} to see next page)",
|
|
||||||
offset + 1,
|
|
||||||
offset + symbols_rendered,
|
|
||||||
offset + RESULTS_PER_PAGE,
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
output
|
|
||||||
})
|
|
||||||
}
|
|
|
@ -1,236 +0,0 @@
|
||||||
use std::sync::Arc;
|
|
||||||
|
|
||||||
use crate::schema::json_schema_for;
|
|
||||||
use anyhow::{Result, anyhow};
|
|
||||||
use assistant_tool::{ActionLog, Tool, ToolResult, outline};
|
|
||||||
use gpui::{AnyWindowHandle, App, Entity, Task};
|
|
||||||
use itertools::Itertools;
|
|
||||||
use language_model::{LanguageModelRequestMessage, LanguageModelToolSchemaFormat};
|
|
||||||
use project::Project;
|
|
||||||
use schemars::JsonSchema;
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
use std::{fmt::Write, path::Path};
|
|
||||||
use ui::IconName;
|
|
||||||
use util::markdown::MarkdownInlineCode;
|
|
||||||
|
|
||||||
/// If the model requests to read a file whose size exceeds this, then
|
|
||||||
/// If the model requests to list the entries in a directory with more
|
|
||||||
/// entries than this, then the tool will return a subset of the entries
|
|
||||||
/// and suggest trying again.
|
|
||||||
const MAX_DIR_ENTRIES: usize = 1024;
|
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
|
|
||||||
pub struct ContentsToolInput {
|
|
||||||
/// The relative path of the file or directory to access.
|
|
||||||
///
|
|
||||||
/// This path should never be absolute, and the first component
|
|
||||||
/// of the path should always be a root directory in a project.
|
|
||||||
///
|
|
||||||
/// <example>
|
|
||||||
/// If the project has the following root directories:
|
|
||||||
///
|
|
||||||
/// - directory1
|
|
||||||
/// - directory2
|
|
||||||
///
|
|
||||||
/// If you want to access `file.txt` in `directory1`, you should use the path `directory1/file.txt`.
|
|
||||||
/// If you want to list contents in the directory `directory2/subfolder`, you should use the path `directory2/subfolder`.
|
|
||||||
/// </example>
|
|
||||||
pub path: String,
|
|
||||||
|
|
||||||
/// Optional position (1-based index) to start reading on, if you want to read a subset of the contents.
|
|
||||||
/// When reading a file, this refers to a line number in the file (e.g. 1 is the first line).
|
|
||||||
/// When reading a directory, this refers to the number of the directory entry (e.g. 1 is the first entry).
|
|
||||||
///
|
|
||||||
/// Defaults to 1.
|
|
||||||
pub start: Option<u32>,
|
|
||||||
|
|
||||||
/// Optional position (1-based index) to end reading on, if you want to read a subset of the contents.
|
|
||||||
/// When reading a file, this refers to a line number in the file (e.g. 1 is the first line).
|
|
||||||
/// When reading a directory, this refers to the number of the directory entry (e.g. 1 is the first entry).
|
|
||||||
///
|
|
||||||
/// Defaults to reading until the end of the file or directory.
|
|
||||||
pub end: Option<u32>,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct ContentsTool;
|
|
||||||
|
|
||||||
impl Tool for ContentsTool {
|
|
||||||
fn name(&self) -> String {
|
|
||||||
"contents".into()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn needs_confirmation(&self, _: &serde_json::Value, _: &App) -> bool {
|
|
||||||
false
|
|
||||||
}
|
|
||||||
|
|
||||||
fn description(&self) -> String {
|
|
||||||
include_str!("./contents_tool/description.md").into()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn icon(&self) -> IconName {
|
|
||||||
IconName::FileSearch
|
|
||||||
}
|
|
||||||
|
|
||||||
fn input_schema(&self, format: LanguageModelToolSchemaFormat) -> Result<serde_json::Value> {
|
|
||||||
json_schema_for::<ContentsToolInput>(format)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn ui_text(&self, input: &serde_json::Value) -> String {
|
|
||||||
match serde_json::from_value::<ContentsToolInput>(input.clone()) {
|
|
||||||
Ok(input) => {
|
|
||||||
let path = MarkdownInlineCode(&input.path);
|
|
||||||
|
|
||||||
match (input.start, input.end) {
|
|
||||||
(Some(start), None) => format!("Read {path} (from line {start})"),
|
|
||||||
(Some(start), Some(end)) => {
|
|
||||||
format!("Read {path} (lines {start}-{end})")
|
|
||||||
}
|
|
||||||
_ => format!("Read {path}"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Err(_) => "Read file or directory".to_string(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn run(
|
|
||||||
self: Arc<Self>,
|
|
||||||
input: serde_json::Value,
|
|
||||||
_messages: &[LanguageModelRequestMessage],
|
|
||||||
project: Entity<Project>,
|
|
||||||
action_log: Entity<ActionLog>,
|
|
||||||
_window: Option<AnyWindowHandle>,
|
|
||||||
cx: &mut App,
|
|
||||||
) -> ToolResult {
|
|
||||||
let input = match serde_json::from_value::<ContentsToolInput>(input) {
|
|
||||||
Ok(input) => input,
|
|
||||||
Err(err) => return Task::ready(Err(anyhow!(err))).into(),
|
|
||||||
};
|
|
||||||
|
|
||||||
// Sometimes models will return these even though we tell it to give a path and not a glob.
|
|
||||||
// When this happens, just list the root worktree directories.
|
|
||||||
if matches!(input.path.as_str(), "." | "" | "./" | "*") {
|
|
||||||
let output = project
|
|
||||||
.read(cx)
|
|
||||||
.worktrees(cx)
|
|
||||||
.filter_map(|worktree| {
|
|
||||||
worktree.read(cx).root_entry().and_then(|entry| {
|
|
||||||
if entry.is_dir() {
|
|
||||||
entry.path.to_str()
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
.collect::<Vec<_>>()
|
|
||||||
.join("\n");
|
|
||||||
|
|
||||||
return Task::ready(Ok(output)).into();
|
|
||||||
}
|
|
||||||
|
|
||||||
let Some(project_path) = project.read(cx).find_project_path(&input.path, cx) else {
|
|
||||||
return Task::ready(Err(anyhow!("Path {} not found in project", &input.path))).into();
|
|
||||||
};
|
|
||||||
|
|
||||||
let Some(worktree) = project
|
|
||||||
.read(cx)
|
|
||||||
.worktree_for_id(project_path.worktree_id, cx)
|
|
||||||
else {
|
|
||||||
return Task::ready(Err(anyhow!("Worktree not found"))).into();
|
|
||||||
};
|
|
||||||
let worktree = worktree.read(cx);
|
|
||||||
|
|
||||||
let Some(entry) = worktree.entry_for_path(&project_path.path) else {
|
|
||||||
return Task::ready(Err(anyhow!("Path not found: {}", input.path))).into();
|
|
||||||
};
|
|
||||||
|
|
||||||
// If it's a directory, list its contents
|
|
||||||
if entry.is_dir() {
|
|
||||||
let mut output = String::new();
|
|
||||||
let start_index = input
|
|
||||||
.start
|
|
||||||
.map(|line| (line as usize).saturating_sub(1))
|
|
||||||
.unwrap_or(0);
|
|
||||||
let end_index = input
|
|
||||||
.end
|
|
||||||
.map(|line| (line as usize).saturating_sub(1))
|
|
||||||
.unwrap_or(MAX_DIR_ENTRIES);
|
|
||||||
let mut skipped = 0;
|
|
||||||
|
|
||||||
for (index, entry) in worktree.child_entries(&project_path.path).enumerate() {
|
|
||||||
if index >= start_index && index <= end_index {
|
|
||||||
writeln!(
|
|
||||||
output,
|
|
||||||
"{}",
|
|
||||||
Path::new(worktree.root_name()).join(&entry.path).display(),
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
} else {
|
|
||||||
skipped += 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if output.is_empty() {
|
|
||||||
output.push_str(&input.path);
|
|
||||||
output.push_str(" is empty.");
|
|
||||||
}
|
|
||||||
|
|
||||||
if skipped > 0 {
|
|
||||||
write!(
|
|
||||||
output,
|
|
||||||
"\n\nNote: Skipped {skipped} entries. Adjust start and end to see other entries.",
|
|
||||||
).ok();
|
|
||||||
}
|
|
||||||
|
|
||||||
Task::ready(Ok(output)).into()
|
|
||||||
} else {
|
|
||||||
// It's a file, so read its contents
|
|
||||||
let file_path = input.path.clone();
|
|
||||||
cx.spawn(async move |cx| {
|
|
||||||
let buffer = cx
|
|
||||||
.update(|cx| {
|
|
||||||
project.update(cx, |project, cx| project.open_buffer(project_path, cx))
|
|
||||||
})?
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
if input.start.is_some() || input.end.is_some() {
|
|
||||||
let result = buffer.read_with(cx, |buffer, _cx| {
|
|
||||||
let text = buffer.text();
|
|
||||||
let start = input.start.unwrap_or(1);
|
|
||||||
let lines = text.split('\n').skip(start as usize - 1);
|
|
||||||
if let Some(end) = input.end {
|
|
||||||
let count = end.saturating_sub(start).max(1); // Ensure at least 1 line
|
|
||||||
Itertools::intersperse(lines.take(count as usize), "\n").collect()
|
|
||||||
} else {
|
|
||||||
Itertools::intersperse(lines, "\n").collect()
|
|
||||||
}
|
|
||||||
})?;
|
|
||||||
|
|
||||||
action_log.update(cx, |log, cx| {
|
|
||||||
log.track_buffer(buffer, cx);
|
|
||||||
})?;
|
|
||||||
|
|
||||||
Ok(result)
|
|
||||||
} else {
|
|
||||||
// No line ranges specified, so check file size to see if it's too big.
|
|
||||||
let file_size = buffer.read_with(cx, |buffer, _cx| buffer.text().len())?;
|
|
||||||
|
|
||||||
if file_size <= outline::AUTO_OUTLINE_SIZE {
|
|
||||||
let result = buffer.read_with(cx, |buffer, _cx| buffer.text())?;
|
|
||||||
|
|
||||||
action_log.update(cx, |log, cx| {
|
|
||||||
log.track_buffer(buffer, cx);
|
|
||||||
})?;
|
|
||||||
|
|
||||||
Ok(result)
|
|
||||||
} else {
|
|
||||||
// File is too big, so return its outline and a suggestion to
|
|
||||||
// read again with a line number range specified.
|
|
||||||
let outline = outline::file_outline(project, file_path, action_log, None, cx).await?;
|
|
||||||
|
|
||||||
Ok(format!("This file was too big to read all at once. Here is an outline of its symbols:\n\n{outline}\n\nUsing the line numbers in this outline, you can call this tool again while specifying the start and end fields to see the implementations of symbols in the outline."))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}).into()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,204 +0,0 @@
|
||||||
use anyhow::{Context as _, Result, anyhow};
|
|
||||||
use assistant_tool::{ActionLog, Tool, ToolResult};
|
|
||||||
use gpui::{AnyWindowHandle, App, Entity, Task};
|
|
||||||
use language::{self, Buffer, ToPointUtf16};
|
|
||||||
use language_model::{LanguageModelRequestMessage, LanguageModelToolSchemaFormat};
|
|
||||||
use project::Project;
|
|
||||||
use schemars::JsonSchema;
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
use std::sync::Arc;
|
|
||||||
use ui::IconName;
|
|
||||||
|
|
||||||
use crate::schema::json_schema_for;
|
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
|
|
||||||
pub struct RenameToolInput {
|
|
||||||
/// The relative path to the file containing the symbol to rename.
|
|
||||||
///
|
|
||||||
/// WARNING: you MUST start this path with one of the project's root directories.
|
|
||||||
pub path: String,
|
|
||||||
|
|
||||||
/// The new name to give to the symbol.
|
|
||||||
pub new_name: String,
|
|
||||||
|
|
||||||
/// The text that comes immediately before the symbol in the file.
|
|
||||||
pub context_before_symbol: String,
|
|
||||||
|
|
||||||
/// The symbol to rename. This text must appear in the file right between
|
|
||||||
/// `context_before_symbol` and `context_after_symbol`.
|
|
||||||
///
|
|
||||||
/// The file must contain exactly one occurrence of `context_before_symbol` followed by
|
|
||||||
/// `symbol` followed by `context_after_symbol`. If the file contains zero occurrences,
|
|
||||||
/// or if it contains more than one occurrence, the tool will fail, so it is absolutely
|
|
||||||
/// critical that you verify ahead of time that the string is unique. You can search
|
|
||||||
/// the file's contents to verify this ahead of time.
|
|
||||||
///
|
|
||||||
/// To make the string more likely to be unique, include a minimum of 1 line of context
|
|
||||||
/// before the symbol, as well as a minimum of 1 line of context after the symbol.
|
|
||||||
/// If these lines of context are not enough to obtain a string that appears only once
|
|
||||||
/// in the file, then double the number of context lines until the string becomes unique.
|
|
||||||
/// (Start with 1 line before and 1 line after though, because too much context is
|
|
||||||
/// needlessly costly.)
|
|
||||||
///
|
|
||||||
/// Do not alter the context lines of code in any way, and make sure to preserve all
|
|
||||||
/// whitespace and indentation for all lines of code. The combined string must be exactly
|
|
||||||
/// as it appears in the file, or else this tool call will fail.
|
|
||||||
pub symbol: String,
|
|
||||||
|
|
||||||
/// The text that comes immediately after the symbol in the file.
|
|
||||||
pub context_after_symbol: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct RenameTool;
|
|
||||||
|
|
||||||
impl Tool for RenameTool {
|
|
||||||
fn name(&self) -> String {
|
|
||||||
"rename".into()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn needs_confirmation(&self, _input: &serde_json::Value, _cx: &App) -> bool {
|
|
||||||
false
|
|
||||||
}
|
|
||||||
|
|
||||||
fn description(&self) -> String {
|
|
||||||
include_str!("./rename_tool/description.md").into()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn icon(&self) -> IconName {
|
|
||||||
IconName::Pencil
|
|
||||||
}
|
|
||||||
|
|
||||||
fn input_schema(&self, format: LanguageModelToolSchemaFormat) -> Result<serde_json::Value> {
|
|
||||||
json_schema_for::<RenameToolInput>(format)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn ui_text(&self, input: &serde_json::Value) -> String {
|
|
||||||
match serde_json::from_value::<RenameToolInput>(input.clone()) {
|
|
||||||
Ok(input) => {
|
|
||||||
format!("Rename '{}' to '{}'", input.symbol, input.new_name)
|
|
||||||
}
|
|
||||||
Err(_) => "Rename symbol".to_string(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn run(
|
|
||||||
self: Arc<Self>,
|
|
||||||
input: serde_json::Value,
|
|
||||||
_messages: &[LanguageModelRequestMessage],
|
|
||||||
project: Entity<Project>,
|
|
||||||
action_log: Entity<ActionLog>,
|
|
||||||
_window: Option<AnyWindowHandle>,
|
|
||||||
cx: &mut App,
|
|
||||||
) -> ToolResult {
|
|
||||||
let input = match serde_json::from_value::<RenameToolInput>(input) {
|
|
||||||
Ok(input) => input,
|
|
||||||
Err(err) => return Task::ready(Err(anyhow!(err))).into(),
|
|
||||||
};
|
|
||||||
|
|
||||||
cx.spawn(async move |cx| {
|
|
||||||
let buffer = {
|
|
||||||
let project_path = project.read_with(cx, |project, cx| {
|
|
||||||
project
|
|
||||||
.find_project_path(&input.path, cx)
|
|
||||||
.context("Path not found in project")
|
|
||||||
})??;
|
|
||||||
|
|
||||||
project.update(cx, |project, cx| project.open_buffer(project_path, cx))?.await?
|
|
||||||
};
|
|
||||||
|
|
||||||
action_log.update(cx, |action_log, cx| {
|
|
||||||
action_log.track_buffer(buffer.clone(), cx);
|
|
||||||
})?;
|
|
||||||
|
|
||||||
let position = {
|
|
||||||
let Some(position) = buffer.read_with(cx, |buffer, _cx| {
|
|
||||||
find_symbol_position(&buffer, &input.context_before_symbol, &input.symbol, &input.context_after_symbol)
|
|
||||||
})? else {
|
|
||||||
return Err(anyhow!(
|
|
||||||
"Failed to locate the symbol specified by context_before_symbol, symbol, and context_after_symbol. Make sure context_before_symbol and context_after_symbol each match exactly once in the file."
|
|
||||||
));
|
|
||||||
};
|
|
||||||
|
|
||||||
buffer.read_with(cx, |buffer, _| {
|
|
||||||
position.to_point_utf16(&buffer.snapshot())
|
|
||||||
})?
|
|
||||||
};
|
|
||||||
|
|
||||||
project
|
|
||||||
.update(cx, |project, cx| {
|
|
||||||
project.perform_rename(buffer.clone(), position, input.new_name.clone(), cx)
|
|
||||||
})?
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
project
|
|
||||||
.update(cx, |project, cx| project.save_buffer(buffer.clone(), cx))?
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
action_log.update(cx, |log, cx| {
|
|
||||||
log.buffer_edited(buffer.clone(), cx)
|
|
||||||
})?;
|
|
||||||
|
|
||||||
Ok(format!("Renamed '{}' to '{}'", input.symbol, input.new_name))
|
|
||||||
}).into()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Finds the position of the symbol in the buffer, if it appears between context_before_symbol
|
|
||||||
/// and context_after_symbol, and if that combined string has one unique result in the buffer.
|
|
||||||
///
|
|
||||||
/// If an exact match fails, it tries adding a newline to the end of context_before_symbol and
|
|
||||||
/// to the beginning of context_after_symbol to accommodate line-based context matching.
|
|
||||||
fn find_symbol_position(
|
|
||||||
buffer: &Buffer,
|
|
||||||
context_before_symbol: &str,
|
|
||||||
symbol: &str,
|
|
||||||
context_after_symbol: &str,
|
|
||||||
) -> Option<language::Anchor> {
|
|
||||||
let snapshot = buffer.snapshot();
|
|
||||||
let text = snapshot.text();
|
|
||||||
|
|
||||||
// First try with exact match
|
|
||||||
let search_string = format!("{context_before_symbol}{symbol}{context_after_symbol}");
|
|
||||||
let mut positions = text.match_indices(&search_string);
|
|
||||||
let position_result = positions.next();
|
|
||||||
|
|
||||||
if let Some(position) = position_result {
|
|
||||||
// Check if the matched string is unique
|
|
||||||
if positions.next().is_none() {
|
|
||||||
let symbol_start = position.0 + context_before_symbol.len();
|
|
||||||
let symbol_start_anchor =
|
|
||||||
snapshot.anchor_before(snapshot.offset_to_point(symbol_start));
|
|
||||||
|
|
||||||
return Some(symbol_start_anchor);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// If exact match fails or is not unique, try with line-based context
|
|
||||||
// Add a newline to the end of before context and beginning of after context
|
|
||||||
let line_based_before = if context_before_symbol.ends_with('\n') {
|
|
||||||
context_before_symbol.to_string()
|
|
||||||
} else {
|
|
||||||
format!("{context_before_symbol}\n")
|
|
||||||
};
|
|
||||||
|
|
||||||
let line_based_after = if context_after_symbol.starts_with('\n') {
|
|
||||||
context_after_symbol.to_string()
|
|
||||||
} else {
|
|
||||||
format!("\n{context_after_symbol}")
|
|
||||||
};
|
|
||||||
|
|
||||||
let line_search_string = format!("{line_based_before}{symbol}{line_based_after}");
|
|
||||||
let mut line_positions = text.match_indices(&line_search_string);
|
|
||||||
let line_position = line_positions.next()?;
|
|
||||||
|
|
||||||
// The line-based search string must also appear exactly once
|
|
||||||
if line_positions.next().is_some() {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
|
|
||||||
let line_symbol_start = line_position.0 + line_based_before.len();
|
|
||||||
let line_symbol_start_anchor =
|
|
||||||
snapshot.anchor_before(snapshot.offset_to_point(line_symbol_start));
|
|
||||||
|
|
||||||
Some(line_symbol_start_anchor)
|
|
||||||
}
|
|
|
@ -1,307 +0,0 @@
|
||||||
use anyhow::{Context as _, Result, anyhow};
|
|
||||||
use assistant_tool::{ActionLog, Tool, ToolResult};
|
|
||||||
use gpui::{AnyWindowHandle, App, AsyncApp, Entity, Task};
|
|
||||||
use language::{self, Anchor, Buffer, BufferSnapshot, Location, Point, ToPoint, ToPointUtf16};
|
|
||||||
use language_model::{LanguageModelRequestMessage, LanguageModelToolSchemaFormat};
|
|
||||||
use project::Project;
|
|
||||||
use schemars::JsonSchema;
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
use std::{fmt::Write, ops::Range, sync::Arc};
|
|
||||||
use ui::IconName;
|
|
||||||
use util::markdown::MarkdownInlineCode;
|
|
||||||
|
|
||||||
use crate::schema::json_schema_for;
|
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
|
|
||||||
pub struct SymbolInfoToolInput {
|
|
||||||
/// The relative path to the file containing the symbol.
|
|
||||||
///
|
|
||||||
/// WARNING: you MUST start this path with one of the project's root directories.
|
|
||||||
pub path: String,
|
|
||||||
|
|
||||||
/// The information to get about the symbol.
|
|
||||||
pub command: Info,
|
|
||||||
|
|
||||||
/// The text that comes immediately before the symbol in the file.
|
|
||||||
pub context_before_symbol: String,
|
|
||||||
|
|
||||||
/// The symbol name. This text must appear in the file right between `context_before_symbol`
|
|
||||||
/// and `context_after_symbol`.
|
|
||||||
///
|
|
||||||
/// The file must contain exactly one occurrence of `context_before_symbol` followed by
|
|
||||||
/// `symbol` followed by `context_after_symbol`. If the file contains zero occurrences,
|
|
||||||
/// or if it contains more than one occurrence, the tool will fail, so it is absolutely
|
|
||||||
/// critical that you verify ahead of time that the string is unique. You can search
|
|
||||||
/// the file's contents to verify this ahead of time.
|
|
||||||
///
|
|
||||||
/// To make the string more likely to be unique, include a minimum of 1 line of context
|
|
||||||
/// before the symbol, as well as a minimum of 1 line of context after the symbol.
|
|
||||||
/// If these lines of context are not enough to obtain a string that appears only once
|
|
||||||
/// in the file, then double the number of context lines until the string becomes unique.
|
|
||||||
/// (Start with 1 line before and 1 line after though, because too much context is
|
|
||||||
/// needlessly costly.)
|
|
||||||
///
|
|
||||||
/// Do not alter the context lines of code in any way, and make sure to preserve all
|
|
||||||
/// whitespace and indentation for all lines of code. The combined string must be exactly
|
|
||||||
/// as it appears in the file, or else this tool call will fail.
|
|
||||||
pub symbol: String,
|
|
||||||
|
|
||||||
/// The text that comes immediately after the symbol in the file.
|
|
||||||
pub context_after_symbol: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
|
|
||||||
#[serde(rename_all = "snake_case")]
|
|
||||||
pub enum Info {
|
|
||||||
/// Get the symbol's definition (where it's first assigned, even if it's declared elsewhere)
|
|
||||||
Definition,
|
|
||||||
/// Get the symbol's declaration (where it's first declared)
|
|
||||||
Declaration,
|
|
||||||
/// Get the symbol's implementation
|
|
||||||
Implementation,
|
|
||||||
/// Get the symbol's type definition
|
|
||||||
TypeDefinition,
|
|
||||||
/// Find all references to the symbol in the project
|
|
||||||
References,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct SymbolInfoTool;
|
|
||||||
|
|
||||||
impl Tool for SymbolInfoTool {
|
|
||||||
fn name(&self) -> String {
|
|
||||||
"symbol_info".into()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn needs_confirmation(&self, _: &serde_json::Value, _: &App) -> bool {
|
|
||||||
false
|
|
||||||
}
|
|
||||||
|
|
||||||
fn description(&self) -> String {
|
|
||||||
include_str!("./symbol_info_tool/description.md").into()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn icon(&self) -> IconName {
|
|
||||||
IconName::Code
|
|
||||||
}
|
|
||||||
|
|
||||||
fn input_schema(&self, format: LanguageModelToolSchemaFormat) -> Result<serde_json::Value> {
|
|
||||||
json_schema_for::<SymbolInfoToolInput>(format)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn ui_text(&self, input: &serde_json::Value) -> String {
|
|
||||||
match serde_json::from_value::<SymbolInfoToolInput>(input.clone()) {
|
|
||||||
Ok(input) => {
|
|
||||||
let symbol = MarkdownInlineCode(&input.symbol);
|
|
||||||
|
|
||||||
match input.command {
|
|
||||||
Info::Definition => {
|
|
||||||
format!("Find definition for {symbol}")
|
|
||||||
}
|
|
||||||
Info::Declaration => {
|
|
||||||
format!("Find declaration for {symbol}")
|
|
||||||
}
|
|
||||||
Info::Implementation => {
|
|
||||||
format!("Find implementation for {symbol}")
|
|
||||||
}
|
|
||||||
Info::TypeDefinition => {
|
|
||||||
format!("Find type definition for {symbol}")
|
|
||||||
}
|
|
||||||
Info::References => {
|
|
||||||
format!("Find references for {symbol}")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Err(_) => "Get symbol info".to_string(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn run(
|
|
||||||
self: Arc<Self>,
|
|
||||||
input: serde_json::Value,
|
|
||||||
_messages: &[LanguageModelRequestMessage],
|
|
||||||
project: Entity<Project>,
|
|
||||||
action_log: Entity<ActionLog>,
|
|
||||||
_window: Option<AnyWindowHandle>,
|
|
||||||
cx: &mut App,
|
|
||||||
) -> ToolResult {
|
|
||||||
let input = match serde_json::from_value::<SymbolInfoToolInput>(input) {
|
|
||||||
Ok(input) => input,
|
|
||||||
Err(err) => return Task::ready(Err(anyhow!(err))).into(),
|
|
||||||
};
|
|
||||||
|
|
||||||
cx.spawn(async move |cx| {
|
|
||||||
let buffer = {
|
|
||||||
let project_path = project.read_with(cx, |project, cx| {
|
|
||||||
project
|
|
||||||
.find_project_path(&input.path, cx)
|
|
||||||
.context("Path not found in project")
|
|
||||||
})??;
|
|
||||||
|
|
||||||
project.update(cx, |project, cx| project.open_buffer(project_path, cx))?.await?
|
|
||||||
};
|
|
||||||
|
|
||||||
action_log.update(cx, |action_log, cx| {
|
|
||||||
action_log.track_buffer(buffer.clone(), cx);
|
|
||||||
})?;
|
|
||||||
|
|
||||||
let position = {
|
|
||||||
let Some(range) = buffer.read_with(cx, |buffer, _cx| {
|
|
||||||
find_symbol_range(&buffer, &input.context_before_symbol, &input.symbol, &input.context_after_symbol)
|
|
||||||
})? else {
|
|
||||||
return Err(anyhow!(
|
|
||||||
"Failed to locate the text specified by context_before_symbol, symbol, and context_after_symbol. Make sure context_before_symbol and context_after_symbol each match exactly once in the file."
|
|
||||||
));
|
|
||||||
};
|
|
||||||
|
|
||||||
buffer.read_with(cx, |buffer, _| {
|
|
||||||
range.start.to_point_utf16(&buffer.snapshot())
|
|
||||||
})?
|
|
||||||
};
|
|
||||||
|
|
||||||
let output: String = match input.command {
|
|
||||||
Info::Definition => {
|
|
||||||
render_locations(project
|
|
||||||
.update(cx, |project, cx| {
|
|
||||||
project.definition(&buffer, position, cx)
|
|
||||||
})?
|
|
||||||
.await?.into_iter().map(|link| link.target),
|
|
||||||
cx)
|
|
||||||
}
|
|
||||||
Info::Declaration => {
|
|
||||||
render_locations(project
|
|
||||||
.update(cx, |project, cx| {
|
|
||||||
project.declaration(&buffer, position, cx)
|
|
||||||
})?
|
|
||||||
.await?.into_iter().map(|link| link.target),
|
|
||||||
cx)
|
|
||||||
}
|
|
||||||
Info::Implementation => {
|
|
||||||
render_locations(project
|
|
||||||
.update(cx, |project, cx| {
|
|
||||||
project.implementation(&buffer, position, cx)
|
|
||||||
})?
|
|
||||||
.await?.into_iter().map(|link| link.target),
|
|
||||||
cx)
|
|
||||||
}
|
|
||||||
Info::TypeDefinition => {
|
|
||||||
render_locations(project
|
|
||||||
.update(cx, |project, cx| {
|
|
||||||
project.type_definition(&buffer, position, cx)
|
|
||||||
})?
|
|
||||||
.await?.into_iter().map(|link| link.target),
|
|
||||||
cx)
|
|
||||||
}
|
|
||||||
Info::References => {
|
|
||||||
render_locations(project
|
|
||||||
.update(cx, |project, cx| {
|
|
||||||
project.references(&buffer, position, cx)
|
|
||||||
})?
|
|
||||||
.await?,
|
|
||||||
cx)
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
if output.is_empty() {
|
|
||||||
Err(anyhow!("None found."))
|
|
||||||
} else {
|
|
||||||
Ok(output)
|
|
||||||
}
|
|
||||||
}).into()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Finds the range of the symbol in the buffer, if it appears between context_before_symbol
|
|
||||||
/// and context_after_symbol, and if that combined string has one unique result in the buffer.
|
|
||||||
fn find_symbol_range(
|
|
||||||
buffer: &Buffer,
|
|
||||||
context_before_symbol: &str,
|
|
||||||
symbol: &str,
|
|
||||||
context_after_symbol: &str,
|
|
||||||
) -> Option<Range<Anchor>> {
|
|
||||||
let snapshot = buffer.snapshot();
|
|
||||||
let text = snapshot.text();
|
|
||||||
let search_string = format!("{context_before_symbol}{symbol}{context_after_symbol}");
|
|
||||||
let mut positions = text.match_indices(&search_string);
|
|
||||||
let position = positions.next()?.0;
|
|
||||||
|
|
||||||
// The combined string must appear exactly once.
|
|
||||||
if positions.next().is_some() {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
|
|
||||||
let symbol_start = position + context_before_symbol.len();
|
|
||||||
let symbol_end = symbol_start + symbol.len();
|
|
||||||
let symbol_start_anchor = snapshot.anchor_before(snapshot.offset_to_point(symbol_start));
|
|
||||||
let symbol_end_anchor = snapshot.anchor_before(snapshot.offset_to_point(symbol_end));
|
|
||||||
|
|
||||||
Some(symbol_start_anchor..symbol_end_anchor)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn render_locations(locations: impl IntoIterator<Item = Location>, cx: &mut AsyncApp) -> String {
|
|
||||||
let mut answer = String::new();
|
|
||||||
|
|
||||||
for location in locations {
|
|
||||||
location
|
|
||||||
.buffer
|
|
||||||
.read_with(cx, |buffer, _cx| {
|
|
||||||
if let Some(target_path) = buffer
|
|
||||||
.file()
|
|
||||||
.and_then(|file| file.path().as_os_str().to_str())
|
|
||||||
{
|
|
||||||
let snapshot = buffer.snapshot();
|
|
||||||
let start = location.range.start.to_point(&snapshot);
|
|
||||||
let end = location.range.end.to_point(&snapshot);
|
|
||||||
let start_line = start.row + 1;
|
|
||||||
let start_col = start.column + 1;
|
|
||||||
let end_line = end.row + 1;
|
|
||||||
let end_col = end.column + 1;
|
|
||||||
|
|
||||||
if start_line == end_line {
|
|
||||||
writeln!(answer, "{target_path}:{start_line},{start_col}")
|
|
||||||
} else {
|
|
||||||
writeln!(
|
|
||||||
answer,
|
|
||||||
"{target_path}:{start_line},{start_col}-{end_line},{end_col}",
|
|
||||||
)
|
|
||||||
}
|
|
||||||
.ok();
|
|
||||||
|
|
||||||
write_code_excerpt(&mut answer, &snapshot, &location.range);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.ok();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Trim trailing newlines without reallocating.
|
|
||||||
answer.truncate(answer.trim_end().len());
|
|
||||||
|
|
||||||
answer
|
|
||||||
}
|
|
||||||
|
|
||||||
fn write_code_excerpt(buf: &mut String, snapshot: &BufferSnapshot, range: &Range<Anchor>) {
|
|
||||||
const MAX_LINE_LEN: u32 = 200;
|
|
||||||
|
|
||||||
let start = range.start.to_point(snapshot);
|
|
||||||
let end = range.end.to_point(snapshot);
|
|
||||||
|
|
||||||
for row in start.row..=end.row {
|
|
||||||
let row_start = Point::new(row, 0);
|
|
||||||
let row_end = if row < snapshot.max_point().row {
|
|
||||||
Point::new(row + 1, 0)
|
|
||||||
} else {
|
|
||||||
Point::new(row, u32::MAX)
|
|
||||||
};
|
|
||||||
|
|
||||||
buf.extend(
|
|
||||||
snapshot
|
|
||||||
.text_for_range(row_start..row_end)
|
|
||||||
.take(MAX_LINE_LEN as usize),
|
|
||||||
);
|
|
||||||
|
|
||||||
if row_end.column > MAX_LINE_LEN {
|
|
||||||
buf.push_str("…\n");
|
|
||||||
}
|
|
||||||
|
|
||||||
buf.push('\n');
|
|
||||||
}
|
|
||||||
}
|
|
Loading…
Add table
Add a link
Reference in a new issue