Compare commits

..

11 Commits

Author SHA1 Message Date
Richard Feldman
05660ec626 Have the delete tool use paths, not globs 2025-03-13 10:10:59 -04:00
Antonio Scandurra
8cf5af1a84 Introduce DiagnosticsTool (#26670)
Release Notes:

- N/A
2025-03-13 14:53:00 +01:00
Albin Kocheril Chacko
247ee880d2 Fix typo in default.json (#26666)
minor typo fix

Release Notes:

- N/A
2025-03-13 13:39:28 +00:00
Nate Butler
2e217759c0 gruvbox: version_control_ -> version_control. (#26665)
Missed this in PR #26606 

Before:

![CleanShot 2025-03-13 at 08 58
59@2x](https://github.com/user-attachments/assets/021df4b1-5a70-4fae-a109-9b8bb35949e3)

After:

![CleanShot 2025-03-13 at 08 59
22@2x](https://github.com/user-attachments/assets/01dca26d-77ec-4a54-8b7c-aa2fb160ff7d)

Release Notes:

- theme: Fixed an issue where version control colors weren't applying
correctly. (again)
2025-03-13 13:13:35 +00:00
Danilo Leal
0a0c163692 assistant2: Use icons for tool call status communication (#26617)
It was hard to catch the running & pending states, though. When running,
it will appear as a spinning arrow circle icon.

<img
src="https://github.com/user-attachments/assets/dbf1bc0a-6fa3-41c6-bcd7-2226e89c87b4"
width="500px" />

Release Notes:

- N/A
2025-03-13 10:01:20 -03:00
Antonio Scandurra
e80df25386 Iterate on tools some more (#26663)
Release Notes:

- N/A
2025-03-13 12:42:02 +00:00
Danilo Leal
d9590f3f0e docs: Improve introduction to Edit Prediction (#26620)
As I was writing a blog post about Edit Prediction, I realized we didn't
have a great section in the docs I could link to talking about
configuring it. We weren't: 1) explicitly exposing the settings code to
add Zed as the edit prediction provider, and 2) not showing an image of
the title bar banner.

Release Notes:

- N/A
2025-03-13 09:03:49 -03:00
Antonio Scandurra
4ecd1b5174 Fix bad cd sometimes used by BashTool and set edit model temperature to 0 (#26656)
Release Notes:

- N/A
2025-03-13 10:47:00 +00:00
Antonio Scandurra
70c973f6c3 Fix issues in EditFilesTool, ListDirectoryTool and BashTool (#26647)
Release Notes:

- N/A
2025-03-13 09:41:27 +00:00
Stanislav Alekseev
e842b4eade macOS: Disable automatic window tabbing in fullscreen mode (#26600)
Fixes #26534 (this time for real)

Release Notes:

- Fixed issue where Zed would behave weirdly when opening new fullscreen
windows by disabling window tabbing

Apple docs:
https://developer.apple.com/documentation/appkit/nswindow/allowsautomaticwindowtabbing
2025-03-13 12:45:01 +05:30
Agus Zubiaga
606aa7a78c Edit tool debugging (#26637)
Adds an `debug: edit tool` action that opens a new view which will help
us debug the edit tool internals. As the edit tool runs, the log
displays:

- Instructions provided by the main model
- Response stream from the editor model
- Parsed edit blocks
- Tool output provided back to main model

The log automatically records all edit tool interactions for staff, so
if you notice something weird, you can debug it retroactively without
having to open the debug tool first. We may want to limit the number of
recorded requests later.

I have a few more ideas for it, but this seems like a good starting
point.


https://github.com/user-attachments/assets/c61f5ce8-08b1-4500-accb-db2a480eb3ab


Release Notes:

- N/A
2025-03-13 04:03:01 +00:00
27 changed files with 887 additions and 293 deletions

6
Cargo.lock generated
View File

@@ -673,16 +673,22 @@ dependencies = [
"assistant_tool",
"chrono",
"collections",
"feature_flags",
"futures 0.3.31",
"gpui",
"language",
"language_model",
"project",
"rand 0.8.5",
"release_channel",
"schemars",
"serde",
"serde_json",
"settings",
"theme",
"ui",
"util",
"workspace",
]
[[package]]

View File

@@ -11,8 +11,8 @@ You should only perform actions that modify the users system if explicitly re
Be concise and direct in your responses.
The user has opened a project that contains the following top-level directories/files:
The user has opened a project that contains the following root directories/files:
{{#each worktree_root_names}}
- {{this}}
{{#each worktrees}}
- {{root_name}} (absolute path: {{abs_path}})
{{/each}}

View File

@@ -547,7 +547,7 @@
"git_panel": {
// Whether to show the git panel button in the status bar.
"button": true,
// Where to the git panel. Can be 'left' or 'right'.
// Where to show the git panel. Can be 'left' or 'right'.
"dock": "left",
// Default width of the git panel.
"default_width": 360,

View File

@@ -6,15 +6,7 @@
{
"name": "Gruvbox Dark",
"appearance": "dark",
"accents": [
"#cc241dff",
"#98971aff",
"#d79921ff",
"#458588ff",
"#b16286ff",
"#689d6aff",
"#d65d0eff"
],
"accents": ["#cc241dff", "#98971aff", "#d79921ff", "#458588ff", "#b16286ff", "#689d6aff", "#d65d0eff"],
"style": {
"border": "#5b534dff",
"border.variant": "#494340ff",
@@ -105,9 +97,9 @@
"terminal.ansi.bright_white": "#fbf1c7ff",
"terminal.ansi.dim_white": "#b0a189ff",
"link_text.hover": "#83a598ff",
"version_control_added": "#b7bb26ff",
"version_control_modified": "#f9bd2fff",
"version_control_deleted": "#fb4a35ff",
"version_control.added": "#b7bb26ff",
"version_control.modified": "#f9bd2fff",
"version_control.deleted": "#fb4a35ff",
"conflict": "#f9bd2fff",
"conflict.background": "#572e10ff",
"conflict.border": "#754916ff",
@@ -399,15 +391,7 @@
{
"name": "Gruvbox Dark Hard",
"appearance": "dark",
"accents": [
"#cc241dff",
"#98971aff",
"#d79921ff",
"#458588ff",
"#b16286ff",
"#689d6aff",
"#d65d0eff"
],
"accents": ["#cc241dff", "#98971aff", "#d79921ff", "#458588ff", "#b16286ff", "#689d6aff", "#d65d0eff"],
"style": {
"border": "#5b534dff",
"border.variant": "#494340ff",
@@ -498,9 +482,9 @@
"terminal.ansi.bright_white": "#fbf1c7ff",
"terminal.ansi.dim_white": "#b0a189ff",
"link_text.hover": "#83a598ff",
"version_control_added": "#b7bb26ff",
"version_control_modified": "#f9bd2fff",
"version_control_deleted": "#fb4a35ff",
"version_control.added": "#b7bb26ff",
"version_control.modified": "#f9bd2fff",
"version_control.deleted": "#fb4a35ff",
"conflict": "#f9bd2fff",
"conflict.background": "#572e10ff",
"conflict.border": "#754916ff",
@@ -792,15 +776,7 @@
{
"name": "Gruvbox Dark Soft",
"appearance": "dark",
"accents": [
"#cc241dff",
"#98971aff",
"#d79921ff",
"#458588ff",
"#b16286ff",
"#689d6aff",
"#d65d0eff"
],
"accents": ["#cc241dff", "#98971aff", "#d79921ff", "#458588ff", "#b16286ff", "#689d6aff", "#d65d0eff"],
"style": {
"border": "#5b534dff",
"border.variant": "#494340ff",
@@ -891,9 +867,9 @@
"terminal.ansi.bright_white": "#fbf1c7ff",
"terminal.ansi.dim_white": "#b0a189ff",
"link_text.hover": "#83a598ff",
"version_control_added": "#b7bb26ff",
"version_control_modified": "#f9bd2fff",
"version_control_deleted": "#fb4a35ff",
"version_control.added": "#b7bb26ff",
"version_control.modified": "#f9bd2fff",
"version_control.deleted": "#fb4a35ff",
"conflict": "#f9bd2fff",
"conflict.background": "#572e10ff",
"conflict.border": "#754916ff",
@@ -1185,15 +1161,7 @@
{
"name": "Gruvbox Light",
"appearance": "light",
"accents": [
"#cc241dff",
"#98971aff",
"#d79921ff",
"#458588ff",
"#b16286ff",
"#689d6aff",
"#d65d0eff"
],
"accents": ["#cc241dff", "#98971aff", "#d79921ff", "#458588ff", "#b16286ff", "#689d6aff", "#d65d0eff"],
"style": {
"border": "#c8b899ff",
"border.variant": "#ddcca7ff",
@@ -1284,9 +1252,9 @@
"terminal.ansi.bright_white": "#282828ff",
"terminal.ansi.dim_white": "#73675eff",
"link_text.hover": "#0b6678ff",
"version_control_added": "#797410ff",
"version_control_modified": "#b57615ff",
"version_control_deleted": "#9d0308ff",
"version_control.added": "#797410ff",
"version_control.modified": "#b57615ff",
"version_control.deleted": "#9d0308ff",
"conflict": "#b57615ff",
"conflict.background": "#f5e2d0ff",
"conflict.border": "#ebccabff",
@@ -1578,15 +1546,7 @@
{
"name": "Gruvbox Light Hard",
"appearance": "light",
"accents": [
"#cc241dff",
"#98971aff",
"#d79921ff",
"#458588ff",
"#b16286ff",
"#689d6aff",
"#d65d0eff"
],
"accents": ["#cc241dff", "#98971aff", "#d79921ff", "#458588ff", "#b16286ff", "#689d6aff", "#d65d0eff"],
"style": {
"border": "#c8b899ff",
"border.variant": "#ddcca7ff",
@@ -1677,9 +1637,9 @@
"terminal.ansi.bright_white": "#282828ff",
"terminal.ansi.dim_white": "#73675eff",
"link_text.hover": "#0b6678ff",
"version_control_added": "#797410ff",
"version_control_modified": "#b57615ff",
"version_control_deleted": "#9d0308ff",
"version_control.added": "#797410ff",
"version_control.modified": "#b57615ff",
"version_control.deleted": "#9d0308ff",
"conflict": "#b57615ff",
"conflict.background": "#f5e2d0ff",
"conflict.border": "#ebccabff",
@@ -1971,15 +1931,7 @@
{
"name": "Gruvbox Light Soft",
"appearance": "light",
"accents": [
"#cc241dff",
"#98971aff",
"#d79921ff",
"#458588ff",
"#b16286ff",
"#689d6aff",
"#d65d0eff"
],
"accents": ["#cc241dff", "#98971aff", "#d79921ff", "#458588ff", "#b16286ff", "#689d6aff", "#d65d0eff"],
"style": {
"border": "#c8b899ff",
"border.variant": "#ddcca7ff",
@@ -2070,9 +2022,9 @@
"terminal.ansi.bright_white": "#282828ff",
"terminal.ansi.dim_white": "#73675eff",
"link_text.hover": "#0b6678ff",
"version_control_added": "#797410ff",
"version_control_modified": "#b57615ff",
"version_control_deleted": "#9d0308ff",
"version_control.added": "#797410ff",
"version_control.modified": "#b57615ff",
"version_control.deleted": "#9d0308ff",
"conflict": "#b57615ff",
"conflict.background": "#f5e2d0ff",
"conflict.border": "#ebccabff",

View File

@@ -1,11 +1,13 @@
use std::sync::Arc;
use std::time::Duration;
use collections::HashMap;
use editor::{Editor, MultiBuffer};
use gpui::{
list, AbsoluteLength, AnyElement, App, ClickEvent, DefiniteLength, EdgesRefinement, Empty,
Entity, Focusable, Length, ListAlignment, ListOffset, ListState, StyleRefinement, Subscription,
Task, TextStyleRefinement, UnderlineStyle,
list, percentage, AbsoluteLength, Animation, AnimationExt, AnyElement, App, ClickEvent,
DefiniteLength, EdgesRefinement, Empty, Entity, Focusable, Length, ListAlignment, ListOffset,
ListState, StyleRefinement, Subscription, Task, TextStyleRefinement, Transformation,
UnderlineStyle,
};
use language::{Buffer, LanguageRegistry};
use language_model::{LanguageModelRegistry, LanguageModelToolUseId, Role};
@@ -701,13 +703,13 @@ impl ActiveThread {
.child(
h_flex()
.justify_between()
.py_0p5()
.py_1()
.pl_1()
.pr_2()
.bg(cx.theme().colors().editor_foreground.opacity(0.02))
.bg(cx.theme().colors().editor_foreground.opacity(0.025))
.map(|element| {
if is_open {
element.border_b_1().rounded_t(px(6.))
element.border_b_1().rounded_t_md()
} else {
element.rounded_md()
}
@@ -731,16 +733,35 @@ impl ActiveThread {
))
.child(Label::new(tool_use.name)),
)
.child(
Label::new(match tool_use.status {
ToolUseStatus::Pending => "Pending",
ToolUseStatus::Running => "Running",
ToolUseStatus::Finished(_) => "Finished",
ToolUseStatus::Error(_) => "Error",
})
.size(LabelSize::XSmall)
.buffer_font(cx),
),
.child({
let (icon_name, color, animated) = match &tool_use.status {
ToolUseStatus::Pending => {
(IconName::Warning, Color::Warning, false)
}
ToolUseStatus::Running => {
(IconName::ArrowCircle, Color::Accent, true)
}
ToolUseStatus::Finished(_) => {
(IconName::Check, Color::Success, false)
}
ToolUseStatus::Error(_) => (IconName::Close, Color::Error, false),
};
let icon = Icon::new(icon_name).color(color).size(IconSize::Small);
if animated {
icon.with_animation(
"arrow-circle",
Animation::new(Duration::from_secs(2)).repeat(),
|icon, delta| {
icon.transform(Transformation::rotate(percentage(delta)))
},
)
.into_any_element()
} else {
icon.into_any_element()
}
}),
)
.map(|parent| {
if !is_open {
@@ -812,7 +833,7 @@ impl ActiveThread {
.bg(cx.theme().colors().editor_foreground.opacity(0.02))
.map(|element| {
if is_open {
element.border_b_1().rounded_t(px(6.))
element.border_b_1().rounded_t_md()
} else {
element.rounded_md()
}

View File

@@ -13,7 +13,7 @@ use language_model::{
Role, StopReason,
};
use project::Project;
use prompt_store::PromptBuilder;
use prompt_store::{AssistantSystemPromptWorktree, PromptBuilder};
use scripting_tool::{ScriptingSession, ScriptingTool};
use serde::{Deserialize, Serialize};
use util::{post_inc, ResultExt, TryFutureExt as _};
@@ -384,8 +384,14 @@ impl Thread {
let worktree_root_names = self
.project
.read(cx)
.worktree_root_names(cx)
.map(ToString::to_string)
.visible_worktrees(cx)
.map(|worktree| {
let worktree = worktree.read(cx);
AssistantSystemPromptWorktree {
root_name: worktree.root_name().into(),
abs_path: worktree.abs_path(),
}
})
.collect::<Vec<_>>();
let system_prompt = self
.prompt_builder

View File

@@ -16,15 +16,21 @@ anyhow.workspace = true
assistant_tool.workspace = true
chrono.workspace = true
collections.workspace = true
feature_flags.workspace = true
futures.workspace = true
gpui.workspace = true
language.workspace = true
language_model.workspace = true
project.workspace = true
release_channel.workspace = true
schemars.workspace = true
serde.workspace = true
serde_json.workspace = true
theme.workspace = true
ui.workspace = true
util.workspace = true
workspace.workspace = true
settings.workspace = true
[dev-dependencies]
rand.workspace = true

View File

@@ -1,5 +1,6 @@
mod bash_tool;
mod delete_path_tool;
mod diagnostics_tool;
mod edit_files_tool;
mod list_directory_tool;
mod now_tool;
@@ -12,6 +13,7 @@ use gpui::App;
use crate::bash_tool::BashTool;
use crate::delete_path_tool::DeletePathTool;
use crate::diagnostics_tool::DiagnosticsTool;
use crate::edit_files_tool::EditFilesTool;
use crate::list_directory_tool::ListDirectoryTool;
use crate::now_tool::NowTool;
@@ -21,14 +23,16 @@ use crate::regex_search::RegexSearchTool;
pub fn init(cx: &mut App) {
assistant_tool::init(cx);
crate::edit_files_tool::log::init(cx);
let registry = ToolRegistry::global(cx);
registry.register_tool(NowTool);
registry.register_tool(ReadFileTool);
registry.register_tool(ListDirectoryTool);
registry.register_tool(EditFilesTool);
registry.register_tool(PathSearchTool);
registry.register_tool(RegexSearchTool);
registry.register_tool(DeletePathTool);
registry.register_tool(BashTool);
registry.register_tool(DeletePathTool);
registry.register_tool(DiagnosticsTool);
registry.register_tool(EditFilesTool);
registry.register_tool(ListDirectoryTool);
registry.register_tool(NowTool);
registry.register_tool(PathSearchTool);
registry.register_tool(ReadFileTool);
registry.register_tool(RegexSearchTool);
}

View File

@@ -1,4 +1,4 @@
use anyhow::{anyhow, Result};
use anyhow::{anyhow, Context as _, Result};
use assistant_tool::Tool;
use gpui::{App, Entity, Task};
use language_model::LanguageModelRequestMessage;
@@ -6,11 +6,17 @@ use project::Project;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use std::sync::Arc;
use util::command::new_smol_command;
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
pub struct BashToolInput {
/// The bash command to execute as a one-liner.
///
/// WARNING: you must not `cd` into the working directory, as that's already
/// taken care of automatically. Doing so will cause the command to fail!
command: String,
/// Working directory for the command. This must be one of the root directories of the project.
working_directory: String,
}
pub struct BashTool;
@@ -33,7 +39,7 @@ impl Tool for BashTool {
self: Arc<Self>,
input: serde_json::Value,
_messages: &[LanguageModelRequestMessage],
_project: Entity<Project>,
project: Entity<Project>,
cx: &mut App,
) -> Task<Result<String>> {
let input: BashToolInput = match serde_json::from_value(input) {
@@ -41,23 +47,34 @@ impl Tool for BashTool {
Err(err) => return Task::ready(Err(anyhow!(err))),
};
let Some(worktree) = project
.read(cx)
.worktree_for_root_name(&input.working_directory, cx)
else {
return Task::ready(Err(anyhow!("Working directory not found in the project")));
};
let working_directory = worktree.read(cx).abs_path();
cx.spawn(|_| async move {
// Add 2>&1 to merge stderr into stdout for proper interleaving
// Add 2>&1 to merge stderr into stdout for proper interleaving.
let command = format!("{} 2>&1", input.command);
// Spawn a blocking task to execute the command
let output = futures::executor::block_on(async {
std::process::Command::new("bash")
.arg("-c")
.arg(&command)
.output()
.map_err(|err| anyhow!("Failed to execute bash command: {}", err))
})?;
let output = new_smol_command("bash")
.arg("-c")
.arg(&command)
.current_dir(working_directory)
.output()
.await
.context("Failed to execute bash command")?;
let output_string = String::from_utf8_lossy(&output.stdout).to_string();
if output.status.success() {
Ok(output_string)
if output_string.is_empty() {
Ok("Command executed successfully.".to_string())
} else {
Ok(output_string)
}
} else {
Ok(format!(
"Command failed with exit code {}\n{}",

View File

@@ -1 +1,7 @@
Executes a bash one-liner and returns the combined output. This tool spawns a bash process, combines stdout and stderr into one interleaved stream as they are produced (preserving the order of writes), and captures that stream into a string which is returned. Use this tool when you need to run shell commands to get information about the system or process files.
Executes a bash one-liner and returns the combined output.
This tool spawns a bash process IN THE SPECIFIED WORKING DIRECTORY, combines stdout and stderr into one interleaved stream as they are produced (preserving the order of writes), and captures that stream into a string which is returned.
WARNING: **NEVER** use 'cd' commands to navigate to the working directory - this is automatically handled by the 'working_directory' parameter. Only use 'cd' to navigate to subdirectories within the specified working directory.
Remember that each invocation of this tool will spawn a new bash process, so you can't rely on any state from previous invocations.

View File

@@ -5,12 +5,11 @@ use language_model::LanguageModelRequestMessage;
use project::Project;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use std::{fs, path::PathBuf, sync::Arc};
use util::paths::PathMatcher;
use std::{fs, sync::Arc};
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
pub struct DeletePathToolInput {
/// The glob to match files in the project to delete.
/// The path to the file or directory to delete.
///
/// <example>
/// If the project has the following files:
@@ -19,9 +18,9 @@ pub struct DeletePathToolInput {
/// - directory2/a/things.txt
/// - directory3/a/other.txt
///
/// You can delete the first two files by providing a glob of "*thing*.txt"
/// You can delete the first file by providing the path "directory1/a/something.txt"
/// </example>
pub glob: String,
pub path: String,
}
pub struct DeletePathTool;
@@ -47,47 +46,19 @@ impl Tool for DeletePathTool {
project: Entity<Project>,
cx: &mut App,
) -> Task<Result<String>> {
let glob = match serde_json::from_value::<DeletePathToolInput>(input) {
Ok(input) => input.glob,
let target_path = match serde_json::from_value::<DeletePathToolInput>(input) {
Ok(input) => input.path,
Err(err) => return Task::ready(Err(anyhow!(err))),
};
let path_matcher = match PathMatcher::new(&[glob.clone()]) {
Ok(matcher) => matcher,
Err(err) => return Task::ready(Err(anyhow!("Invalid glob: {}", err))),
};
struct Match {
display_path: String,
path: PathBuf,
}
let mut matches = Vec::new();
let mut deleted_paths = Vec::new();
let mut errors = Vec::new();
// Find the path in any of the worktrees
for worktree_handle in project.read(cx).worktrees(cx) {
let worktree = worktree_handle.read(cx);
let worktree_root = worktree.abs_path().to_path_buf();
// Don't consider ignored entries.
for entry in worktree.entries(false, 0) {
if path_matcher.is_match(&entry.path) {
matches.push(Match {
path: worktree_root.join(&entry.path),
display_path: entry.path.display().to_string(),
});
}
}
}
if matches.is_empty() {
return Task::ready(Ok(format!("No paths in the project matched {glob:?}")));
}
let paths_matched = matches.len();
// Delete the files
for Match { path, display_path } in matches {
let path = worktree.abs_path().join(&target_path);
let display_path = target_path.clone();
match fs::remove_file(&path) {
Ok(()) => {
deleted_paths.push(display_path);
@@ -112,54 +83,16 @@ impl Tool for DeletePathTool {
}
if errors.is_empty() {
// 0 deleted paths should never happen if there were no errors;
// we already returned if matches was empty.
let answer = if deleted_paths.len() == 1 {
format!(
"Deleted {}",
deleted_paths.first().unwrap_or(&String::new())
)
} else {
// Sort to group entries in the same directory together
deleted_paths.sort();
let mut buf = format!("Deleted these {} paths:\n", deleted_paths.len());
for path in deleted_paths.iter() {
buf.push('\n');
buf.push_str(path);
}
buf
};
Task::ready(Ok(answer))
} else {
if deleted_paths.is_empty() {
Task::ready(Err(anyhow!(
"{glob:?} matched {} deleted because of {}:\n{}",
if paths_matched == 1 {
"1 path, but it was not".to_string()
} else {
format!("{} paths, but none were", paths_matched)
},
if errors.len() == 1 {
"this error".to_string()
} else {
format!("{} errors", errors.len())
},
errors.join("\n")
Task::ready(Ok(format!(
"No file or directory found at path: {}",
target_path
)))
} else {
// Sort to group entries in the same directory together
deleted_paths.sort();
Task::ready(Ok(format!(
"Deleted {} paths matching glob {glob:?}:\n{}\n\nErrors:\n{}",
deleted_paths.len(),
deleted_paths.join("\n"),
errors.join("\n")
)))
Task::ready(Ok(format!("Deleted {}", target_path)))
}
} else {
Task::ready(Err(anyhow!(errors.join("\n"))))
}
}
}

View File

@@ -1 +1 @@
Deletes all files and directories in the project which match the given glob, and returns a list of the paths that were deleted.
Deletes a specific file or directory in the project at the given path, and returns confirmation of what was deleted.

View File

@@ -0,0 +1,127 @@
use anyhow::{anyhow, Result};
use assistant_tool::Tool;
use gpui::{App, Entity, Task};
use language::{DiagnosticSeverity, OffsetRangeExt};
use language_model::LanguageModelRequestMessage;
use project::Project;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use std::{
fmt::Write,
path::{Path, PathBuf},
sync::Arc,
};
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
pub struct DiagnosticsToolInput {
/// The path to get diagnostics for. If not provided, returns a project-wide summary.
///
/// 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 wanna access diagnostics for `dolor.txt` in `ipsum`, you should use the path `ipsum/dolor.txt`.
/// </example>
pub path: Option<PathBuf>,
}
pub struct DiagnosticsTool;
impl Tool for DiagnosticsTool {
fn name(&self) -> String {
"diagnostics".into()
}
fn description(&self) -> String {
include_str!("./diagnostics_tool/description.md").into()
}
fn input_schema(&self) -> serde_json::Value {
let schema = schemars::schema_for!(DiagnosticsToolInput);
serde_json::to_value(&schema).unwrap()
}
fn run(
self: Arc<Self>,
input: serde_json::Value,
_messages: &[LanguageModelRequestMessage],
project: Entity<Project>,
cx: &mut App,
) -> Task<Result<String>> {
let input = match serde_json::from_value::<DiagnosticsToolInput>(input) {
Ok(input) => input,
Err(err) => return Task::ready(Err(anyhow!(err))),
};
if let Some(path) = input.path {
let Some(project_path) = project.read(cx).find_project_path(&path, cx) else {
return Task::ready(Err(anyhow!("Could not find path in project")));
};
let buffer = project.update(cx, |project, cx| project.open_buffer(project_path, cx));
cx.spawn(|cx| async move {
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)
}
})
} else {
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;
};
has_diagnostics = true;
output.push_str(&format!(
"{}: {} error(s), {} warning(s)\n",
Path::new(worktree.read(cx).root_name())
.join(project_path.path)
.display(),
summary.error_count,
summary.warning_count
));
}
}
if has_diagnostics {
Task::ready(Ok(output))
} else {
Task::ready(Ok("No errors or warnings found in the project.".to_string()))
}
}
}
}

View File

@@ -0,0 +1,16 @@
Get errors and warnings for the project or a specific file.
This tool can be invoked after a series of edits to determine if further edits are necessary, or if the user asks to fix errors or warnings in their codebase.
When a path is provided, shows all diagnostics for that specific file.
When no path is provided, shows a summary of error and warning counts for all files in the project.
<example>
To get diagnostics for a specific file:
{
"path": "src/main.rs"
}
To get a project-wide diagnostic summary:
{}
</example>

View File

@@ -1,4 +1,5 @@
mod edit_action;
pub mod log;
use anyhow::{anyhow, Context, Result};
use assistant_tool::Tool;
@@ -9,25 +10,50 @@ use gpui::{App, Entity, Task};
use language_model::{
LanguageModelRegistry, LanguageModelRequest, LanguageModelRequestMessage, Role,
};
use project::{Project, ProjectPath};
use log::{EditToolLog, EditToolRequestId};
use project::Project;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use std::fmt::Write;
use std::sync::Arc;
use util::ResultExt;
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
pub struct EditFilesToolInput {
/// High-level edit instructions. These will be interpreted by a smaller model,
/// so explain the edits you want that model to make and to which files need changing.
/// The description should be concise and clear. We will show this description to the user
/// as well.
/// High-level edit instructions. These will be interpreted by a smaller
/// model, so explain the changes you want that model to make and which
/// file paths need changing.
///
/// The description should be concise and clear. We will show this
/// description to the user as well.
///
/// WARNING: When specifying which file paths need changing, you MUST
/// start each path with one of the project's root directories.
///
/// WARNING: NEVER include code blocks or snippets in edit instructions.
/// Only provide natural language descriptions of the changes needed! The tool will
/// reject any instructions that contain code blocks or snippets.
///
/// The following examples assume we have two root directories in the project:
/// - root-1
/// - root-2
///
/// <example>
/// If you want to rename a function you can say "Rename the function 'foo' to 'bar'".
/// If you want to introduce a new quit function to kill the process, your
/// instructions should be: "Add a new `quit` function to
/// `root-1/src/main.rs` to kill the process".
///
/// Notice how the file path starts with root-1. Without that, the path
/// would be ambiguous and the call would fail!
/// </example>
///
/// <example>
/// If you want to add a new function you can say "Add a new method to the `User` struct that prints the age".
/// If you want to change documentation to always start with a capital
/// letter, your instructions should be: "In `root-2/db.js`,
/// `root-2/inMemory.js` and `root-2/sql.js`, change all the documentation
/// to start with a capital letter".
///
/// Notice how we never specify code snippets in the instructions!
/// </example>
pub edit_instructions: String,
}
@@ -60,6 +86,45 @@ impl Tool for EditFilesTool {
Err(err) => return Task::ready(Err(anyhow!(err))),
};
match EditToolLog::try_global(cx) {
Some(log) => {
let req_id = log.update(cx, |log, cx| {
log.new_request(input.edit_instructions.clone(), cx)
});
let task =
EditFilesTool::run(input, messages, project, Some((log.clone(), req_id)), cx);
cx.spawn(|mut cx| async move {
let result = task.await;
let str_result = match &result {
Ok(out) => Ok(out.clone()),
Err(err) => Err(err.to_string()),
};
log.update(&mut cx, |log, cx| {
log.set_tool_output(req_id, str_result, cx)
})
.log_err();
result
})
}
None => EditFilesTool::run(input, messages, project, None, cx),
}
}
}
impl EditFilesTool {
fn run(
input: EditFilesToolInput,
messages: &[LanguageModelRequestMessage],
project: Entity<Project>,
log: Option<(Entity<EditToolLog>, EditToolRequestId)>,
cx: &mut App,
) -> Task<Result<String>> {
let model_registry = LanguageModelRegistry::read_global(cx);
let Some(model) = model_registry.editor_model() else {
return Task::ready(Err(anyhow!("No editor model configured")));
@@ -86,7 +151,7 @@ impl Tool for EditFilesTool {
messages,
tools: vec![],
stop: vec![],
temperature: None,
temperature: Some(0.0),
};
let mut parser = EditActionParser::new();
@@ -97,26 +162,25 @@ impl Tool for EditFilesTool {
let mut changed_buffers = HashSet::default();
let mut applied_edits = 0;
let log = log.clone();
while let Some(chunk) = chunks.stream.next().await {
for action in parser.parse_chunk(&chunk?) {
let chunk = chunk?;
let new_actions = parser.parse_chunk(&chunk);
if let Some((ref log, req_id)) = log {
log.update(&mut cx, |log, cx| {
log.push_editor_response_chunk(req_id, &chunk, &new_actions, cx)
})
.log_err();
}
for action in new_actions {
let project_path = project.read_with(&cx, |project, cx| {
let worktree_root_name = action
.file_path()
.components()
.next()
.context("Invalid path")?;
let worktree = project
.worktree_for_root_name(
&worktree_root_name.as_os_str().to_string_lossy(),
cx,
)
.context("Directory not found in project")?;
anyhow::Ok(ProjectPath {
worktree_id: worktree.read(cx).id(),
path: Arc::from(
action.file_path().strip_prefix(worktree_root_name).unwrap(),
),
})
project
.find_project_path(action.file_path(), cx)
.context("Path not found in project")
})??;
let buffer = project
@@ -126,15 +190,27 @@ impl Tool for EditFilesTool {
let diff = buffer
.read_with(&cx, |buffer, cx| {
let new_text = match action {
EditAction::Replace { old, new, .. } => {
EditAction::Replace {
file_path,
old,
new,
} => {
// TODO: Replace in background?
buffer.text().replace(&old, &new)
let text = buffer.text();
if text.contains(&old) {
text.replace(&old, &new)
} else {
return Err(anyhow!(
"Could not find search text in {}",
file_path.display()
));
}
}
EditAction::Write { content, .. } => content,
};
buffer.diff(new_text, cx)
})?
anyhow::Ok(buffer.diff(new_text, cx))
})??
.await;
let _clock =
@@ -157,7 +233,7 @@ impl Tool for EditFilesTool {
project
.update(&mut cx, |project, cx| {
if let Some(file) = buffer.read(&cx).file() {
let _ = write!(&mut answer, "{}\n\n", &file.path().display());
let _ = writeln!(&mut answer, "{}", &file.full_path(cx).display());
}
project.save_buffer(buffer, cx)

View File

@@ -1,4 +1,4 @@
Edit files in the current project.
Edit files in the current project by specifying instructions in natural language.
When using this tool, you should suggest one coherent edit that can be made to the codebase.

View File

@@ -106,7 +106,7 @@ Every *SEARCH/REPLACE block* must use this format:
7. The end of the replace block: >>>>>>> REPLACE
8. The closing fence: ```
Use the *FULL* file path, as shown to you by the user.
Use the *FULL* file path, as shown to you by the user. Make sure to include the project's root directory name at the start of the path. *NEVER* specify the absolute path of the file!
Every *SEARCH* section must *EXACTLY MATCH* the existing file content, character for character, including all comments, docstrings, etc.
If the file contains code or other data wrapped/escaped in json/xml/quotes or other containers, you need to propose edits to the literal contents of the file, including the container markup.

View File

@@ -0,0 +1,415 @@
use std::path::Path;
use collections::HashSet;
use feature_flags::FeatureFlagAppExt;
use gpui::{
actions, list, prelude::*, App, Empty, Entity, EventEmitter, FocusHandle, Focusable, Global,
ListAlignment, ListState, SharedString, Subscription, Window,
};
use release_channel::ReleaseChannel;
use settings::Settings;
use ui::prelude::*;
use workspace::{item::ItemEvent, Item, Workspace, WorkspaceId};
use super::edit_action::EditAction;
actions!(debug, [EditTool]);
pub fn init(cx: &mut App) {
if cx.is_staff() || ReleaseChannel::global(cx) == ReleaseChannel::Dev {
// Track events even before opening the log
EditToolLog::global(cx);
}
cx.observe_new(|workspace: &mut Workspace, _, _| {
workspace.register_action(|workspace, _: &EditTool, window, cx| {
let viewer = cx.new(EditToolLogViewer::new);
workspace.add_item_to_active_pane(Box::new(viewer), None, true, window, cx)
});
})
.detach();
}
pub struct GlobalEditToolLog(Entity<EditToolLog>);
impl Global for GlobalEditToolLog {}
#[derive(Default)]
pub struct EditToolLog {
requests: Vec<EditToolRequest>,
}
#[derive(Clone, Copy, Hash, Eq, PartialEq)]
pub struct EditToolRequestId(u32);
impl EditToolLog {
pub fn global(cx: &mut App) -> Entity<Self> {
match Self::try_global(cx) {
Some(entity) => entity,
None => {
let entity = cx.new(|_cx| Self::default());
cx.set_global(GlobalEditToolLog(entity.clone()));
entity
}
}
}
pub fn try_global(cx: &App) -> Option<Entity<Self>> {
cx.try_global::<GlobalEditToolLog>()
.map(|log| log.0.clone())
}
pub fn new_request(
&mut self,
instructions: String,
cx: &mut Context<Self>,
) -> EditToolRequestId {
let id = EditToolRequestId(self.requests.len() as u32);
self.requests.push(EditToolRequest {
id,
instructions,
editor_response: None,
tool_output: None,
parsed_edits: Vec::new(),
});
cx.emit(EditToolLogEvent::Inserted);
id
}
pub fn push_editor_response_chunk(
&mut self,
id: EditToolRequestId,
chunk: &str,
new_actions: &[EditAction],
cx: &mut Context<Self>,
) {
if let Some(request) = self.requests.get_mut(id.0 as usize) {
match &mut request.editor_response {
None => {
request.editor_response = Some(chunk.to_string());
}
Some(response) => {
response.push_str(chunk);
}
}
request.parsed_edits.extend(new_actions.iter().cloned());
cx.emit(EditToolLogEvent::Updated);
}
}
pub fn set_tool_output(
&mut self,
id: EditToolRequestId,
tool_output: Result<String, String>,
cx: &mut Context<Self>,
) {
if let Some(request) = self.requests.get_mut(id.0 as usize) {
request.tool_output = Some(tool_output);
cx.emit(EditToolLogEvent::Updated);
}
}
}
enum EditToolLogEvent {
Inserted,
Updated,
}
impl EventEmitter<EditToolLogEvent> for EditToolLog {}
pub struct EditToolRequest {
id: EditToolRequestId,
instructions: String,
// we don't use a result here because the error might have occurred after we got a response
editor_response: Option<String>,
parsed_edits: Vec<EditAction>,
tool_output: Option<Result<String, String>>,
}
pub struct EditToolLogViewer {
focus_handle: FocusHandle,
log: Entity<EditToolLog>,
list_state: ListState,
expanded_edits: HashSet<(EditToolRequestId, usize)>,
_subscription: Subscription,
}
impl EditToolLogViewer {
pub fn new(cx: &mut Context<Self>) -> Self {
let log = EditToolLog::global(cx);
let subscription = cx.subscribe(&log, Self::handle_log_event);
Self {
focus_handle: cx.focus_handle(),
log: log.clone(),
list_state: ListState::new(
log.read(cx).requests.len(),
ListAlignment::Bottom,
px(1024.),
{
let this = cx.entity().downgrade();
move |ix, window: &mut Window, cx: &mut App| {
this.update(cx, |this, cx| this.render_request(ix, window, cx))
.unwrap()
}
},
),
expanded_edits: HashSet::default(),
_subscription: subscription,
}
}
fn handle_log_event(
&mut self,
_: Entity<EditToolLog>,
event: &EditToolLogEvent,
cx: &mut Context<Self>,
) {
match event {
EditToolLogEvent::Inserted => {
let count = self.list_state.item_count();
self.list_state.splice(count..count, 1);
}
EditToolLogEvent::Updated => {}
}
cx.notify();
}
fn render_request(
&self,
index: usize,
_window: &mut Window,
cx: &mut Context<Self>,
) -> AnyElement {
let requests = &self.log.read(cx).requests;
let request = &requests[index];
v_flex()
.gap_3()
.child(Self::render_section(IconName::ArrowRight, "Tool Input"))
.child(request.instructions.clone())
.py_5()
.when(index + 1 < requests.len(), |element| {
element
.border_b_1()
.border_color(cx.theme().colors().border)
})
.map(|parent| match &request.editor_response {
None => {
if request.tool_output.is_none() {
parent.child("...")
} else {
parent
}
}
Some(response) => parent
.child(Self::render_section(
IconName::ZedAssistant,
"Editor Response",
))
.child(Label::new(response.clone()).buffer_font(cx)),
})
.when(!request.parsed_edits.is_empty(), |parent| {
parent
.child(Self::render_section(IconName::Microscope, "Parsed Edits"))
.child(
v_flex()
.gap_2()
.children(request.parsed_edits.iter().enumerate().map(
|(index, edit)| {
self.render_edit_action(edit, request.id, index, cx)
},
)),
)
})
.when_some(request.tool_output.as_ref(), |parent, output| {
parent
.child(Self::render_section(IconName::ArrowLeft, "Tool Output"))
.child(match output {
Ok(output) => Label::new(output.clone()).color(Color::Success),
Err(error) => Label::new(error.clone()).color(Color::Error),
})
})
.into_any()
}
fn render_section(icon: IconName, title: &'static str) -> AnyElement {
h_flex()
.gap_1()
.child(Icon::new(icon).color(Color::Muted))
.child(Label::new(title).size(LabelSize::Small).color(Color::Muted))
.into_any()
}
fn render_edit_action(
&self,
edit_action: &EditAction,
request_id: EditToolRequestId,
index: usize,
cx: &Context<Self>,
) -> AnyElement {
let expanded_id = (request_id, index);
match edit_action {
EditAction::Replace {
file_path,
old,
new,
} => self
.render_edit_action_container(
expanded_id,
&file_path,
[
Self::render_block(IconName::MagnifyingGlass, "Search", old.clone(), cx)
.border_r_1()
.border_color(cx.theme().colors().border)
.into_any(),
Self::render_block(IconName::Replace, "Replace", new.clone(), cx)
.into_any(),
],
cx,
)
.into_any(),
EditAction::Write { file_path, content } => self
.render_edit_action_container(
expanded_id,
&file_path,
[
Self::render_block(IconName::Pencil, "Write", content.clone(), cx)
.into_any(),
],
cx,
)
.into_any(),
}
}
fn render_edit_action_container(
&self,
expanded_id: (EditToolRequestId, usize),
file_path: &Path,
content: impl IntoIterator<Item = AnyElement>,
cx: &Context<Self>,
) -> AnyElement {
let is_expanded = self.expanded_edits.contains(&expanded_id);
v_flex()
.child(
h_flex()
.bg(cx.theme().colors().element_background)
.border_1()
.border_color(cx.theme().colors().border)
.rounded_t_md()
.when(!is_expanded, |el| el.rounded_b_md())
.py_1()
.px_2()
.gap_1()
.child(
ui::Disclosure::new(ElementId::Integer(expanded_id.1), is_expanded)
.on_click(cx.listener(move |this, _ev, _window, cx| {
if is_expanded {
this.expanded_edits.remove(&expanded_id);
} else {
this.expanded_edits.insert(expanded_id);
}
cx.notify();
})),
)
.child(Label::new(file_path.display().to_string()).size(LabelSize::Small)),
)
.child(if is_expanded {
h_flex()
.border_1()
.border_t_0()
.border_color(cx.theme().colors().border)
.rounded_b_md()
.children(content)
.into_any()
} else {
Empty.into_any()
})
.into_any()
}
fn render_block(icon: IconName, title: &'static str, content: String, cx: &App) -> Div {
v_flex()
.p_1()
.gap_1()
.flex_1()
.h_full()
.child(
h_flex()
.gap_1()
.child(Icon::new(icon).color(Color::Muted))
.child(Label::new(title).size(LabelSize::Small).color(Color::Muted)),
)
.font(theme::ThemeSettings::get_global(cx).buffer_font.clone())
.text_sm()
.child(content)
.child(div().flex_1())
}
}
impl EventEmitter<()> for EditToolLogViewer {}
impl Focusable for EditToolLogViewer {
fn focus_handle(&self, _: &App) -> gpui::FocusHandle {
self.focus_handle.clone()
}
}
impl Item for EditToolLogViewer {
type Event = ();
fn to_item_events(_: &Self::Event, _: impl FnMut(ItemEvent)) {}
fn tab_content_text(&self, _window: &Window, _cx: &App) -> Option<SharedString> {
Some("Edit Tool Log".into())
}
fn telemetry_event_text(&self) -> Option<&'static str> {
None
}
fn clone_on_split(
&self,
_workspace_id: Option<WorkspaceId>,
_window: &mut Window,
cx: &mut Context<Self>,
) -> Option<Entity<Self>>
where
Self: Sized,
{
Some(cx.new(Self::new))
}
}
impl Render for EditToolLogViewer {
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
if self.list_state.item_count() == 0 {
return v_flex()
.justify_center()
.size_full()
.gap_1()
.bg(cx.theme().colors().editor_background)
.text_center()
.text_lg()
.child("No requests yet")
.child(
div()
.text_ui(cx)
.child("Go ask the assistant to perform some edits"),
);
}
v_flex()
.p_4()
.bg(cx.theme().colors().editor_background)
.size_full()
.child(list(self.list_state.clone()).flex_grow())
}
}

View File

@@ -12,10 +12,10 @@ pub struct ListDirectoryToolInput {
/// The relative path of the directory to list.
///
/// This path should never be absolute, and the first component
/// of the path should always be a top-level directory in a project.
/// of the path should always be a root directory in a project.
///
/// <example>
/// If the project has the following top-level directories:
/// If the project has the following root directories:
///
/// - directory1
/// - directory2
@@ -24,7 +24,7 @@ pub struct ListDirectoryToolInput {
/// </example>
///
/// <example>
/// If the project has the following top-level directories:
/// If the project has the following root directories:
///
/// - foo
/// - bar
@@ -62,27 +62,37 @@ impl Tool for ListDirectoryTool {
Err(err) => return Task::ready(Err(anyhow!(err))),
};
let Some(worktree_root_name) = input.path.components().next() else {
return Task::ready(Err(anyhow!("Invalid path")));
let Some(project_path) = project.read(cx).find_project_path(&input.path, cx) else {
return Task::ready(Err(anyhow!("Path not found in project")));
};
let Some(worktree) = project
.read(cx)
.worktree_for_root_name(&worktree_root_name.as_os_str().to_string_lossy(), cx)
.worktree_for_id(project_path.worktree_id, cx)
else {
return Task::ready(Err(anyhow!("Directory not found in the project")));
return Task::ready(Err(anyhow!("Worktree not found")));
};
let path = input.path.strip_prefix(worktree_root_name).unwrap();
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.display())));
};
if !entry.is_dir() {
return Task::ready(Err(anyhow!("{} is a file.", input.path.display())));
}
let mut output = String::new();
for entry in worktree.read(cx).child_entries(path) {
for entry in worktree.child_entries(&project_path.path) {
writeln!(
output,
"{}",
Path::new(worktree_root_name.as_os_str())
.join(&entry.path)
.display(),
Path::new(worktree.root_name()).join(&entry.path).display(),
)
.unwrap();
}
if output.is_empty() {
return Task::ready(Ok(format!("{} is empty.", input.path.display())));
}
Task::ready(Ok(output))
}
}

View File

@@ -13,7 +13,7 @@ pub struct PathSearchToolInput {
/// The glob to search all project paths for.
///
/// <example>
/// If the project has the following top-level directories:
/// If the project has the following root directories:
///
/// - directory1/a/something.txt
/// - directory2/a/things.txt

View File

@@ -5,7 +5,7 @@ use anyhow::{anyhow, Result};
use assistant_tool::Tool;
use gpui::{App, Entity, Task};
use language_model::LanguageModelRequestMessage;
use project::{Project, ProjectPath};
use project::Project;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
@@ -14,10 +14,10 @@ pub struct ReadFileToolInput {
/// The relative path of the file to read.
///
/// This path should never be absolute, and the first component
/// of the path should always be a top-level directory in a project.
/// of the path should always be a root directory in a project.
///
/// <example>
/// If the project has the following top-level directories:
/// If the project has the following root directories:
///
/// - directory1
/// - directory2
@@ -56,18 +56,8 @@ impl Tool for ReadFileTool {
Err(err) => return Task::ready(Err(anyhow!(err))),
};
let Some(worktree_root_name) = input.path.components().next() else {
return Task::ready(Err(anyhow!("Invalid path")));
};
let Some(worktree) = project
.read(cx)
.worktree_for_root_name(&worktree_root_name.as_os_str().to_string_lossy(), cx)
else {
return Task::ready(Err(anyhow!("Directory not found in the project")));
};
let project_path = ProjectPath {
worktree_id: worktree.read(cx).id(),
path: Arc::from(input.path.strip_prefix(worktree_root_name).unwrap()),
let Some(project_path) = project.read(cx).find_project_path(&input.path, cx) else {
return Task::ready(Err(anyhow!("Path not found in project")));
};
cx.spawn(|cx| async move {
let buffer = cx

View File

@@ -1358,10 +1358,7 @@ impl Editor {
project,
blink_manager: blink_manager.clone(),
show_local_selections: true,
show_scrollbars: match mode {
EditorMode::AutoHeight { .. } | EditorMode::SingleLine { .. } => false,
EditorMode::Full => true,
},
show_scrollbars: true,
mode,
show_breadcrumbs: EditorSettings::get_global(cx).toolbar.breadcrumbs,
show_gutter: mode == EditorMode::Full,
@@ -16779,8 +16776,7 @@ fn wrap_with_prefix(
is_whitespace,
} in tokenizer
{
if (current_line_len + grapheme_len) > wrap_column && (current_line_len != line_prefix_len)
{
if current_line_len + grapheme_len > wrap_column && current_line_len != line_prefix_len {
wrapped_text.push_str(current_line.trim_end());
wrapped_text.push('\n');
current_line.truncate(line_prefix.len());

View File

@@ -45,7 +45,7 @@ use inline_completion::Direction;
use itertools::Itertools;
use language::{
language_settings::{
self, IndentGuideBackgroundColoring, IndentGuideColoring, IndentGuideSettings,
IndentGuideBackgroundColoring, IndentGuideColoring, IndentGuideSettings,
ShowWhitespaceSetting,
},
ChunkRendererContext,
@@ -1308,8 +1308,7 @@ impl EditorElement {
);
let scrollbar_settings = EditorSettings::get_global(cx).scrollbar;
let editor_show_scrollbars = self.editor.read(cx).show_scrollbars;
let show_scrollbars = editor_show_scrollbars
let show_scrollbars = self.editor.read(cx).show_scrollbars
&& match scrollbar_settings.show {
ShowScrollbar::Auto => {
let editor = self.editor.read(cx);
@@ -1339,11 +1338,10 @@ impl EditorElement {
ShowScrollbar::Always => true,
ShowScrollbar::Never => false,
};
let axes: AxisPair<bool> = scrollbar_settings.axes.into();
if snapshot.mode == EditorMode::Full
|| matches!(snapshot.mode, EditorMode::AutoHeight { .. }) && editor_show_scrollbars
{
if snapshot.mode != EditorMode::Full {
return axis_pair(None, None);
}
@@ -7703,7 +7701,7 @@ struct ColoredRange<T> {
color: Hsla,
}
#[derive(Clone, Debug)]
#[derive(Clone)]
struct ScrollbarLayout {
hitbox: Hitbox,
visible_range: Range<f32>,
@@ -8278,15 +8276,8 @@ fn compute_auto_height_layout(
let overscroll = size(em_width, px(0.));
let editor_width = text_width - gutter_dimensions.margin - overscroll.width - em_width;
let soft_wrap_disabled = editor
.soft_wrap_mode_override
.is_some_and(|soft_wrap| matches!(soft_wrap, language_settings::SoftWrap::None));
if !soft_wrap_disabled {
if editor.set_wrap_width(Some(editor_width), cx) {
snapshot = editor.snapshot(window, cx);
}
if editor.set_wrap_width(Some(editor_width), cx) {
snapshot = editor.snapshot(window, cx);
}
let scroll_height = Pixels::from(snapshot.max_point().row().next_row().0) * line_height;

View File

@@ -35,7 +35,6 @@ use gpui::{
Transformation, UniformListScrollHandle, WeakEntity,
};
use itertools::Itertools;
use language::language_settings::SoftWrap;
use language::{Buffer, File};
use language_model::{
LanguageModel, LanguageModelRegistry, LanguageModelRequest, LanguageModelRequestMessage, Role,
@@ -369,8 +368,6 @@ pub(crate) fn commit_message_editor(
commit_editor.set_show_wrap_guides(false, cx);
commit_editor.set_show_indent_guides(false, cx);
commit_editor.set_hard_wrap(Some(72), cx);
commit_editor.set_soft_wrap_mode(SoftWrap::None, cx);
commit_editor.set_show_scrollbars(true, cx);
let placeholder = placeholder.unwrap_or("Enter commit message");
commit_editor.set_placeholder_text(placeholder, cx);
commit_editor

View File

@@ -92,6 +92,8 @@ extern "C" {
#[ctor]
unsafe fn build_classes() {
let _: () = msg_send![class!(NSWindow), setAllowsAutomaticWindowTabbing: NO];
WINDOW_CLASS = build_window_class("GPUIWindow", class!(NSWindow));
PANEL_CLASS = build_window_class("GPUIPanel", class!(NSPanel));
VIEW_CLASS = {

View File

@@ -7,13 +7,24 @@ use handlebars::{Handlebars, RenderError};
use language::{BufferSnapshot, LanguageName, Point};
use parking_lot::Mutex;
use serde::Serialize;
use std::{ops::Range, path::PathBuf, sync::Arc, time::Duration};
use std::{
ops::Range,
path::{Path, PathBuf},
sync::Arc,
time::Duration,
};
use text::LineEnding;
use util::ResultExt;
#[derive(Serialize)]
pub struct AssistantSystemPromptContext {
pub worktree_root_names: Vec<String>,
pub worktrees: Vec<AssistantSystemPromptWorktree>,
}
#[derive(Serialize)]
pub struct AssistantSystemPromptWorktree {
pub root_name: String,
pub abs_path: Arc<Path>,
}
#[derive(Serialize)]
@@ -223,11 +234,9 @@ impl PromptBuilder {
pub fn generate_assistant_system_prompt(
&self,
worktree_root_names: Vec<String>,
worktrees: Vec<AssistantSystemPromptWorktree>,
) -> Result<String, RenderError> {
let prompt = AssistantSystemPromptContext {
worktree_root_names,
};
let prompt = AssistantSystemPromptContext { worktrees };
self.handlebars
.lock()
.render("assistant_system_prompt", &prompt)

View File

@@ -22,10 +22,24 @@ For more information, see:
## Edit Predictions {#edit-predictions}
Zed has built-in support for predicting multiple edits at a time via its [Zeta model](https://huggingface.co/zed-industries/zeta). Clicking "Introducing: Edit Prediction" on the top right will open a brief prompt setting up this feature.
Zed has built-in support for predicting multiple edits at a time [via Zeta](https://huggingface.co/zed-industries/zeta), Zed's open-source and open-data model.
Edit predictions appear as you type, and most of the time, you can accept them by pressing `tab`.
### Configuring Zeta
Zed's Edit Prediction was initially introduced via a banner on the title bar.
Clicking on it would take you to a modal with a button ("Enable Edit Prediction") that sets `zed` as your `edit_prediction_provider`.
![Onboarding banner and modal](https://zed.dev/img/edit-prediction/docs.webp)
But, if you haven't come across the banner, start using Zed's Edit Prediction by adding this to your settings:
```json
"features": {
"edit_prediction_provider": "zed"
},
```
### Conflict With Other `tab` Actions {#edit-predictions-conflict}
By default, when `tab` would normally perform a different action, Zed requires a modifier key to accept predictions: