Compare commits
20 Commits
arm_github
...
quickfix
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8ffdc2c326 | ||
|
|
c3eacb8c83 | ||
|
|
c97f067fd3 | ||
|
|
a8c295e844 | ||
|
|
fd863ac9e9 | ||
|
|
6f2ad775e5 | ||
|
|
3340abd127 | ||
|
|
b3911355b8 | ||
|
|
9a5633b8e2 | ||
|
|
9cf5f85c8b | ||
|
|
a6c4f46bef | ||
|
|
b298ae47f6 | ||
|
|
1927dc039e | ||
|
|
af0e2068cd | ||
|
|
44e6701ccc | ||
|
|
47948f8309 | ||
|
|
84a67d82cb | ||
|
|
d478a709ed | ||
|
|
07f7a391c9 | ||
|
|
09af38a144 |
3
Cargo.lock
generated
3
Cargo.lock
generated
@@ -761,6 +761,8 @@ dependencies = [
|
||||
"itertools 0.14.0",
|
||||
"language",
|
||||
"language_model",
|
||||
"log",
|
||||
"lsp-types",
|
||||
"open",
|
||||
"project",
|
||||
"rand 0.8.5",
|
||||
@@ -768,6 +770,7 @@ dependencies = [
|
||||
"schemars",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"strum",
|
||||
"ui",
|
||||
"unindent",
|
||||
"util",
|
||||
|
||||
@@ -296,6 +296,7 @@ livekit_api = { path = "crates/livekit_api" }
|
||||
livekit_client = { path = "crates/livekit_client" }
|
||||
lmstudio = { path = "crates/lmstudio" }
|
||||
lsp = { path = "crates/lsp" }
|
||||
lsp-types = { git = "https://github.com/zed-industries/lsp-types", rev = "1fff0dd12e2071c5667327394cfec163d2a466ab" }
|
||||
markdown = { path = "crates/markdown" }
|
||||
markdown_preview = { path = "crates/markdown_preview" }
|
||||
media = { path = "crates/media" }
|
||||
|
||||
@@ -1 +1 @@
|
||||
In your response, make sure to remember and follow my instructions about how to format code blocks (and don't mention that you are remembering it, just follow the instructions).
|
||||
In your response, and also when thinking, make sure to remember and follow my instructions about how to format code blocks (and don't ever mention that you are remembering it, just follow the instructions).
|
||||
|
||||
@@ -658,6 +658,7 @@
|
||||
"tools": {
|
||||
"bash": true,
|
||||
"batch_tool": true,
|
||||
"code_actions": true,
|
||||
"code_symbols": true,
|
||||
"copy_path": false,
|
||||
"create_file": true,
|
||||
@@ -671,6 +672,7 @@
|
||||
"path_search": true,
|
||||
"read_file": true,
|
||||
"regex_search": true,
|
||||
"rename": true,
|
||||
"symbol_info": true,
|
||||
"thinking": true
|
||||
}
|
||||
|
||||
@@ -997,7 +997,8 @@ impl Thread {
|
||||
|
||||
self.attached_tracked_files_state(&mut request.messages, cx);
|
||||
|
||||
// Add reminder to the last user message about code blocks
|
||||
// Add reminder to the last user message about
|
||||
// easily-forgotten aspects of the system prompt.
|
||||
if let Some(last_user_message) = request
|
||||
.messages
|
||||
.iter_mut()
|
||||
|
||||
@@ -23,15 +23,18 @@ http_client.workspace = true
|
||||
itertools.workspace = true
|
||||
language.workspace = true
|
||||
language_model.workspace = true
|
||||
log.workspace = true
|
||||
lsp-types.workspace = true
|
||||
open = { workspace = true }
|
||||
project.workspace = true
|
||||
regex.workspace = true
|
||||
schemars.workspace = true
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
strum.workspace = true
|
||||
ui.workspace = true
|
||||
util.workspace = true
|
||||
worktree.workspace = true
|
||||
open = { workspace = true }
|
||||
workspace-hack.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
mod bash_tool;
|
||||
mod batch_tool;
|
||||
mod code_action_tool;
|
||||
mod code_symbols_tool;
|
||||
mod copy_path_tool;
|
||||
mod create_directory_tool;
|
||||
@@ -13,8 +14,10 @@ mod move_path_tool;
|
||||
mod now_tool;
|
||||
mod open_tool;
|
||||
mod path_search_tool;
|
||||
mod quickfix_tool;
|
||||
mod read_file_tool;
|
||||
mod regex_search_tool;
|
||||
mod rename_tool;
|
||||
mod replace;
|
||||
mod schema;
|
||||
mod symbol_info_tool;
|
||||
@@ -30,6 +33,7 @@ use move_path_tool::MovePathTool;
|
||||
|
||||
use crate::bash_tool::BashTool;
|
||||
use crate::batch_tool::BatchTool;
|
||||
use crate::code_action_tool::CodeActionTool;
|
||||
use crate::code_symbols_tool::CodeSymbolsTool;
|
||||
use crate::create_directory_tool::CreateDirectoryTool;
|
||||
use crate::create_file_tool::CreateFileTool;
|
||||
@@ -41,8 +45,10 @@ use crate::list_directory_tool::ListDirectoryTool;
|
||||
use crate::now_tool::NowTool;
|
||||
use crate::open_tool::OpenTool;
|
||||
use crate::path_search_tool::PathSearchTool;
|
||||
use crate::quickfix_tool::QuickfixTool;
|
||||
use crate::read_file_tool::ReadFileTool;
|
||||
use crate::regex_search_tool::RegexSearchTool;
|
||||
use crate::rename_tool::RenameTool;
|
||||
use crate::symbol_info_tool::SymbolInfoTool;
|
||||
use crate::thinking_tool::ThinkingTool;
|
||||
|
||||
@@ -58,6 +64,7 @@ pub fn init(http_client: Arc<HttpClientWithUrl>, cx: &mut App) {
|
||||
registry.register_tool(DeletePathTool);
|
||||
registry.register_tool(FindReplaceFileTool);
|
||||
registry.register_tool(SymbolInfoTool);
|
||||
registry.register_tool(CodeActionTool);
|
||||
registry.register_tool(MovePathTool);
|
||||
registry.register_tool(DiagnosticsTool);
|
||||
registry.register_tool(ListDirectoryTool);
|
||||
@@ -65,8 +72,10 @@ pub fn init(http_client: Arc<HttpClientWithUrl>, cx: &mut App) {
|
||||
registry.register_tool(OpenTool);
|
||||
registry.register_tool(CodeSymbolsTool);
|
||||
registry.register_tool(PathSearchTool);
|
||||
registry.register_tool(QuickfixTool);
|
||||
registry.register_tool(ReadFileTool);
|
||||
registry.register_tool(RegexSearchTool);
|
||||
registry.register_tool(RenameTool);
|
||||
registry.register_tool(ThinkingTool);
|
||||
registry.register_tool(FetchTool::new(http_client));
|
||||
}
|
||||
|
||||
389
crates/assistant_tools/src/code_action_tool.rs
Normal file
389
crates/assistant_tools/src/code_action_tool.rs
Normal file
@@ -0,0 +1,389 @@
|
||||
use anyhow::{Context as _, Result, anyhow};
|
||||
use assistant_tool::{ActionLog, Tool};
|
||||
use gpui::{App, Entity, Task};
|
||||
use language::{self, Anchor, Buffer, ToPointUtf16};
|
||||
use language_model::LanguageModelRequestMessage;
|
||||
use project::{self, LspAction, Project};
|
||||
use regex::Regex;
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::{ops::Range, sync::Arc};
|
||||
use ui::IconName;
|
||||
|
||||
#[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: language_model::LanguageModelToolSchemaFormat,
|
||||
) -> serde_json::Value {
|
||||
let schema = schemars::schema_for!(CodeActionToolInput);
|
||||
serde_json::to_value(&schema).unwrap()
|
||||
}
|
||||
|
||||
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>,
|
||||
cx: &mut App,
|
||||
) -> Task<Result<String>> {
|
||||
let input = match serde_json::from_value::<CodeActionToolInput>(input) {
|
||||
Ok(input) => input,
|
||||
Err(err) => return Task::ready(Err(anyhow!(err))),
|
||||
};
|
||||
|
||||
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.buffer_read(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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// 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)
|
||||
}
|
||||
19
crates/assistant_tools/src/code_action_tool/description.md
Normal file
19
crates/assistant_tools/src/code_action_tool/description.md
Normal file
@@ -0,0 +1,19 @@
|
||||
A tool for applying code actions to specific sections of your code. It uses language servers to provide refactoring capabilities similar to what you'd find in an IDE.
|
||||
|
||||
This tool can:
|
||||
- List all available code actions for a selected text range
|
||||
- Execute a specific code action on that range
|
||||
- Rename symbols across your codebase. This tool is the preferred way to rename things, and you should always prefer to rename code symbols using this tool rather than using textual find/replace when both are available.
|
||||
|
||||
Use this tool when you want to:
|
||||
- Discover what code actions are available for a piece of code
|
||||
- Apply automatic fixes and code transformations
|
||||
- Rename variables, functions, or other symbols consistently throughout your project
|
||||
- Clean up imports, implement interfaces, or perform other language-specific operations
|
||||
|
||||
- If unsure what actions are available, call the tool without specifying an action to get a list
|
||||
- For common operations, you can directly specify actions like "quickfix.all" or "source.organizeImports"
|
||||
- For renaming, use the special "textDocument/rename" action and provide the new name in the arguments field
|
||||
- Be specific with your text range and context to ensure the tool identifies the correct code location
|
||||
|
||||
The tool will automatically save any changes it makes to your files.
|
||||
@@ -11,32 +11,58 @@ use std::{fmt::Write, path::Path, sync::Arc};
|
||||
use ui::IconName;
|
||||
use util::markdown::MarkdownString;
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
|
||||
#[derive(Debug, Serialize, Deserialize, Default, JsonSchema)]
|
||||
pub struct DiagnosticsToolInput {
|
||||
/// The path to get diagnostics for. If not provided, returns a project-wide summary.
|
||||
/// The specific paths to get detailed diagnostics for (including individual line numbers).
|
||||
///
|
||||
/// This path should never be absolute, and the first component
|
||||
/// of the path should always be a root directory in a project.
|
||||
/// Regardless of whether any paths are specified here, a count of the total number of warnings
|
||||
/// and errors in the project will be reported, so providing paths here gets you strictly
|
||||
/// more information.
|
||||
///
|
||||
/// These paths should never be absolute, and the first component
|
||||
/// of each path should always be a root directory in a project.
|
||||
///
|
||||
/// <example>
|
||||
/// If the project has the following root directories:
|
||||
///
|
||||
/// - lorem
|
||||
/// - ipsum
|
||||
/// - amet
|
||||
///
|
||||
/// If you wanna access diagnostics for `dolor.txt` in `ipsum`, you should use the path `ipsum/dolor.txt`.
|
||||
/// If you want detailed diagnostics with line numbers for `dolor.txt` in `ipsum` and `consectetur.txt` in `amet`, you should use:
|
||||
///
|
||||
/// "paths": ["ipsum/dolor.txt", "amet/consectetur.txt"]
|
||||
/// </example>
|
||||
#[serde(deserialize_with = "deserialize_path")]
|
||||
pub path: Option<String>,
|
||||
#[serde(default)]
|
||||
pub paths: Vec<String>,
|
||||
|
||||
/// Which severity levels to show. Default is all.
|
||||
/// To show only errors and warnings, you should use:
|
||||
///
|
||||
/// "severity": ["error", "warning"]
|
||||
#[serde(default)]
|
||||
pub severity: Vec<Severity>,
|
||||
}
|
||||
|
||||
fn deserialize_path<'de, D>(deserializer: D) -> Result<Option<String>, D::Error>
|
||||
#[derive(
|
||||
Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq, Copy, Clone, strum::Display, Hash,
|
||||
)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub enum Severity {
|
||||
Error,
|
||||
Warning,
|
||||
Information,
|
||||
Hint,
|
||||
}
|
||||
|
||||
fn deserialize_path<'de, D>(deserializer: D) -> Result<Vec<String>, D::Error>
|
||||
where
|
||||
D: serde::Deserializer<'de>,
|
||||
{
|
||||
let opt = Option::<String>::deserialize(deserializer)?;
|
||||
// The model passes an empty string sometimes
|
||||
Ok(opt.filter(|s| !s.is_empty()))
|
||||
let paths = Vec::<String>::deserialize(deserializer)?;
|
||||
// The model passes an empty string for some paths
|
||||
Ok(paths.into_iter().filter(|s| !s.is_empty()).collect())
|
||||
}
|
||||
|
||||
pub struct DiagnosticsTool;
|
||||
@@ -63,17 +89,21 @@ impl Tool for DiagnosticsTool {
|
||||
}
|
||||
|
||||
fn ui_text(&self, input: &serde_json::Value) -> String {
|
||||
if let Some(path) = serde_json::from_value::<DiagnosticsToolInput>(input.clone())
|
||||
serde_json::from_value::<DiagnosticsToolInput>(input.clone())
|
||||
.ok()
|
||||
.and_then(|input| match input.path {
|
||||
Some(path) if !path.is_empty() => Some(MarkdownString::inline_code(&path)),
|
||||
_ => None,
|
||||
.and_then(|input| {
|
||||
input.paths.first().map(|first_path| {
|
||||
if input.paths.len() > 1 {
|
||||
format!("Check diagnostics for {} paths", input.paths.len())
|
||||
} else {
|
||||
format!(
|
||||
"Check diagnostics for {}",
|
||||
MarkdownString::inline_code(first_path)
|
||||
)
|
||||
}
|
||||
})
|
||||
})
|
||||
{
|
||||
format!("Check diagnostics for {path}")
|
||||
} else {
|
||||
"Check project diagnostics".to_string()
|
||||
}
|
||||
.unwrap_or_else(|| "Check project diagnostics".to_string())
|
||||
}
|
||||
|
||||
fn run(
|
||||
@@ -84,62 +114,20 @@ impl Tool for DiagnosticsTool {
|
||||
action_log: Entity<ActionLog>,
|
||||
cx: &mut App,
|
||||
) -> Task<Result<String>> {
|
||||
match serde_json::from_value::<DiagnosticsToolInput>(input)
|
||||
.ok()
|
||||
.and_then(|input| input.path)
|
||||
let input = serde_json::from_value::<DiagnosticsToolInput>(input).unwrap_or_default();
|
||||
let severity_filter = input.severity;
|
||||
let mut summary_output = String::new();
|
||||
let mut has_diagnostics = false;
|
||||
|
||||
// Always report the global diagnostics summary.
|
||||
{
|
||||
Some(path) if !path.is_empty() => {
|
||||
let Some(project_path) = project.read(cx).find_project_path(&path, cx) else {
|
||||
return Task::ready(Err(anyhow!("Could not find path {path} in project",)));
|
||||
};
|
||||
|
||||
let buffer =
|
||||
project.update(cx, |project, cx| project.open_buffer(project_path, cx));
|
||||
|
||||
cx.spawn(async move |cx| {
|
||||
let mut output = String::new();
|
||||
let buffer = buffer.await?;
|
||||
let snapshot = buffer.read_with(cx, |buffer, _cx| buffer.snapshot())?;
|
||||
|
||||
for (_, group) in snapshot.diagnostic_groups(None) {
|
||||
let entry = &group.entries[group.primary_ix];
|
||||
let range = entry.range.to_point(&snapshot);
|
||||
let severity = match entry.diagnostic.severity {
|
||||
DiagnosticSeverity::ERROR => "error",
|
||||
DiagnosticSeverity::WARNING => "warning",
|
||||
_ => continue,
|
||||
};
|
||||
|
||||
writeln!(
|
||||
output,
|
||||
"{} at line {}: {}",
|
||||
severity,
|
||||
range.start.row + 1,
|
||||
entry.diagnostic.message
|
||||
)?;
|
||||
}
|
||||
|
||||
if output.is_empty() {
|
||||
Ok("File doesn't have errors or warnings!".to_string())
|
||||
} else {
|
||||
Ok(output)
|
||||
}
|
||||
})
|
||||
}
|
||||
_ => {
|
||||
let project = project.read(cx);
|
||||
let mut output = String::new();
|
||||
let mut has_diagnostics = false;
|
||||
|
||||
for (project_path, _, summary) in project.diagnostic_summaries(true, cx) {
|
||||
if summary.error_count > 0 || summary.warning_count > 0 {
|
||||
let Some(worktree) = project.worktree_for_id(project_path.worktree_id, cx)
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
let project = project.read(cx);
|
||||
|
||||
for (project_path, _, summary) in project.diagnostic_summaries(true, cx) {
|
||||
if summary.error_count > 0 || summary.warning_count > 0 {
|
||||
if let Some(worktree) = project.worktree_for_id(project_path.worktree_id, cx) {
|
||||
has_diagnostics = true;
|
||||
output.push_str(&format!(
|
||||
summary_output.push_str(&format!(
|
||||
"{}: {} error(s), {} warning(s)\n",
|
||||
Path::new(worktree.read(cx).root_name())
|
||||
.join(project_path.path)
|
||||
@@ -149,17 +137,113 @@ impl Tool for DiagnosticsTool {
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
action_log.update(cx, |action_log, _cx| {
|
||||
action_log.checked_project_diagnostics();
|
||||
});
|
||||
action_log.update(cx, |action_log, _cx| {
|
||||
action_log.checked_project_diagnostics();
|
||||
});
|
||||
}
|
||||
|
||||
if input.paths.is_empty() {
|
||||
// If no paths specified, just return the summary
|
||||
if has_diagnostics {
|
||||
Task::ready(Ok(summary_output))
|
||||
} else {
|
||||
Task::ready(Ok("No errors or warnings found.".to_string()))
|
||||
}
|
||||
} else {
|
||||
let buffer_tasks = input
|
||||
.paths
|
||||
.into_iter()
|
||||
.filter_map(|path| {
|
||||
project
|
||||
.read(cx)
|
||||
.find_project_path(&path, cx)
|
||||
.map(|project_path| {
|
||||
(
|
||||
project_path.clone(),
|
||||
project.update(cx, |project, cx| {
|
||||
project.open_buffer(project_path, cx)
|
||||
}),
|
||||
)
|
||||
})
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
cx.spawn(async move |cx| {
|
||||
let mut output = String::new();
|
||||
|
||||
output.push_str("# Project Summary\n\n");
|
||||
|
||||
if has_diagnostics {
|
||||
Task::ready(Ok(output))
|
||||
output.push_str(&summary_output);
|
||||
output.push_str("\n");
|
||||
} else {
|
||||
Task::ready(Ok("No errors or warnings found in the project.".to_string()))
|
||||
output.push_str("No errors or warnings found in the project.\n");
|
||||
}
|
||||
}
|
||||
|
||||
output.push('\n');
|
||||
|
||||
let mut header_printed = false;
|
||||
|
||||
for (project_path, buffer_task) in buffer_tasks {
|
||||
let mut path_printed = false;
|
||||
let buffer = buffer_task.await?;
|
||||
let snapshot = buffer.read_with(cx, |buffer, _cx| buffer.snapshot())?;
|
||||
|
||||
for (_, group) in snapshot.diagnostic_groups(None) {
|
||||
let entry = &group.entries[group.primary_ix];
|
||||
let range = entry.range.to_point(&snapshot);
|
||||
|
||||
if let Ok(severity) = Severity::try_from(&entry.diagnostic.severity) {
|
||||
if severity_filter.is_empty() || severity_filter.contains(&severity) {
|
||||
if !header_printed {
|
||||
output.push_str("# Per-Path Diagnostics\n\n");
|
||||
header_printed = true;
|
||||
}
|
||||
|
||||
if !path_printed {
|
||||
writeln!(output, "## {}", project_path.path.display())?;
|
||||
path_printed = true;
|
||||
}
|
||||
|
||||
writeln!(
|
||||
output,
|
||||
"\n### {severity} at line {}\n{}",
|
||||
range.start.row + 1,
|
||||
entry.diagnostic.message
|
||||
)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !header_printed {
|
||||
output.push_str("No specific diagnostics found for the requested paths.");
|
||||
}
|
||||
|
||||
Ok(output)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<&DiagnosticSeverity> for Severity {
|
||||
type Error = ();
|
||||
|
||||
fn try_from(
|
||||
value: &DiagnosticSeverity,
|
||||
) -> Result<Self, <Severity as TryFrom<&DiagnosticSeverity>>::Error> {
|
||||
if *value == DiagnosticSeverity::ERROR {
|
||||
Ok(Self::Error)
|
||||
} else if *value == DiagnosticSeverity::WARNING {
|
||||
Ok(Self::Warning)
|
||||
} else if *value == DiagnosticSeverity::INFORMATION {
|
||||
Ok(Self::Information)
|
||||
} else if *value == DiagnosticSeverity::HINT {
|
||||
Ok(Self::Hint)
|
||||
} else {
|
||||
Err(())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -63,6 +63,16 @@ pub struct FindReplaceFileToolInput {
|
||||
/// even one character in this string is different in any way from how it appears
|
||||
/// in the file, then the tool call will fail.
|
||||
///
|
||||
/// If you get an error that the `find` string was not found, this means that either
|
||||
/// you made a mistake, or that the file has changed since you last looked at it.
|
||||
/// Either way, when this happens, you should retry doing this tool call until it
|
||||
/// succeeds, up to 3 times. Each time you retry, you should take another look at
|
||||
/// the exact text of the file in question, to make sure that you are searching for
|
||||
/// exactly the right string. Regardless of whether it was because you made a mistake
|
||||
/// or because the file changed since you last looked at it, you should be extra
|
||||
/// careful when retrying in this way. It's a bad experience for the user if
|
||||
/// this `find` string isn't found, so be super careful to get it exactly right!
|
||||
///
|
||||
/// <example>
|
||||
/// If a file contains this code:
|
||||
///
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
Find one unique part of a file in the project and replace that text with new text.
|
||||
|
||||
This tool is the preferred way to make edits to files. If you have multiple edits to make, including edits across multiple files, then make a plan to respond with a single message containing multiple calls to this tool - one call for each find/replace operation.
|
||||
This tool is the preferred way to make edits to files *except* when making a rename. When making a rename specifically, the rename tool must always be used instead.
|
||||
|
||||
You should use this tool when you want to edit a subset of a file's contents, but not the entire file. You should not use this tool when you want to replace the entire contents of a file with completely different contents. You also should not use this tool when you want to move or rename a file. You absolutely must NEVER use this tool to create new files from scratch. If you ever consider using this tool to create a new file from scratch, for any reason, instead you must reconsider and choose a different approach.
|
||||
If you have multiple edits to make, including edits across multiple files, then make a plan to respond with a single message containing a batch of calls to this tool - one call for each find/replace operation.
|
||||
|
||||
You should only use this tool when you want to edit a subset of a file's contents, but not the entire file. You should not use this tool when you want to replace the entire contents of a file with completely different contents. You also should not use this tool when you want to move or rename a file. You absolutely must NEVER use this tool to create new files from scratch. If you ever consider using this tool to create a new file from scratch, for any reason, instead you must reconsider and choose a different approach.
|
||||
|
||||
DO NOT call this tool until the code to be edited appears in the conversation! You must use another tool to read the file's contents into the conversation, or ask the user to add it to context first.
|
||||
|
||||
|
||||
361
crates/assistant_tools/src/quickfix_tool.rs
Normal file
361
crates/assistant_tools/src/quickfix_tool.rs
Normal file
@@ -0,0 +1,361 @@
|
||||
use crate::schema::json_schema_for;
|
||||
use anyhow::{Result, anyhow};
|
||||
use assistant_tool::{ActionLog, Tool};
|
||||
use gpui::AppContext;
|
||||
use gpui::{App, Entity, Task};
|
||||
use language::{DiagnosticSeverity, OffsetRangeExt};
|
||||
use language_model::{LanguageModelRequestMessage, LanguageModelToolSchemaFormat};
|
||||
use lsp_types::CodeActionKind;
|
||||
use project::LspAction;
|
||||
use project::Project;
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::f32::consts::E;
|
||||
use std::{fmt::Write, path::Path, sync::Arc};
|
||||
use ui::IconName;
|
||||
use util::markdown::MarkdownString;
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct QuickfixToolInput {
|
||||
/// The path to get diagnostics for and apply quickfixes. If not provided, checks the entire project.
|
||||
///
|
||||
/// 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:
|
||||
///
|
||||
/// - lorem
|
||||
/// - ipsum
|
||||
///
|
||||
/// If you want to apply quickfixes for `dolor.txt` in `ipsum`, you should use the path `ipsum/dolor.txt`.
|
||||
/// </example>
|
||||
pub path: Option<String>,
|
||||
}
|
||||
|
||||
pub struct QuickfixTool;
|
||||
|
||||
impl Tool for QuickfixTool {
|
||||
fn name(&self) -> String {
|
||||
"quickfix".into()
|
||||
}
|
||||
|
||||
fn needs_confirmation(&self, _: &serde_json::Value, _: &App) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn description(&self) -> String {
|
||||
include_str!("./quickfix_tool/description.md").into()
|
||||
}
|
||||
|
||||
fn icon(&self) -> IconName {
|
||||
IconName::Check
|
||||
}
|
||||
|
||||
fn input_schema(&self, format: LanguageModelToolSchemaFormat) -> serde_json::Value {
|
||||
json_schema_for::<QuickfixToolInput>(format)
|
||||
}
|
||||
|
||||
fn ui_text(&self, input: &serde_json::Value) -> String {
|
||||
if let Some(path) = serde_json::from_value::<QuickfixToolInput>(input.clone())
|
||||
.ok()
|
||||
.and_then(|input| match input.path {
|
||||
Some(path) if !path.is_empty() => Some(MarkdownString::inline_code(&path)),
|
||||
_ => None,
|
||||
})
|
||||
{
|
||||
format!("Apply quickfixes for {path}")
|
||||
} else {
|
||||
"Apply project-wide quickfixes".to_string()
|
||||
}
|
||||
}
|
||||
|
||||
fn run(
|
||||
self: Arc<Self>,
|
||||
input: serde_json::Value,
|
||||
_messages: &[LanguageModelRequestMessage],
|
||||
project: Entity<Project>,
|
||||
action_log: Entity<ActionLog>,
|
||||
cx: &mut App,
|
||||
) -> Task<Result<String>> {
|
||||
match serde_json::from_value::<QuickfixToolInput>(input)
|
||||
.ok()
|
||||
.and_then(|input| input.path)
|
||||
{
|
||||
Some(path) if !path.is_empty() => {
|
||||
let Some(project_path) = project.read(cx).find_project_path(&path, cx) else {
|
||||
return Task::ready(Err(anyhow!("Could not find path {path} in project")));
|
||||
};
|
||||
|
||||
let buffer =
|
||||
project.update(cx, |project, cx| project.open_buffer(project_path, cx));
|
||||
|
||||
cx.spawn(async move |cx| {
|
||||
let mut output = String::new();
|
||||
let mut fixes_applied = 0;
|
||||
let buffer = buffer.await?;
|
||||
let snapshot = buffer.read_with(cx, |buffer, _cx| buffer.snapshot())?;
|
||||
let mut unfixed = Vec::new();
|
||||
|
||||
// Collect diagnostics to apply quickfixes for
|
||||
for entry in snapshot.diagnostic_groups(None).into_iter().flat_map(
|
||||
|(_, group)| group.entries.into_iter()
|
||||
) {
|
||||
let range = entry.range.to_point(&snapshot);
|
||||
let actions = project
|
||||
.update(cx, |project, cx| {
|
||||
project.code_actions(
|
||||
&buffer,
|
||||
range.start..range.end,
|
||||
None,
|
||||
cx
|
||||
)
|
||||
})?.await?;
|
||||
|
||||
let quickfixes = actions.into_iter().filter(|action| {
|
||||
if let LspAction::Action(code_action) = &action.lsp_action {
|
||||
code_action.kind == Some(CodeActionKind::QUICKFIX)
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}).collect::<Vec<_>>();
|
||||
|
||||
match quickfixes.first() {
|
||||
Some(default_quickfix) => {
|
||||
// If there's a quickfix marked as preferred, use that.
|
||||
// Otherwise fall back on the first available quickfix in the list.
|
||||
let preferred_action = quickfixes.iter().find(|action| {
|
||||
if let LspAction::Action(code_action) = &action.lsp_action {
|
||||
code_action.is_preferred.unwrap_or(false)
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}).unwrap_or(default_quickfix);
|
||||
|
||||
project
|
||||
.update(cx, |project, cx| {
|
||||
project.apply_code_action(buffer.clone(), preferred_action.clone(), true, cx)
|
||||
})?
|
||||
.await?;
|
||||
|
||||
log::info!("Applied quickfix: {}", preferred_action.lsp_action.title());
|
||||
|
||||
fixes_applied += 1;
|
||||
}
|
||||
None => {
|
||||
unfixed.push(entry);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Save the buffer after applying fixes
|
||||
if fixes_applied > 0 {
|
||||
project
|
||||
.update(cx, |project, cx| project.save_buffer(buffer.clone(), cx))?
|
||||
.await?;
|
||||
|
||||
action_log.update(cx, |log, cx| {
|
||||
log.buffer_edited(buffer.clone(), cx)
|
||||
})?;
|
||||
}
|
||||
|
||||
// Generate summary
|
||||
if output.is_empty() {
|
||||
Ok("No issues found in the file!".to_string())
|
||||
} else {
|
||||
writeln!(output, "\nSummary: Applied {} quickfixes. Remaining issues: {} - use the diagnostics tool to see them.",
|
||||
fixes_applied, unfixed.len())?;
|
||||
Ok(output)
|
||||
}
|
||||
})
|
||||
}
|
||||
_ => {
|
||||
todo!("Share code with the other branch.");
|
||||
let mut output = String::new();
|
||||
let mut files_with_diagnostics = Vec::new();
|
||||
|
||||
// Collect all files with diagnostics for processing
|
||||
{
|
||||
let project_ref = project.read(cx);
|
||||
for (project_path, _, summary) in project_ref.diagnostic_summaries(true, cx) {
|
||||
if summary.error_count > 0 || summary.warning_count > 0 {
|
||||
if let Some(worktree) =
|
||||
project_ref.worktree_for_id(project_path.worktree_id, cx)
|
||||
{
|
||||
let path_str = Path::new(worktree.read(cx).root_name())
|
||||
.join(project_path.path)
|
||||
.display()
|
||||
.to_string();
|
||||
|
||||
files_with_diagnostics.push(path_str);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Create a task to process all files with diagnostics
|
||||
let project = project.clone();
|
||||
let action_log = action_log.clone();
|
||||
let files_to_process = files_with_diagnostics.clone();
|
||||
|
||||
cx.spawn(async move |cx| {
|
||||
let mut total_fixes_applied = 0;
|
||||
let mut total_errors_unfixed = 0;
|
||||
let mut total_warnings_unfixed = 0;
|
||||
|
||||
// Process each file with diagnostics
|
||||
for file_path in files_to_process {
|
||||
writeln!(output, "Processing {}...", file_path)?;
|
||||
|
||||
let Ok(Some(project_path)) = project.read_with(cx, |project, cx| project.find_project_path(&file_path, cx)) else {
|
||||
writeln!(output, " Could not resolve project path for {}", file_path)?;
|
||||
continue;
|
||||
};
|
||||
|
||||
let buffer_open_result = project.update(cx, |project, cx|
|
||||
project.open_buffer(project_path, cx));
|
||||
|
||||
let Ok(buffer_handle) = buffer_open_result else {
|
||||
writeln!(output, " Failed to open buffer for {}", file_path)?;
|
||||
continue;
|
||||
};
|
||||
|
||||
let buffer = match buffer_handle.await {
|
||||
Ok(buffer) => buffer,
|
||||
Err(err) => {
|
||||
writeln!(output, " Failed to load buffer for {}: {}", file_path, err)?;
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
let mut file_fixes_applied = 0;
|
||||
let mut file_errors_unfixed = 0;
|
||||
let mut file_warnings_unfixed = 0;
|
||||
let mut needs_save = false;
|
||||
|
||||
let snapshot = buffer.read_with(cx, |buffer, _cx| buffer.snapshot())?;
|
||||
|
||||
// Process diagnostics for this file
|
||||
for (_, group) in snapshot.diagnostic_groups(None) {
|
||||
let entry = &group.entries[group.primary_ix];
|
||||
let range = entry.range.to_point(&snapshot);
|
||||
let severity = entry.diagnostic.severity;
|
||||
|
||||
// Skip informational and hint diagnostics
|
||||
if severity != DiagnosticSeverity::ERROR &&
|
||||
severity != DiagnosticSeverity::WARNING {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Get code actions (quickfixes) for this diagnostic
|
||||
let actions = project
|
||||
.update(cx, |project, cx| {
|
||||
project.code_actions(
|
||||
&buffer,
|
||||
range.start..range.end,
|
||||
None,
|
||||
cx
|
||||
)
|
||||
})?;
|
||||
|
||||
let actions_result = actions.await;
|
||||
|
||||
match actions_result {
|
||||
Ok(actions) => {
|
||||
// Find quickfix actions
|
||||
let quickfixes = actions.into_iter().filter(|action| {
|
||||
if let LspAction::Action(code_action) = &action.lsp_action {
|
||||
if let Some(kind) = &code_action.kind {
|
||||
kind.as_str().starts_with("quickfix")
|
||||
} else {
|
||||
false
|
||||
}
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}).collect::<Vec<_>>();
|
||||
|
||||
if !quickfixes.is_empty() {
|
||||
// Find the preferred quickfix (marked as isPreferred or the first one)
|
||||
let preferred_action = quickfixes.iter().find(|action| {
|
||||
if let LspAction::Action(code_action) = &action.lsp_action {
|
||||
code_action.is_preferred.unwrap_or(false)
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}).unwrap_or(&quickfixes[0]);
|
||||
|
||||
// Apply the quickfix
|
||||
let title = preferred_action.lsp_action.title().to_string();
|
||||
project
|
||||
.update(cx, |project, cx| {
|
||||
project.apply_code_action(buffer.clone(), preferred_action.clone(), true, cx)
|
||||
})?
|
||||
.await?;
|
||||
|
||||
writeln!(output, " Applied quickfix: {title}")?;
|
||||
file_fixes_applied += 1;
|
||||
needs_save = true;
|
||||
} else {
|
||||
// Track unfixed diagnostics
|
||||
match severity {
|
||||
DiagnosticSeverity::ERROR => file_errors_unfixed += 1,
|
||||
DiagnosticSeverity::WARNING => file_warnings_unfixed += 1,
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
},
|
||||
Err(_) => {
|
||||
// Track unfixed diagnostics
|
||||
match severity {
|
||||
DiagnosticSeverity::ERROR => file_errors_unfixed += 1,
|
||||
DiagnosticSeverity::WARNING => file_warnings_unfixed += 1,
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Save the buffer after applying fixes
|
||||
if needs_save {
|
||||
project
|
||||
.update(cx, |project, cx| project.save_buffer(buffer.clone(), cx))?
|
||||
.await?;
|
||||
|
||||
action_log.update(cx, |log, cx| {
|
||||
log.buffer_edited(buffer.clone(), cx)
|
||||
})?;
|
||||
}
|
||||
|
||||
// Update totals
|
||||
total_fixes_applied += file_fixes_applied;
|
||||
total_errors_unfixed += file_errors_unfixed;
|
||||
total_warnings_unfixed += file_warnings_unfixed;
|
||||
|
||||
// File summary
|
||||
if file_fixes_applied > 0 || file_errors_unfixed > 0 || file_warnings_unfixed > 0 {
|
||||
writeln!(output, " {} quickfixes applied. Remaining: {} errors, {} warnings\n",
|
||||
file_fixes_applied, file_errors_unfixed, file_warnings_unfixed)?;
|
||||
} else {
|
||||
writeln!(output, " No issues fixed or found\n")?;
|
||||
}
|
||||
}
|
||||
|
||||
// Mark that we've checked diagnostics
|
||||
action_log.update(cx, |action_log, _cx| {
|
||||
action_log.checked_project_diagnostics();
|
||||
})?;
|
||||
|
||||
// Generate overall summary
|
||||
if files_with_diagnostics.is_empty() {
|
||||
Ok("No issues found in the project!".to_string())
|
||||
} else {
|
||||
writeln!(output, "\nProject-wide summary: Applied {} quickfixes. Remaining issues: {} errors, {} warnings.",
|
||||
total_fixes_applied, total_errors_unfixed, total_warnings_unfixed)?;
|
||||
Ok(output)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
16
crates/assistant_tools/src/quickfix_tool/description.md
Normal file
16
crates/assistant_tools/src/quickfix_tool/description.md
Normal file
@@ -0,0 +1,16 @@
|
||||
Get errors and warnings for the project or a specific file and apply quickfixes automatically where possible.
|
||||
|
||||
This tool can be invoked to find diagnostics in your code and automatically apply available quickfixes to resolve them. Quickfixes are code edits suggested by language servers that can automatically fix common issues.
|
||||
|
||||
When a path is provided, it checks that specific file for diagnostics and applies quickfixes.
|
||||
When no path is provided, it finds diagnostics project-wide and applies quickfixes where possible.
|
||||
|
||||
<example>
|
||||
To automatically fix issues in a specific file:
|
||||
{
|
||||
"path": "src/main.rs"
|
||||
}
|
||||
|
||||
To find and fix issues across the entire project:
|
||||
{}
|
||||
</example>
|
||||
205
crates/assistant_tools/src/rename_tool.rs
Normal file
205
crates/assistant_tools/src/rename_tool.rs
Normal file
@@ -0,0 +1,205 @@
|
||||
use anyhow::{Context as _, Result, anyhow};
|
||||
use assistant_tool::{ActionLog, Tool};
|
||||
use gpui::{App, Entity, Task};
|
||||
use language::{self, Buffer, ToPointUtf16};
|
||||
use language_model::LanguageModelRequestMessage;
|
||||
use project::Project;
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::sync::Arc;
|
||||
use ui::IconName;
|
||||
|
||||
#[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: language_model::LanguageModelToolSchemaFormat,
|
||||
) -> serde_json::Value {
|
||||
let schema = schemars::schema_for!(RenameToolInput);
|
||||
serde_json::to_value(&schema).unwrap()
|
||||
}
|
||||
|
||||
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>,
|
||||
cx: &mut App,
|
||||
) -> Task<Result<String>> {
|
||||
let input = match serde_json::from_value::<RenameToolInput>(input) {
|
||||
Ok(input) => input,
|
||||
Err(err) => return Task::ready(Err(anyhow!(err))),
|
||||
};
|
||||
|
||||
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.buffer_read(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))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// 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)
|
||||
}
|
||||
15
crates/assistant_tools/src/rename_tool/description.md
Normal file
15
crates/assistant_tools/src/rename_tool/description.md
Normal file
@@ -0,0 +1,15 @@
|
||||
Renames a symbol across your codebase using the language server's semantic knowledge.
|
||||
|
||||
This tool performs a rename refactoring operation on a specified symbol. It uses the project's language server to analyze the code and perform the rename correctly across all files where the symbol is referenced.
|
||||
|
||||
Unlike a simple find and replace, this tool understands the semantic meaning of the code, so it only renames the specific symbol you specify and not unrelated text that happens to have the same name.
|
||||
|
||||
Examples of symbols you can rename:
|
||||
- Variables
|
||||
- Functions
|
||||
- Classes/structs
|
||||
- Fields/properties
|
||||
- Methods
|
||||
- Interfaces/traits
|
||||
|
||||
The language server handles updating all references to the renamed symbol throughout the codebase.
|
||||
@@ -22,7 +22,7 @@ collections.workspace = true
|
||||
futures.workspace = true
|
||||
gpui.workspace = true
|
||||
log.workspace = true
|
||||
lsp-types = { git = "https://github.com/zed-industries/lsp-types", rev = "1fff0dd12e2071c5667327394cfec163d2a466ab" }
|
||||
lsp-types.workspace = true
|
||||
parking_lot.workspace = true
|
||||
postage.workspace = true
|
||||
serde.workspace = true
|
||||
|
||||
Reference in New Issue
Block a user