Detailed changes
@@ -3202,7 +3202,6 @@ dependencies = [
"rustc-hash 2.1.1",
"schemars 1.0.4",
"serde",
- "serde_json",
"strum 0.27.2",
]
@@ -8867,6 +8866,7 @@ dependencies = [
"open_router",
"parking_lot",
"proto",
+ "schemars 1.0.4",
"serde",
"serde_json",
"settings",
@@ -21685,6 +21685,7 @@ dependencies = [
"serde",
"serde_json",
"settings",
+ "smol",
"thiserror 2.0.17",
"util",
"uuid",
@@ -21702,6 +21703,7 @@ dependencies = [
"clap",
"client",
"cloud_llm_client",
+ "cloud_zeta2_prompt",
"collections",
"edit_prediction_context",
"editor",
@@ -21715,7 +21717,6 @@ dependencies = [
"ordered-float 2.10.1",
"pretty_assertions",
"project",
- "regex-syntax",
"serde",
"serde_json",
"settings",
@@ -455,6 +455,7 @@
"<": "vim::Outdent",
"=": "vim::AutoIndent",
"d": "vim::HelixDelete",
+ "alt-d": "editor::Delete", // Delete selection, without yanking
"c": "vim::HelixSubstitute",
"alt-c": "vim::HelixSubstituteNoYank",
@@ -6,7 +6,6 @@ mod native_agent_server;
pub mod outline;
mod templates;
mod thread;
-mod tool_schema;
mod tools;
#[cfg(test)]
@@ -1,6 +1,6 @@
use anyhow::Result;
use gpui::{AsyncApp, Entity};
-use language::{Buffer, OutlineItem, ParseStatus};
+use language::{Buffer, OutlineItem};
use regex::Regex;
use std::fmt::Write;
use text::Point;
@@ -30,10 +30,9 @@ pub async fn get_buffer_content_or_outline(
if file_size > AUTO_OUTLINE_SIZE {
// For large files, use outline instead of full content
// Wait until the buffer has been fully parsed, so we can read its outline
- let mut parse_status = buffer.read_with(cx, |buffer, _| buffer.parse_status())?;
- while *parse_status.borrow() != ParseStatus::Idle {
- parse_status.changed().await?;
- }
+ buffer
+ .read_with(cx, |buffer, _| buffer.parsing_idle())?
+ .await;
let outline_items = buffer.read_with(cx, |buffer, _| {
let snapshot = buffer.snapshot();
@@ -2139,7 +2139,7 @@ where
/// Returns the JSON schema that describes the tool's input.
fn input_schema(format: LanguageModelToolSchemaFormat) -> Schema {
- crate::tool_schema::root_schema_for::<Self::Input>(format)
+ language_model::tool_schema::root_schema_for::<Self::Input>(format)
}
/// Some tools rely on a provider for the underlying billing or other reasons.
@@ -2226,7 +2226,7 @@ where
fn input_schema(&self, format: LanguageModelToolSchemaFormat) -> Result<serde_json::Value> {
let mut json = serde_json::to_value(T::input_schema(format))?;
- crate::tool_schema::adapt_schema_to_format(&mut json, format)?;
+ language_model::tool_schema::adapt_schema_to_format(&mut json, format)?;
Ok(json)
}
@@ -165,7 +165,7 @@ impl AnyAgentTool for ContextServerTool {
format: language_model::LanguageModelToolSchemaFormat,
) -> Result<serde_json::Value> {
let mut schema = self.tool.input_schema.clone();
- crate::tool_schema::adapt_schema_to_format(&mut schema, format)?;
+ language_model::tool_schema::adapt_schema_to_format(&mut schema, format)?;
Ok(match schema {
serde_json::Value::Null => {
serde_json::json!({ "type": "object", "properties": [] })
@@ -646,16 +646,14 @@ impl ContextPickerCompletionProvider {
cx: &mut App,
) -> Vec<ContextPickerEntry> {
let embedded_context = self.prompt_capabilities.borrow().embedded_context;
- let mut entries = if embedded_context {
- vec![
- ContextPickerEntry::Mode(ContextPickerMode::File),
- ContextPickerEntry::Mode(ContextPickerMode::Symbol),
- ContextPickerEntry::Mode(ContextPickerMode::Thread),
- ]
- } else {
- // File is always available, but we don't need a mode entry
- vec![]
- };
+ let mut entries = vec![
+ ContextPickerEntry::Mode(ContextPickerMode::File),
+ ContextPickerEntry::Mode(ContextPickerMode::Symbol),
+ ];
+
+ if embedded_context {
+ entries.push(ContextPickerEntry::Mode(ContextPickerMode::Thread));
+ }
let has_selection = workspace
.read(cx)
@@ -356,7 +356,7 @@ impl MessageEditor {
let task = match mention_uri.clone() {
MentionUri::Fetch { url } => self.confirm_mention_for_fetch(url, cx),
- MentionUri::Directory { .. } => Task::ready(Ok(Mention::UriOnly)),
+ MentionUri::Directory { .. } => Task::ready(Ok(Mention::Link)),
MentionUri::Thread { id, .. } => self.confirm_mention_for_thread(id, cx),
MentionUri::TextThread { path, .. } => self.confirm_mention_for_text_thread(path, cx),
MentionUri::File { abs_path } => self.confirm_mention_for_file(abs_path, cx),
@@ -373,7 +373,6 @@ impl MessageEditor {
)))
}
MentionUri::Selection { .. } => {
- // Handled elsewhere
debug_panic!("unexpected selection URI");
Task::ready(Err(anyhow!("unexpected selection URI")))
}
@@ -704,13 +703,11 @@ impl MessageEditor {
return Task::ready(Err(err));
}
- let contents = self.mention_set.contents(
- &self.prompt_capabilities.borrow(),
- full_mention_content,
- self.project.clone(),
- cx,
- );
+ let contents = self
+ .mention_set
+ .contents(full_mention_content, self.project.clone(), cx);
let editor = self.editor.clone();
+ let supports_embedded_context = self.prompt_capabilities.borrow().embedded_context;
cx.spawn(async move |_, cx| {
let contents = contents.await?;
@@ -741,18 +738,32 @@ impl MessageEditor {
tracked_buffers,
} => {
all_tracked_buffers.extend(tracked_buffers.iter().cloned());
- acp::ContentBlock::Resource(acp::EmbeddedResource {
- annotations: None,
- resource: acp::EmbeddedResourceResource::TextResourceContents(
- acp::TextResourceContents {
- mime_type: None,
- text: content.clone(),
- uri: uri.to_uri().to_string(),
- meta: None,
- },
- ),
- meta: None,
- })
+ if supports_embedded_context {
+ acp::ContentBlock::Resource(acp::EmbeddedResource {
+ annotations: None,
+ resource:
+ acp::EmbeddedResourceResource::TextResourceContents(
+ acp::TextResourceContents {
+ mime_type: None,
+ text: content.clone(),
+ uri: uri.to_uri().to_string(),
+ meta: None,
+ },
+ ),
+ meta: None,
+ })
+ } else {
+ acp::ContentBlock::ResourceLink(acp::ResourceLink {
+ name: uri.name(),
+ uri: uri.to_uri().to_string(),
+ annotations: None,
+ description: None,
+ mime_type: None,
+ size: None,
+ title: None,
+ meta: None,
+ })
+ }
}
Mention::Image(mention_image) => {
let uri = match uri {
@@ -774,18 +785,16 @@ impl MessageEditor {
meta: None,
})
}
- Mention::UriOnly => {
- acp::ContentBlock::ResourceLink(acp::ResourceLink {
- name: uri.name(),
- uri: uri.to_uri().to_string(),
- annotations: None,
- description: None,
- mime_type: None,
- size: None,
- title: None,
- meta: None,
- })
- }
+ Mention::Link => acp::ContentBlock::ResourceLink(acp::ResourceLink {
+ name: uri.name(),
+ uri: uri.to_uri().to_string(),
+ annotations: None,
+ description: None,
+ mime_type: None,
+ size: None,
+ title: None,
+ meta: None,
+ }),
};
chunks.push(chunk);
ix = crease_range.end;
@@ -1114,7 +1123,7 @@ impl MessageEditor {
let start = text.len();
write!(&mut text, "{}", mention_uri.as_link()).ok();
let end = text.len();
- mentions.push((start..end, mention_uri, Mention::UriOnly));
+ mentions.push((start..end, mention_uri, Mention::Link));
}
}
acp::ContentBlock::Image(acp::ImageContent {
@@ -1520,7 +1529,7 @@ pub enum Mention {
tracked_buffers: Vec<Entity<Buffer>>,
},
Image(MentionImage),
- UriOnly,
+ Link,
}
#[derive(Clone, Debug, Eq, PartialEq)]
@@ -1537,21 +1546,10 @@ pub struct MentionSet {
impl MentionSet {
fn contents(
&self,
- prompt_capabilities: &acp::PromptCapabilities,
full_mention_content: bool,
project: Entity<Project>,
cx: &mut App,
) -> Task<Result<HashMap<CreaseId, (MentionUri, Mention)>>> {
- if !prompt_capabilities.embedded_context {
- let mentions = self
- .mentions
- .iter()
- .map(|(crease_id, (uri, _))| (*crease_id, (uri.clone(), Mention::UriOnly)))
- .collect();
-
- return Task::ready(Ok(mentions));
- }
-
let mentions = self.mentions.clone();
cx.spawn(async move |cx| {
let mut contents = HashMap::default();
@@ -2201,6 +2199,8 @@ mod tests {
format!("seven.txt b{slash}"),
format!("six.txt b{slash}"),
format!("five.txt b{slash}"),
+ "Files & Directories".into(),
+ "Symbols".into()
]
);
editor.set_text("", window, cx);
@@ -2285,21 +2285,11 @@ mod tests {
assert_eq!(fold_ranges(editor, cx).len(), 1);
});
- let all_prompt_capabilities = acp::PromptCapabilities {
- image: true,
- audio: true,
- embedded_context: true,
- meta: None,
- };
-
let contents = message_editor
.update(&mut cx, |message_editor, cx| {
- message_editor.mention_set().contents(
- &all_prompt_capabilities,
- false,
- project.clone(),
- cx,
- )
+ message_editor
+ .mention_set()
+ .contents(false, project.clone(), cx)
})
.await
.unwrap()
@@ -2317,30 +2307,6 @@ mod tests {
);
}
- let contents = message_editor
- .update(&mut cx, |message_editor, cx| {
- message_editor.mention_set().contents(
- &acp::PromptCapabilities::default(),
- false,
- project.clone(),
- cx,
- )
- })
- .await
- .unwrap()
- .into_values()
- .collect::<Vec<_>>();
-
- {
- let [(uri, Mention::UriOnly)] = contents.as_slice() else {
- panic!("Unexpected mentions");
- };
- pretty_assertions::assert_eq!(
- uri,
- &MentionUri::parse(&url_one, PathStyle::local()).unwrap()
- );
- }
-
cx.simulate_input(" ");
editor.update(&mut cx, |editor, cx| {
@@ -2376,12 +2342,9 @@ mod tests {
let contents = message_editor
.update(&mut cx, |message_editor, cx| {
- message_editor.mention_set().contents(
- &all_prompt_capabilities,
- false,
- project.clone(),
- cx,
- )
+ message_editor
+ .mention_set()
+ .contents(false, project.clone(), cx)
})
.await
.unwrap()
@@ -2502,12 +2465,9 @@ mod tests {
let contents = message_editor
.update(&mut cx, |message_editor, cx| {
- message_editor.mention_set().contents(
- &all_prompt_capabilities,
- false,
- project.clone(),
- cx,
- )
+ message_editor
+ .mention_set()
+ .contents(false, project.clone(), cx)
})
.await
.unwrap()
@@ -2553,12 +2513,9 @@ mod tests {
// Getting the message contents fails
message_editor
.update(&mut cx, |message_editor, cx| {
- message_editor.mention_set().contents(
- &all_prompt_capabilities,
- false,
- project.clone(),
- cx,
- )
+ message_editor
+ .mention_set()
+ .contents(false, project.clone(), cx)
})
.await
.expect_err("Should fail to load x.png");
@@ -2609,12 +2566,9 @@ mod tests {
// Now getting the contents succeeds, because the invalid mention was removed
let contents = message_editor
.update(&mut cx, |message_editor, cx| {
- message_editor.mention_set().contents(
- &all_prompt_capabilities,
- false,
- project.clone(),
- cx,
- )
+ message_editor
+ .mention_set()
+ .contents(false, project.clone(), cx)
})
.await
.unwrap();
@@ -2896,6 +2850,147 @@ mod tests {
);
}
+ #[gpui::test]
+ async fn test_editor_respects_embedded_context_capability(cx: &mut TestAppContext) {
+ init_test(cx);
+
+ let fs = FakeFs::new(cx.executor());
+
+ let file_content = "fn main() { println!(\"Hello, world!\"); }\n";
+
+ fs.insert_tree(
+ "/project",
+ json!({
+ "src": {
+ "main.rs": file_content,
+ }
+ }),
+ )
+ .await;
+
+ let project = Project::test(fs, [Path::new(path!("/project"))], cx).await;
+
+ let (workspace, cx) =
+ cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
+
+ let text_thread_store = cx.new(|cx| TextThreadStore::fake(project.clone(), cx));
+ let history_store = cx.new(|cx| HistoryStore::new(text_thread_store, cx));
+
+ let (message_editor, editor) = workspace.update_in(cx, |workspace, window, cx| {
+ let workspace_handle = cx.weak_entity();
+ let message_editor = cx.new(|cx| {
+ MessageEditor::new(
+ workspace_handle,
+ project.clone(),
+ history_store.clone(),
+ None,
+ Default::default(),
+ Default::default(),
+ "Test Agent".into(),
+ "Test",
+ EditorMode::AutoHeight {
+ max_lines: None,
+ min_lines: 1,
+ },
+ window,
+ cx,
+ )
+ });
+ workspace.active_pane().update(cx, |pane, cx| {
+ pane.add_item(
+ Box::new(cx.new(|_| MessageEditorItem(message_editor.clone()))),
+ true,
+ true,
+ None,
+ window,
+ cx,
+ );
+ });
+ message_editor.read(cx).focus_handle(cx).focus(window);
+ let editor = message_editor.read(cx).editor().clone();
+ (message_editor, editor)
+ });
+
+ cx.simulate_input("What is in @file main");
+
+ editor.update_in(cx, |editor, window, cx| {
+ assert!(editor.has_visible_completions_menu());
+ assert_eq!(editor.text(cx), "What is in @file main");
+ editor.confirm_completion(&editor::actions::ConfirmCompletion::default(), window, cx);
+ });
+
+ let content = message_editor
+ .update(cx, |editor, cx| editor.contents(false, cx))
+ .await
+ .unwrap()
+ .0;
+
+ let main_rs_uri = if cfg!(windows) {
+ "file:///C:/project/src/main.rs".to_string()
+ } else {
+ "file:///project/src/main.rs".to_string()
+ };
+
+ // When embedded context is `false` we should get a resource link
+ pretty_assertions::assert_eq!(
+ content,
+ vec![
+ acp::ContentBlock::Text(acp::TextContent {
+ text: "What is in ".to_string(),
+ annotations: None,
+ meta: None
+ }),
+ acp::ContentBlock::ResourceLink(acp::ResourceLink {
+ uri: main_rs_uri.clone(),
+ name: "main.rs".to_string(),
+ annotations: None,
+ meta: None,
+ description: None,
+ mime_type: None,
+ size: None,
+ title: None,
+ })
+ ]
+ );
+
+ message_editor.update(cx, |editor, _cx| {
+ editor.prompt_capabilities.replace(acp::PromptCapabilities {
+ embedded_context: true,
+ ..Default::default()
+ })
+ });
+
+ let content = message_editor
+ .update(cx, |editor, cx| editor.contents(false, cx))
+ .await
+ .unwrap()
+ .0;
+
+ // When embedded context is `true` we should get a resource
+ pretty_assertions::assert_eq!(
+ content,
+ vec![
+ acp::ContentBlock::Text(acp::TextContent {
+ text: "What is in ".to_string(),
+ annotations: None,
+ meta: None
+ }),
+ acp::ContentBlock::Resource(acp::EmbeddedResource {
+ resource: acp::EmbeddedResourceResource::TextResourceContents(
+ acp::TextResourceContents {
+ text: file_content.to_string(),
+ uri: main_rs_uri,
+ mime_type: None,
+ meta: None
+ }
+ ),
+ annotations: None,
+ meta: None
+ })
+ ]
+ );
+ }
+
#[gpui::test]
async fn test_autoscroll_after_insert_selections(cx: &mut TestAppContext) {
init_test(cx);
@@ -1047,7 +1047,7 @@ impl AgentConfiguration {
AgentIcon::Name(icon_name) => Icon::new(icon_name)
.size(IconSize::Small)
.color(Color::Muted),
- AgentIcon::Path(icon_path) => Icon::from_path(icon_path)
+ AgentIcon::Path(icon_path) => Icon::from_external_svg(icon_path)
.size(IconSize::Small)
.color(Color::Muted),
};
@@ -19,5 +19,4 @@ ordered-float.workspace = true
rustc-hash.workspace = true
schemars.workspace = true
serde.workspace = true
-serde_json.workspace = true
strum.workspace = true
@@ -3,7 +3,7 @@ use cloud_llm_client::predict_edits_v3::{self, Excerpt};
use indoc::indoc;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
-use std::{fmt::Write, sync::LazyLock};
+use std::fmt::Write;
use crate::{push_events, write_codeblock};
@@ -15,7 +15,7 @@ pub fn build_prompt(request: predict_edits_v3::PlanContextRetrievalRequest) -> R
push_events(&mut prompt, &request.events);
}
- writeln!(&mut prompt, "## Excerpt around the cursor\n")?;
+ writeln!(&mut prompt, "## Cursor context")?;
write_codeblock(
&request.excerpt_path,
&[Excerpt {
@@ -39,54 +39,56 @@ pub fn build_prompt(request: predict_edits_v3::PlanContextRetrievalRequest) -> R
#[derive(Clone, Deserialize, Serialize, JsonSchema)]
pub struct SearchToolInput {
/// An array of queries to run for gathering context relevant to the next prediction
- #[schemars(length(max = 5))]
+ #[schemars(length(max = 3))]
pub queries: Box<[SearchToolQuery]>,
}
+/// Search for relevant code by path, syntax hierarchy, and content.
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct SearchToolQuery {
- /// A glob pattern to match file paths in the codebase
+ /// 1. A glob pattern to match file paths in the codebase to search in.
pub glob: String,
- /// A regular expression to match content within the files matched by the glob pattern
- pub regex: String,
+ /// 2. Regular expressions to match syntax nodes **by their first line** and hierarchy.
+ ///
+ /// Subsequent regexes match nodes within the full content of the nodes matched by the previous regexes.
+ ///
+ /// Example: Searching for a `User` class
+ /// ["class\s+User"]
+ ///
+ /// Example: Searching for a `get_full_name` method under a `User` class
+ /// ["class\s+User", "def\sget_full_name"]
+ ///
+ /// Skip this field to match on content alone.
+ #[schemars(length(max = 3))]
+ #[serde(default)]
+ pub syntax_node: Vec<String>,
+ /// 3. An optional regular expression to match the final content that should appear in the results.
+ ///
+ /// - Content will be matched within all lines of the matched syntax nodes.
+ /// - If syntax node regexes are provided, this field can be skipped to include as much of the node itself as possible.
+ /// - If no syntax node regexes are provided, the content will be matched within the entire file.
+ pub content: Option<String>,
}
-pub static TOOL_SCHEMA: LazyLock<(serde_json::Value, String)> = LazyLock::new(|| {
- let schema = schemars::schema_for!(SearchToolInput);
-
- let description = schema
- .get("description")
- .and_then(|description| description.as_str())
- .unwrap()
- .to_string();
-
- (schema.into(), description)
-});
-
pub const TOOL_NAME: &str = "search";
const SEARCH_INSTRUCTIONS: &str = indoc! {r#"
- ## Task
+ You are part of an edit prediction system in a code editor.
+ Your role is to search for code that will serve as context for predicting the next edit.
- You are part of an edit prediction system in a code editor. Your role is to identify relevant code locations
- that will serve as context for predicting the next required edit.
-
- **Your task:**
- Analyze the user's recent edits and current cursor context
- - Use the `search` tool to find code that may be relevant for predicting the next edit
+ - Use the `search` tool to find code that is relevant for predicting the next edit
- Focus on finding:
- Code patterns that might need similar changes based on the recent edits
- Functions, variables, types, and constants referenced in the current cursor context
- Related implementations, usages, or dependencies that may require consistent updates
-
- **Important constraints:**
- - This conversation has exactly 2 turns
- - You must make ALL search queries in your first response via the `search` tool
- - All queries will be executed in parallel and results returned together
- - In the second turn, you will select the most relevant results via the `select` tool.
+ - How items defined in the cursor excerpt are used or altered
+ - You will not be able to filter results or perform subsequent queries, so keep searches as targeted as possible
+ - Use `syntax_node` parameter whenever you're looking for a particular type, class, or function
+ - Avoid using wildcard globs if you already know the file path of the content you're looking for
"#};
const TOOL_USE_REMINDER: &str = indoc! {"
--
- Use the `search` tool now
+ Analyze the user's intent in one to two sentences, then call the `search` tool.
"};
@@ -23,7 +23,7 @@ use project::{
use settings::Settings;
use std::{
any::{Any, TypeId},
- cmp::Ordering,
+ cmp::{self, Ordering},
sync::Arc,
};
use text::{Anchor, BufferSnapshot, OffsetRangeExt};
@@ -410,7 +410,7 @@ impl BufferDiagnosticsEditor {
// in the editor.
// This is done by iterating over the list of diagnostic blocks and
// determine what range does the diagnostic block span.
- let mut excerpt_ranges: Vec<ExcerptRange<Point>> = Vec::new();
+ let mut excerpt_ranges: Vec<ExcerptRange<_>> = Vec::new();
for diagnostic_block in blocks.iter() {
let excerpt_range = context_range_for_entry(
@@ -420,30 +420,43 @@ impl BufferDiagnosticsEditor {
&mut cx,
)
.await;
+ let initial_range = buffer_snapshot
+ .anchor_after(diagnostic_block.initial_range.start)
+ ..buffer_snapshot.anchor_before(diagnostic_block.initial_range.end);
- let index = excerpt_ranges
- .binary_search_by(|probe| {
+ let bin_search = |probe: &ExcerptRange<text::Anchor>| {
+ let context_start = || {
probe
.context
.start
- .cmp(&excerpt_range.start)
- .then(probe.context.end.cmp(&excerpt_range.end))
- .then(
- probe
- .primary
- .start
- .cmp(&diagnostic_block.initial_range.start),
- )
- .then(probe.primary.end.cmp(&diagnostic_block.initial_range.end))
- .then(Ordering::Greater)
- })
- .unwrap_or_else(|index| index);
+ .cmp(&excerpt_range.start, &buffer_snapshot)
+ };
+ let context_end =
+ || probe.context.end.cmp(&excerpt_range.end, &buffer_snapshot);
+ let primary_start = || {
+ probe
+ .primary
+ .start
+ .cmp(&initial_range.start, &buffer_snapshot)
+ };
+ let primary_end =
+ || probe.primary.end.cmp(&initial_range.end, &buffer_snapshot);
+ context_start()
+ .then_with(context_end)
+ .then_with(primary_start)
+ .then_with(primary_end)
+ .then(cmp::Ordering::Greater)
+ };
+
+ let index = excerpt_ranges
+ .binary_search_by(bin_search)
+ .unwrap_or_else(|i| i);
excerpt_ranges.insert(
index,
ExcerptRange {
context: excerpt_range,
- primary: diagnostic_block.initial_range.clone(),
+ primary: initial_range,
},
)
}
@@ -466,6 +479,13 @@ impl BufferDiagnosticsEditor {
buffer_diagnostics_editor
.multibuffer
.update(cx, |multibuffer, cx| {
+ let excerpt_ranges = excerpt_ranges
+ .into_iter()
+ .map(|range| ExcerptRange {
+ context: range.context.to_point(&buffer_snapshot),
+ primary: range.primary.to_point(&buffer_snapshot),
+ })
+ .collect();
multibuffer.set_excerpt_ranges_for_path(
PathKey::for_buffer(&buffer, cx),
buffer.clone(),
@@ -39,8 +39,8 @@ impl DiagnosticRenderer {
let group_id = primary.diagnostic.group_id;
let mut results = vec![];
for entry in diagnostic_group.iter() {
+ let mut markdown = Self::markdown(&entry.diagnostic);
if entry.diagnostic.is_primary {
- let mut markdown = Self::markdown(&entry.diagnostic);
let diagnostic = &primary.diagnostic;
if diagnostic.source.is_some() || diagnostic.code.is_some() {
markdown.push_str(" (");
@@ -81,21 +81,12 @@ impl DiagnosticRenderer {
diagnostics_editor: diagnostics_editor.clone(),
markdown: cx.new(|cx| Markdown::new(markdown.into(), None, None, cx)),
});
- } else if entry.range.start.row.abs_diff(primary.range.start.row) < 5 {
- let markdown = Self::markdown(&entry.diagnostic);
-
- results.push(DiagnosticBlock {
- initial_range: entry.range.clone(),
- severity: entry.diagnostic.severity,
- diagnostics_editor: diagnostics_editor.clone(),
- markdown: cx.new(|cx| Markdown::new(markdown.into(), None, None, cx)),
- });
} else {
- let mut markdown = Self::markdown(&entry.diagnostic);
- markdown.push_str(&format!(
- " ([back](file://#diagnostic-{buffer_id}-{group_id}-{primary_ix}))"
- ));
-
+ if entry.range.start.row.abs_diff(primary.range.start.row) >= 5 {
+ markdown.push_str(&format!(
+ " ([back](file://#diagnostic-{buffer_id}-{group_id}-{primary_ix}))"
+ ));
+ }
results.push(DiagnosticBlock {
initial_range: entry.range.clone(),
severity: entry.diagnostic.severity,
@@ -498,7 +498,7 @@ impl ProjectDiagnosticsEditor {
cx: &mut Context<Self>,
) -> Task<Result<()>> {
let was_empty = self.multibuffer.read(cx).is_empty();
- let buffer_snapshot = buffer.read(cx).snapshot();
+ let mut buffer_snapshot = buffer.read(cx).snapshot();
let buffer_id = buffer_snapshot.remote_id();
let max_severity = if self.include_warnings {
@@ -546,6 +546,7 @@ impl ProjectDiagnosticsEditor {
}
let mut blocks: Vec<DiagnosticBlock> = Vec::new();
+ let diagnostics_toolbar_editor = Arc::new(this.clone());
for (_, group) in grouped {
let group_severity = group.iter().map(|d| d.diagnostic.severity).min();
if group_severity.is_none_or(|s| s > max_severity) {
@@ -555,7 +556,7 @@ impl ProjectDiagnosticsEditor {
crate::diagnostic_renderer::DiagnosticRenderer::diagnostic_blocks_for_group(
group,
buffer_snapshot.remote_id(),
- Some(Arc::new(this.clone())),
+ Some(diagnostics_toolbar_editor.clone()),
cx,
)
})?;
@@ -563,7 +564,7 @@ impl ProjectDiagnosticsEditor {
blocks.extend(more);
}
- let mut excerpt_ranges: Vec<ExcerptRange<Point>> = this.update(cx, |this, cx| {
+ let mut excerpt_ranges: Vec<ExcerptRange<_>> = this.update(cx, |this, cx| {
this.multibuffer.update(cx, |multi_buffer, cx| {
let is_dirty = multi_buffer
.buffer(buffer_id)
@@ -573,10 +574,7 @@ impl ProjectDiagnosticsEditor {
RetainExcerpts::All | RetainExcerpts::Dirty => multi_buffer
.excerpts_for_buffer(buffer_id, cx)
.into_iter()
- .map(|(_, range)| ExcerptRange {
- context: range.context.to_point(&buffer_snapshot),
- primary: range.primary.to_point(&buffer_snapshot),
- })
+ .map(|(_, range)| range)
.collect(),
}
})
@@ -591,24 +589,41 @@ impl ProjectDiagnosticsEditor {
cx,
)
.await;
+ buffer_snapshot = cx.update(|_, cx| buffer.read(cx).snapshot())?;
+ let initial_range = buffer_snapshot.anchor_after(b.initial_range.start)
+ ..buffer_snapshot.anchor_before(b.initial_range.end);
- let i = excerpt_ranges
- .binary_search_by(|probe| {
+ let bin_search = |probe: &ExcerptRange<text::Anchor>| {
+ let context_start = || {
probe
.context
.start
- .cmp(&excerpt_range.start)
- .then(probe.context.end.cmp(&excerpt_range.end))
- .then(probe.primary.start.cmp(&b.initial_range.start))
- .then(probe.primary.end.cmp(&b.initial_range.end))
- .then(cmp::Ordering::Greater)
- })
+ .cmp(&excerpt_range.start, &buffer_snapshot)
+ };
+ let context_end =
+ || probe.context.end.cmp(&excerpt_range.end, &buffer_snapshot);
+ let primary_start = || {
+ probe
+ .primary
+ .start
+ .cmp(&initial_range.start, &buffer_snapshot)
+ };
+ let primary_end =
+ || probe.primary.end.cmp(&initial_range.end, &buffer_snapshot);
+ context_start()
+ .then_with(context_end)
+ .then_with(primary_start)
+ .then_with(primary_end)
+ .then(cmp::Ordering::Greater)
+ };
+ let i = excerpt_ranges
+ .binary_search_by(bin_search)
.unwrap_or_else(|i| i);
excerpt_ranges.insert(
i,
ExcerptRange {
context: excerpt_range,
- primary: b.initial_range.clone(),
+ primary: initial_range,
},
);
result_blocks.insert(i, Some(b));
@@ -623,6 +638,13 @@ impl ProjectDiagnosticsEditor {
})
}
let (anchor_ranges, _) = this.multibuffer.update(cx, |multi_buffer, cx| {
+ let excerpt_ranges = excerpt_ranges
+ .into_iter()
+ .map(|range| ExcerptRange {
+ context: range.context.to_point(&buffer_snapshot),
+ primary: range.primary.to_point(&buffer_snapshot),
+ })
+ .collect();
multi_buffer.set_excerpt_ranges_for_path(
PathKey::for_buffer(&buffer, cx),
buffer.clone(),
@@ -968,8 +990,8 @@ async fn context_range_for_entry(
context: u32,
snapshot: BufferSnapshot,
cx: &mut AsyncApp,
-) -> Range<Point> {
- if let Some(rows) = heuristic_syntactic_expand(
+) -> Range<text::Anchor> {
+ let range = if let Some(rows) = heuristic_syntactic_expand(
range.clone(),
DIAGNOSTIC_EXPANSION_ROW_LIMIT,
snapshot.clone(),
@@ -977,15 +999,17 @@ async fn context_range_for_entry(
)
.await
{
- return Range {
+ Range {
start: Point::new(*rows.start(), 0),
end: snapshot.clip_point(Point::new(*rows.end(), u32::MAX), Bias::Left),
- };
- }
- Range {
- start: Point::new(range.start.row.saturating_sub(context), 0),
- end: snapshot.clip_point(Point::new(range.end.row + context, u32::MAX), Bias::Left),
- }
+ }
+ } else {
+ Range {
+ start: Point::new(range.start.row.saturating_sub(context), 0),
+ end: snapshot.clip_point(Point::new(range.end.row + context, u32::MAX), Bias::Left),
+ }
+ };
+ snapshot.anchor_after(range.start)..snapshot.anchor_before(range.end)
}
/// Expands the input range using syntax information from TreeSitter. This expansion will be limited
@@ -6,9 +6,9 @@ use crate::{
EditDisplayMode, EditPrediction, Editor, EditorMode, EditorSettings, EditorSnapshot,
EditorStyle, FILE_HEADER_HEIGHT, FocusedBlock, GutterDimensions, HalfPageDown, HalfPageUp,
HandleInput, HoveredCursor, InlayHintRefreshReason, JumpData, LineDown, LineHighlight, LineUp,
- MAX_LINE_LEN, MINIMAP_FONT_SIZE, MULTI_BUFFER_EXCERPT_HEADER_HEIGHT, OpenExcerpts, PageDown,
- PageUp, PhantomBreakpointIndicator, Point, RowExt, RowRangeExt, SelectPhase,
- SelectedTextHighlight, Selection, SelectionDragState, SizingBehavior, SoftWrap,
+ MAX_LINE_LEN, MINIMAP_FONT_SIZE, MULTI_BUFFER_EXCERPT_HEADER_HEIGHT, OpenExcerpts,
+ OpenExcerptsSplit, PageDown, PageUp, PhantomBreakpointIndicator, Point, RowExt, RowRangeExt,
+ SelectPhase, SelectedTextHighlight, Selection, SelectionDragState, SizingBehavior, SoftWrap,
StickyHeaderExcerpt, ToPoint, ToggleFold, ToggleFoldAll,
code_context_menus::{CodeActionsMenu, MENU_ASIDE_MAX_WIDTH, MENU_ASIDE_MIN_WIDTH, MENU_GAP},
display_map::{
@@ -4043,17 +4043,24 @@ impl EditorElement {
)
.group_hover("", |div| div.underline()),
)
- .on_click(window.listener_for(&self.editor, {
- let jump_data = jump_data.clone();
- move |editor, e: &ClickEvent, window, cx| {
- editor.open_excerpts_common(
- Some(jump_data.clone()),
- e.modifiers().secondary(),
- window,
- cx,
- );
+ .on_click({
+ let focus_handle = focus_handle.clone();
+ move |event, window, cx| {
+ if event.modifiers().secondary() {
+ focus_handle.dispatch_action(
+ &OpenExcerptsSplit,
+ window,
+ cx,
+ );
+ } else {
+ focus_handle.dispatch_action(
+ &OpenExcerpts,
+ window,
+ cx,
+ );
+ }
}
- })),
+ }),
)
.when_some(parent_path, |then, path| {
then.child(div().child(path).text_color(
@@ -4069,24 +4076,36 @@ impl EditorElement {
can_open_excerpts && is_selected && relative_path.is_some(),
|el| {
el.child(
- Button::new("open-file", "Open File")
+ ButtonLike::new("open-file-button")
.style(ButtonStyle::OutlinedGhost)
- .key_binding(KeyBinding::for_action_in(
- &OpenExcerpts,
- &focus_handle,
- cx,
- ))
- .on_click(window.listener_for(&self.editor, {
- let jump_data = jump_data.clone();
- move |editor, e: &ClickEvent, window, cx| {
- editor.open_excerpts_common(
- Some(jump_data.clone()),
- e.modifiers().secondary(),
- window,
+ .child(
+ h_flex()
+ .gap_2p5()
+ .child(Label::new("Open file"))
+ .child(KeyBinding::for_action_in(
+ &OpenExcerpts,
+ &focus_handle,
cx,
- );
+ )),
+ )
+ .on_click({
+ let focus_handle = focus_handle.clone();
+ move |event, window, cx| {
+ if event.modifiers().secondary() {
+ focus_handle.dispatch_action(
+ &OpenExcerptsSplit,
+ window,
+ cx,
+ );
+ } else {
+ focus_handle.dispatch_action(
+ &OpenExcerpts,
+ window,
+ cx,
+ );
+ }
}
- })),
+ }),
)
},
)
@@ -8,6 +8,8 @@ use crate::{
};
use gpui::prelude::FluentBuilder;
use gpui::{Context, DismissEvent, Entity, Focusable as _, Pixels, Point, Subscription, Window};
+use project::DisableAiSettings;
+use settings::Settings;
use std::ops::Range;
use text::PointUtf16;
use workspace::OpenInTerminal;
@@ -202,6 +204,7 @@ pub fn deploy_context_menu(
let evaluate_selection = window.is_action_available(&EvaluateSelectedText, cx);
let run_to_cursor = window.is_action_available(&RunToCursor, cx);
+ let disable_ai = DisableAiSettings::get_global(cx).disable_ai;
ui::ContextMenu::build(window, cx, |menu, _window, _cx| {
let builder = menu
@@ -234,7 +237,9 @@ pub fn deploy_context_menu(
quick_launch: false,
}),
)
- .action("Add to Agent Thread", Box::new(AddSelectionToThread))
+ .when(!disable_ai && has_selections, |this| {
+ this.action("Add to Agent Thread", Box::new(AddSelectionToThread))
+ })
.separator()
.action("Cut", Box::new(Cut))
.action("Copy", Box::new(Copy))
@@ -1332,21 +1332,13 @@ fn parse_normal_key(
lparam: LPARAM,
mut modifiers: Modifiers,
) -> Option<(Keystroke, bool)> {
- let mut key_char = None;
+ let (key_char, prefer_character_input) = process_key(vkey, lparam.hiword());
+
let key = parse_immutable(vkey).or_else(|| {
let scan_code = lparam.hiword() & 0xFF;
- key_char = generate_key_char(
- vkey,
- scan_code as u32,
- modifiers.control,
- modifiers.shift,
- modifiers.alt,
- );
get_keystroke_key(vkey, scan_code as u32, &mut modifiers)
})?;
- let prefer_character_input = should_prefer_character_input(vkey, lparam.hiword() & 0xFF);
-
Some((
Keystroke {
modifiers,
@@ -1357,11 +1349,11 @@ fn parse_normal_key(
))
}
-fn should_prefer_character_input(vkey: VIRTUAL_KEY, scan_code: u16) -> bool {
+fn process_key(vkey: VIRTUAL_KEY, scan_code: u16) -> (Option<String>, bool) {
let mut keyboard_state = [0u8; 256];
unsafe {
if GetKeyboardState(&mut keyboard_state).is_err() {
- return false;
+ return (None, false);
}
}
@@ -1372,21 +1364,25 @@ fn should_prefer_character_input(vkey: VIRTUAL_KEY, scan_code: u16) -> bool {
scan_code as u32,
Some(&keyboard_state),
&mut buffer_c,
- 0x5,
+ 0x4,
)
};
+
+ if result_c == 0 {
+ return (None, false);
+ }
+
+ let c = &buffer_c[..result_c.unsigned_abs() as usize];
+ let key_char = String::from_utf16(c)
+ .ok()
+ .filter(|s| !s.is_empty() && !s.chars().next().unwrap().is_control());
+
if result_c < 0 {
- return false;
+ return (key_char, true);
}
- let c = &buffer_c[..result_c as usize];
- if char::decode_utf16(c.iter().copied())
- .next()
- .and_then(|ch| ch.ok())
- .map(|ch| ch.is_control())
- .unwrap_or(true)
- {
- return false;
+ if key_char.is_none() {
+ return (None, false);
}
// Workaround for some bug that makes the compiler think keyboard_state is still zeroed out
@@ -1395,9 +1391,10 @@ fn should_prefer_character_input(vkey: VIRTUAL_KEY, scan_code: u16) -> bool {
let alt_down = (keyboard_state[VK_MENU.0 as usize] & 0x80) != 0;
let win_down = (keyboard_state[VK_LWIN.0 as usize] & 0x80) != 0
|| (keyboard_state[VK_RWIN.0 as usize] & 0x80) != 0;
+
let has_modifiers = ctrl_down || alt_down || win_down;
if !has_modifiers {
- return false;
+ return (key_char, false);
}
let mut state_no_modifiers = keyboard_state;
@@ -1417,15 +1414,15 @@ fn should_prefer_character_input(vkey: VIRTUAL_KEY, scan_code: u16) -> bool {
scan_code as u32,
Some(&state_no_modifiers),
&mut buffer_c_no_modifiers,
- 0x5,
+ 0x4,
)
};
- if result_c_no_modifiers <= 0 {
- return false;
- }
- let c_no_modifiers = &buffer_c_no_modifiers[..result_c_no_modifiers as usize];
- c != c_no_modifiers
+ let c_no_modifiers = &buffer_c_no_modifiers[..result_c_no_modifiers.unsigned_abs() as usize];
+ (
+ key_char,
+ result_c != result_c_no_modifiers || c != c_no_modifiers,
+ )
}
fn parse_ime_composition_string(ctx: HIMC, comp_type: IME_COMPOSITION_STRING) -> Option<String> {
@@ -1662,6 +1662,18 @@ impl Buffer {
self.parse_status.1.clone()
}
+ /// Wait until the buffer is no longer parsing
+ pub fn parsing_idle(&self) -> impl Future<Output = ()> + use<> {
+ let mut parse_status = self.parse_status();
+ async move {
+ while *parse_status.borrow() != ParseStatus::Idle {
+ if parse_status.changed().await.is_err() {
+ break;
+ }
+ }
+ }
+ }
+
/// Assign to the buffer a set of diagnostics created by a given language server.
pub fn update_diagnostics(
&mut self,
@@ -17,7 +17,6 @@ test-support = []
[dependencies]
anthropic = { workspace = true, features = ["schemars"] }
-open_router.workspace = true
anyhow.workspace = true
base64.workspace = true
client.workspace = true
@@ -30,8 +29,10 @@ http_client.workspace = true
icons.workspace = true
image.workspace = true
log.workspace = true
+open_router.workspace = true
parking_lot.workspace = true
proto.workspace = true
+schemars.workspace = true
serde.workspace = true
serde_json.workspace = true
settings.workspace = true
@@ -4,6 +4,7 @@ mod registry;
mod request;
mod role;
mod telemetry;
+pub mod tool_schema;
#[cfg(any(test, feature = "test-support"))]
pub mod fake_provider;
@@ -35,6 +36,7 @@ pub use crate::registry::*;
pub use crate::request::*;
pub use crate::role::*;
pub use crate::telemetry::*;
+pub use crate::tool_schema::LanguageModelToolSchemaFormat;
pub const ANTHROPIC_PROVIDER_ID: LanguageModelProviderId =
LanguageModelProviderId::new("anthropic");
@@ -409,15 +411,6 @@ impl From<open_router::ApiError> for LanguageModelCompletionError {
}
}
-/// Indicates the format used to define the input schema for a language model tool.
-#[derive(Debug, PartialEq, Eq, Clone, Copy, Hash)]
-pub enum LanguageModelToolSchemaFormat {
- /// A JSON schema, see https://json-schema.org
- JsonSchema,
- /// A subset of an OpenAPI 3.0 schema object supported by Google AI, see https://ai.google.dev/api/caching#Schema
- JsonSchemaSubset,
-}
-
#[derive(Debug, PartialEq, Clone, Copy, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum StopReason {
@@ -1,5 +1,4 @@
use anyhow::Result;
-use language_model::LanguageModelToolSchemaFormat;
use schemars::{
JsonSchema, Schema,
generate::SchemaSettings,
@@ -7,7 +6,16 @@ use schemars::{
};
use serde_json::Value;
-pub(crate) fn root_schema_for<T: JsonSchema>(format: LanguageModelToolSchemaFormat) -> Schema {
+/// Indicates the format used to define the input schema for a language model tool.
+#[derive(Debug, PartialEq, Eq, Clone, Copy, Hash)]
+pub enum LanguageModelToolSchemaFormat {
+ /// A JSON schema, see https://json-schema.org
+ JsonSchema,
+ /// A subset of an OpenAPI 3.0 schema object supported by Google AI, see https://ai.google.dev/api/caching#Schema
+ JsonSchemaSubset,
+}
+
+pub fn root_schema_for<T: JsonSchema>(format: LanguageModelToolSchemaFormat) -> Schema {
let mut generator = match format {
LanguageModelToolSchemaFormat::JsonSchema => SchemaSettings::draft07().into_generator(),
LanguageModelToolSchemaFormat::JsonSchemaSubset => SchemaSettings::openapi3()
@@ -20,13 +20,13 @@ use std::pin::Pin;
use std::str::FromStr;
use std::sync::{Arc, LazyLock};
use strum::IntoEnumIterator;
-use ui::{Icon, IconName, List, Tooltip, prelude::*};
+use ui::{List, prelude::*};
use ui_input::InputField;
-use util::{ResultExt, truncate_and_trailoff};
+use util::ResultExt;
use zed_env_vars::{EnvVar, env_var};
use crate::api_key::ApiKeyState;
-use crate::ui::InstructionListItem;
+use crate::ui::{ConfiguredApiCard, InstructionListItem};
pub use settings::AnthropicAvailableModel as AvailableModel;
@@ -909,9 +909,21 @@ impl ConfigurationView {
impl Render for ConfigurationView {
fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let env_var_set = self.state.read(cx).api_key_state.is_from_env_var();
+ let configured_card_label = if env_var_set {
+ format!("API key set in {API_KEY_ENV_VAR_NAME} environment variable")
+ } else {
+ let api_url = AnthropicLanguageModelProvider::api_url(cx);
+ if api_url == ANTHROPIC_API_URL {
+ "API key configured".to_string()
+ } else {
+ format!("API key configured for {}", api_url)
+ }
+ };
if self.load_credentials_task.is_some() {
- div().child(Label::new("Loading credentials...")).into_any()
+ div()
+ .child(Label::new("Loading credentials..."))
+ .into_any_element()
} else if self.should_render_editor(cx) {
v_flex()
.size_full()
@@ -941,44 +953,17 @@ impl Render for ConfigurationView {
.size(LabelSize::Small)
.color(Color::Muted),
)
- .into_any()
+ .into_any_element()
} else {
- h_flex()
- .mt_1()
- .p_1()
- .justify_between()
- .rounded_md()
- .border_1()
- .border_color(cx.theme().colors().border)
- .bg(cx.theme().colors().background)
- .child(
- h_flex()
- .gap_1()
- .child(Icon::new(IconName::Check).color(Color::Success))
- .child(Label::new(if env_var_set {
- format!("API key set in {API_KEY_ENV_VAR_NAME} environment variable")
- } else {
- let api_url = AnthropicLanguageModelProvider::api_url(cx);
- if api_url == ANTHROPIC_API_URL {
- "API key configured".to_string()
- } else {
- format!("API key configured for {}", truncate_and_trailoff(&api_url, 32))
- }
- })),
- )
- .child(
- Button::new("reset-key", "Reset Key")
- .label_size(LabelSize::Small)
- .icon(Some(IconName::Trash))
- .icon_size(IconSize::Small)
- .icon_position(IconPosition::Start)
- .disabled(env_var_set)
- .when(env_var_set, |this| {
- this.tooltip(Tooltip::text(format!("To reset your API key, unset the {API_KEY_ENV_VAR_NAME} environment variable.")))
- })
- .on_click(cx.listener(|this, _, window, cx| this.reset_api_key(window, cx))),
- )
- .into_any()
+ ConfiguredApiCard::new(configured_card_label)
+ .disabled(env_var_set)
+ .on_click(cx.listener(|this, _, window, cx| this.reset_api_key(window, cx)))
+ .when(env_var_set, |this| {
+ this.tooltip_label(format!(
+ "To reset your API key, unset the {API_KEY_ENV_VAR_NAME} environment variable."
+ ))
+ })
+ .into_any_element()
}
}
}
@@ -2,7 +2,7 @@ use std::pin::Pin;
use std::str::FromStr;
use std::sync::Arc;
-use crate::ui::InstructionListItem;
+use crate::ui::{ConfiguredApiCard, InstructionListItem};
use anyhow::{Context as _, Result, anyhow};
use aws_config::stalled_stream_protection::StalledStreamProtectionConfig;
use aws_config::{BehaviorVersion, Region};
@@ -41,7 +41,7 @@ use serde_json::Value;
use settings::{BedrockAvailableModel as AvailableModel, Settings, SettingsStore};
use smol::lock::OnceCell;
use strum::{EnumIter, IntoEnumIterator, IntoStaticStr};
-use ui::{Icon, IconName, List, Tooltip, prelude::*};
+use ui::{List, prelude::*};
use ui_input::InputField;
use util::ResultExt;
@@ -1155,47 +1155,37 @@ impl Render for ConfigurationView {
return div().child(Label::new("Loading credentials...")).into_any();
}
+ let configured_label = if env_var_set {
+ format!(
+ "Access Key ID is set in {ZED_BEDROCK_ACCESS_KEY_ID_VAR}, Secret Key is set in {ZED_BEDROCK_SECRET_ACCESS_KEY_VAR}, Region is set in {ZED_BEDROCK_REGION_VAR} environment variables."
+ )
+ } else {
+ match bedrock_method {
+ Some(BedrockAuthMethod::Automatic) => "You are using automatic credentials.".into(),
+ Some(BedrockAuthMethod::NamedProfile) => "You are using named profile.".into(),
+ Some(BedrockAuthMethod::SingleSignOn) => {
+ "You are using a single sign on profile.".into()
+ }
+ None => "You are using static credentials.".into(),
+ }
+ };
+
+ let tooltip_label = if env_var_set {
+ Some(format!(
+ "To reset your credentials, unset the {ZED_BEDROCK_ACCESS_KEY_ID_VAR}, {ZED_BEDROCK_SECRET_ACCESS_KEY_VAR}, and {ZED_BEDROCK_REGION_VAR} environment variables."
+ ))
+ } else if bedrock_method.is_some() {
+ Some("You cannot reset credentials as they're being derived, check Zed settings to understand how.".to_string())
+ } else {
+ None
+ };
+
if self.should_render_editor(cx) {
- return h_flex()
- .mt_1()
- .p_1()
- .justify_between()
- .rounded_md()
- .border_1()
- .border_color(cx.theme().colors().border)
- .bg(cx.theme().colors().background)
- .child(
- h_flex()
- .gap_1()
- .child(Icon::new(IconName::Check).color(Color::Success))
- .child(Label::new(if env_var_set {
- format!("Access Key ID is set in {ZED_BEDROCK_ACCESS_KEY_ID_VAR}, Secret Key is set in {ZED_BEDROCK_SECRET_ACCESS_KEY_VAR}, Region is set in {ZED_BEDROCK_REGION_VAR} environment variables.")
- } else {
- match bedrock_method {
- Some(BedrockAuthMethod::Automatic) => "You are using automatic credentials".into(),
- Some(BedrockAuthMethod::NamedProfile) => {
- "You are using named profile".into()
- },
- Some(BedrockAuthMethod::SingleSignOn) => "You are using a single sign on profile".into(),
- None => "You are using static credentials".into(),
- }
- })),
- )
- .child(
- Button::new("reset-key", "Reset Key")
- .icon(Some(IconName::Trash))
- .icon_size(IconSize::Small)
- .icon_position(IconPosition::Start)
- .disabled(env_var_set || bedrock_method.is_some())
- .when(env_var_set, |this| {
- this.tooltip(Tooltip::text(format!("To reset your credentials, unset the {ZED_BEDROCK_ACCESS_KEY_ID_VAR}, {ZED_BEDROCK_SECRET_ACCESS_KEY_VAR}, and {ZED_BEDROCK_REGION_VAR} environment variables.")))
- })
- .when(bedrock_method.is_some(), |this| {
- this.tooltip(Tooltip::text("You cannot reset credentials as they're being derived, check Zed settings to understand how"))
- })
- .on_click(cx.listener(|this, _, window, cx| this.reset_credentials(window, cx))),
- )
- .into_any();
+ return ConfiguredApiCard::new(configured_label)
+ .disabled(env_var_set || bedrock_method.is_some())
+ .on_click(cx.listener(|this, _, window, cx| this.reset_credentials(window, cx)))
+ .when_some(tooltip_label, |this, label| this.tooltip_label(label))
+ .into_any_element();
}
v_flex()
@@ -1241,7 +1231,7 @@ impl Render for ConfigurationView {
}
impl ConfigurationView {
- fn render_static_credentials_ui(&self) -> AnyElement {
+ fn render_static_credentials_ui(&self) -> impl IntoElement {
v_flex()
.my_2()
.gap_1p5()
@@ -1278,6 +1268,5 @@ impl ConfigurationView {
.child(self.secret_access_key_editor.clone())
.child(self.session_token_editor.clone())
.child(self.region_editor.clone())
- .into_any_element()
}
}
@@ -29,6 +29,8 @@ use settings::SettingsStore;
use ui::{CommonAnimationExt, prelude::*};
use util::debug_panic;
+use crate::ui::ConfiguredApiCard;
+
const PROVIDER_ID: LanguageModelProviderId = LanguageModelProviderId::new("copilot_chat");
const PROVIDER_NAME: LanguageModelProviderName =
LanguageModelProviderName::new("GitHub Copilot Chat");
@@ -1326,27 +1328,12 @@ impl ConfigurationView {
impl Render for ConfigurationView {
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
if self.state.read(cx).is_authenticated(cx) {
- h_flex()
- .mt_1()
- .p_1()
- .justify_between()
- .rounded_md()
- .border_1()
- .border_color(cx.theme().colors().border)
- .bg(cx.theme().colors().background)
- .child(
- h_flex()
- .gap_1()
- .child(Icon::new(IconName::Check).color(Color::Success))
- .child(Label::new("Authorized")),
- )
- .child(
- Button::new("sign_out", "Sign Out")
- .label_size(LabelSize::Small)
- .on_click(|_, window, cx| {
- window.dispatch_action(copilot::SignOut.boxed_clone(), cx);
- }),
- )
+ ConfiguredApiCard::new("Authorized")
+ .button_label("Sign Out")
+ .on_click(|_, window, cx| {
+ window.dispatch_action(copilot::SignOut.boxed_clone(), cx);
+ })
+ .into_any_element()
} else {
let loading_icon = Icon::new(IconName::ArrowCircle).with_rotate_animation(4);
@@ -1357,37 +1344,49 @@ impl Render for ConfigurationView {
Status::Starting { task: _ } => h_flex()
.gap_2()
.child(loading_icon)
- .child(Label::new("Starting Copilot…")),
+ .child(Label::new("Starting Copilot…"))
+ .into_any_element(),
Status::SigningIn { prompt: _ }
| Status::SignedOut {
awaiting_signing_in: true,
} => h_flex()
.gap_2()
.child(loading_icon)
- .child(Label::new("Signing into Copilot…")),
+ .child(Label::new("Signing into Copilot…"))
+ .into_any_element(),
Status::Error(_) => {
const LABEL: &str = "Copilot had issues starting. Please try restarting it. If the issue persists, try reinstalling Copilot.";
v_flex()
.gap_6()
.child(Label::new(LABEL))
.child(svg().size_8().path(IconName::CopilotError.path()))
+ .into_any_element()
}
_ => {
const LABEL: &str = "To use Zed's agent with GitHub Copilot, you need to be logged in to GitHub. Note that your GitHub account must have an active Copilot Chat subscription.";
- v_flex().gap_2().child(Label::new(LABEL)).child(
- Button::new("sign_in", "Sign in to use GitHub Copilot")
- .full_width()
- .style(ButtonStyle::Outlined)
- .icon_color(Color::Muted)
- .icon(IconName::Github)
- .icon_position(IconPosition::Start)
- .icon_size(IconSize::Small)
- .on_click(|_, window, cx| copilot::initiate_sign_in(window, cx)),
- )
+ v_flex()
+ .gap_2()
+ .child(Label::new(LABEL))
+ .child(
+ Button::new("sign_in", "Sign in to use GitHub Copilot")
+ .full_width()
+ .style(ButtonStyle::Outlined)
+ .icon_color(Color::Muted)
+ .icon(IconName::Github)
+ .icon_position(IconPosition::Start)
+ .icon_size(IconSize::Small)
+ .on_click(|_, window, cx| {
+ copilot::initiate_sign_in(window, cx)
+ }),
+ )
+ .into_any_element()
}
},
- None => v_flex().gap_6().child(Label::new(ERROR_LABEL)),
+ None => v_flex()
+ .gap_6()
+ .child(Label::new(ERROR_LABEL))
+ .into_any_element(),
}
}
}
@@ -19,11 +19,12 @@ use std::pin::Pin;
use std::str::FromStr;
use std::sync::{Arc, LazyLock};
-use ui::{Icon, IconName, List, prelude::*};
+use ui::{List, prelude::*};
use ui_input::InputField;
-use util::{ResultExt, truncate_and_trailoff};
+use util::ResultExt;
use zed_env_vars::{EnvVar, env_var};
+use crate::ui::ConfiguredApiCard;
use crate::{api_key::ApiKeyState, ui::InstructionListItem};
const PROVIDER_ID: LanguageModelProviderId = LanguageModelProviderId::new("deepseek");
@@ -601,9 +602,21 @@ impl ConfigurationView {
impl Render for ConfigurationView {
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let env_var_set = self.state.read(cx).api_key_state.is_from_env_var();
+ let configured_card_label = if env_var_set {
+ format!("API key set in {API_KEY_ENV_VAR_NAME} environment variable")
+ } else {
+ let api_url = DeepSeekLanguageModelProvider::api_url(cx);
+ if api_url == DEEPSEEK_API_URL {
+ "API key configured".to_string()
+ } else {
+ format!("API key configured for {}", api_url)
+ }
+ };
if self.load_credentials_task.is_some() {
- div().child(Label::new("Loading credentials...")).into_any()
+ div()
+ .child(Label::new("Loading credentials..."))
+ .into_any_element()
} else if self.should_render_editor(cx) {
v_flex()
.size_full()
@@ -628,46 +641,12 @@ impl Render for ConfigurationView {
.size(LabelSize::Small)
.color(Color::Muted),
)
- .into_any()
+ .into_any_element()
} else {
- h_flex()
- .mt_1()
- .p_1()
- .justify_between()
- .rounded_md()
- .border_1()
- .border_color(cx.theme().colors().border)
- .bg(cx.theme().colors().background)
- .child(
- h_flex()
- .gap_1()
- .child(Icon::new(IconName::Check).color(Color::Success))
- .child(Label::new(if env_var_set {
- format!("API key set in {API_KEY_ENV_VAR_NAME} environment variable")
- } else {
- let api_url = DeepSeekLanguageModelProvider::api_url(cx);
- if api_url == DEEPSEEK_API_URL {
- "API key configured".to_string()
- } else {
- format!(
- "API key configured for {}",
- truncate_and_trailoff(&api_url, 32)
- )
- }
- })),
- )
- .child(
- Button::new("reset-key", "Reset Key")
- .label_size(LabelSize::Small)
- .icon(Some(IconName::Trash))
- .icon_size(IconSize::Small)
- .icon_position(IconPosition::Start)
- .disabled(env_var_set)
- .on_click(
- cx.listener(|this, _, window, cx| this.reset_api_key(window, cx)),
- ),
- )
- .into_any()
+ ConfiguredApiCard::new(configured_card_label)
+ .disabled(env_var_set)
+ .on_click(cx.listener(|this, _, window, cx| this.reset_api_key(window, cx)))
+ .into_any_element()
}
}
}
@@ -28,14 +28,14 @@ use std::sync::{
atomic::{self, AtomicU64},
};
use strum::IntoEnumIterator;
-use ui::{Icon, IconName, List, Tooltip, prelude::*};
+use ui::{List, prelude::*};
use ui_input::InputField;
-use util::{ResultExt, truncate_and_trailoff};
+use util::ResultExt;
use zed_env_vars::EnvVar;
use crate::api_key::ApiKey;
use crate::api_key::ApiKeyState;
-use crate::ui::InstructionListItem;
+use crate::ui::{ConfiguredApiCard, InstructionListItem};
const PROVIDER_ID: LanguageModelProviderId = language_model::GOOGLE_PROVIDER_ID;
const PROVIDER_NAME: LanguageModelProviderName = language_model::GOOGLE_PROVIDER_NAME;
@@ -835,9 +835,24 @@ impl ConfigurationView {
impl Render for ConfigurationView {
fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let env_var_set = self.state.read(cx).api_key_state.is_from_env_var();
+ let configured_card_label = if env_var_set {
+ format!(
+ "API key set in {} environment variable",
+ API_KEY_ENV_VAR.name
+ )
+ } else {
+ let api_url = GoogleLanguageModelProvider::api_url(cx);
+ if api_url == google_ai::API_URL {
+ "API key configured".to_string()
+ } else {
+ format!("API key configured for {}", api_url)
+ }
+ };
if self.load_credentials_task.is_some() {
- div().child(Label::new("Loading credentials...")).into_any()
+ div()
+ .child(Label::new("Loading credentials..."))
+ .into_any_element()
} else if self.should_render_editor(cx) {
v_flex()
.size_full()
@@ -864,44 +879,15 @@ impl Render for ConfigurationView {
)
.size(LabelSize::Small).color(Color::Muted),
)
- .into_any()
+ .into_any_element()
} else {
- h_flex()
- .mt_1()
- .p_1()
- .justify_between()
- .rounded_md()
- .border_1()
- .border_color(cx.theme().colors().border)
- .bg(cx.theme().colors().background)
- .child(
- h_flex()
- .gap_1()
- .child(Icon::new(IconName::Check).color(Color::Success))
- .child(Label::new(if env_var_set {
- format!("API key set in {} environment variable", API_KEY_ENV_VAR.name)
- } else {
- let api_url = GoogleLanguageModelProvider::api_url(cx);
- if api_url == google_ai::API_URL {
- "API key configured".to_string()
- } else {
- format!("API key configured for {}", truncate_and_trailoff(&api_url, 32))
- }
- })),
- )
- .child(
- Button::new("reset-key", "Reset Key")
- .label_size(LabelSize::Small)
- .icon(Some(IconName::Trash))
- .icon_size(IconSize::Small)
- .icon_position(IconPosition::Start)
- .disabled(env_var_set)
- .when(env_var_set, |this| {
- this.tooltip(Tooltip::text(format!("To reset your API key, make sure {GEMINI_API_KEY_VAR_NAME} and {GOOGLE_AI_API_KEY_VAR_NAME} environment variables are unset.")))
- })
- .on_click(cx.listener(|this, _, window, cx| this.reset_api_key(window, cx))),
- )
- .into_any()
+ ConfiguredApiCard::new(configured_card_label)
+ .disabled(env_var_set)
+ .on_click(cx.listener(|this, _, window, cx| this.reset_api_key(window, cx)))
+ .when(env_var_set, |this| {
+ this.tooltip_label(format!("To reset your API key, make sure {GEMINI_API_KEY_VAR_NAME} and {GOOGLE_AI_API_KEY_VAR_NAME} environment variables are unset."))
+ })
+ .into_any_element()
}
}
}
@@ -19,11 +19,12 @@ use std::pin::Pin;
use std::str::FromStr;
use std::sync::{Arc, LazyLock};
use strum::IntoEnumIterator;
-use ui::{Icon, IconName, List, Tooltip, prelude::*};
+use ui::{List, prelude::*};
use ui_input::InputField;
-use util::{ResultExt, truncate_and_trailoff};
+use util::ResultExt;
use zed_env_vars::{EnvVar, env_var};
+use crate::ui::ConfiguredApiCard;
use crate::{api_key::ApiKeyState, ui::InstructionListItem};
const PROVIDER_ID: LanguageModelProviderId = LanguageModelProviderId::new("mistral");
@@ -883,6 +884,12 @@ impl ConfigurationView {
let key_state = &self.state.read(cx).codestral_api_key_state;
let should_show_editor = !key_state.has_key();
let env_var_set = key_state.is_from_env_var();
+ let configured_card_label = if env_var_set {
+ format!("API key set in {CODESTRAL_API_KEY_ENV_VAR_NAME} environment variable")
+ } else {
+ "Codestral API key configured".to_string()
+ };
+
if should_show_editor {
v_flex()
.id("codestral")
@@ -910,42 +917,19 @@ impl ConfigurationView {
.size(LabelSize::Small).color(Color::Muted),
).into_any()
} else {
- h_flex()
- .id("codestral")
- .mt_2()
- .p_1()
- .justify_between()
- .rounded_md()
- .border_1()
- .border_color(cx.theme().colors().border)
- .bg(cx.theme().colors().background)
- .child(
- h_flex()
- .gap_1()
- .child(Icon::new(IconName::Check).color(Color::Success))
- .child(Label::new(if env_var_set {
- format!("API key set in {CODESTRAL_API_KEY_ENV_VAR_NAME} environment variable")
- } else {
- "Codestral API key configured".to_string()
- })),
+ ConfiguredApiCard::new(configured_card_label)
+ .disabled(env_var_set)
+ .on_click(cx.listener(|this, _, window, cx| this.reset_api_key(window, cx)))
+ .when(env_var_set, |this| {
+ this.tooltip_label(format!(
+ "To reset your API key, \
+ unset the {CODESTRAL_API_KEY_ENV_VAR_NAME} environment variable."
+ ))
+ })
+ .on_click(
+ cx.listener(|this, _, window, cx| this.reset_codestral_api_key(window, cx)),
)
- .child(
- Button::new("reset-key", "Reset Key")
- .label_size(LabelSize::Small)
- .icon(Some(IconName::Trash))
- .icon_size(IconSize::Small)
- .icon_position(IconPosition::Start)
- .disabled(env_var_set)
- .when(env_var_set, |this| {
- this.tooltip(Tooltip::text(format!(
- "To reset your API key, \
- unset the {CODESTRAL_API_KEY_ENV_VAR_NAME} environment variable."
- )))
- })
- .on_click(
- cx.listener(|this, _, window, cx| this.reset_codestral_api_key(window, cx)),
- ),
- ).into_any()
+ .into_any_element()
}
}
}
@@ -953,6 +937,16 @@ impl ConfigurationView {
impl Render for ConfigurationView {
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let env_var_set = self.state.read(cx).api_key_state.is_from_env_var();
+ let configured_card_label = if env_var_set {
+ format!("API key set in {API_KEY_ENV_VAR_NAME} environment variable")
+ } else {
+ let api_url = MistralLanguageModelProvider::api_url(cx);
+ if api_url == MISTRAL_API_URL {
+ "API key configured".to_string()
+ } else {
+ format!("API key configured for {}", api_url)
+ }
+ };
if self.load_credentials_task.is_some() {
div().child(Label::new("Loading credentials...")).into_any()
@@ -987,52 +981,17 @@ impl Render for ConfigurationView {
} else {
v_flex()
.size_full()
+ .gap_1()
.child(
- h_flex()
- .mt_1()
- .p_1()
- .justify_between()
- .rounded_md()
- .border_1()
- .border_color(cx.theme().colors().border)
- .bg(cx.theme().colors().background)
- .child(
- h_flex()
- .gap_1()
- .child(Icon::new(IconName::Check).color(Color::Success))
- .child(Label::new(if env_var_set {
- format!(
- "API key set in {API_KEY_ENV_VAR_NAME} environment variable"
- )
- } else {
- let api_url = MistralLanguageModelProvider::api_url(cx);
- if api_url == MISTRAL_API_URL {
- "API key configured".to_string()
- } else {
- format!(
- "API key configured for {}",
- truncate_and_trailoff(&api_url, 32)
- )
- }
- })),
- )
- .child(
- Button::new("reset-key", "Reset Key")
- .label_size(LabelSize::Small)
- .icon(Some(IconName::Trash))
- .icon_size(IconSize::Small)
- .icon_position(IconPosition::Start)
- .disabled(env_var_set)
- .when(env_var_set, |this| {
- this.tooltip(Tooltip::text(format!(
- "To reset your API key, \
- unset the {API_KEY_ENV_VAR_NAME} environment variable."
- )))
- })
- .on_click(cx.listener(|this, _, window, cx| {
- this.reset_api_key(window, cx)
- })),
- ),
+ ConfiguredApiCard::new(configured_card_label)
+ .disabled(env_var_set)
+ .on_click(cx.listener(|this, _, window, cx| this.reset_api_key(window, cx)))
+ .when(env_var_set, |this| {
+ this.tooltip_label(format!(
+ "To reset your API key, \
+ unset the {API_KEY_ENV_VAR_NAME} environment variable."
+ ))
+ }),
)
.child(self.render_codestral_api_key_editor(cx))
.into_any()
@@ -28,7 +28,7 @@ use zed_env_vars::{EnvVar, env_var};
use crate::AllLanguageModelSettings;
use crate::api_key::ApiKeyState;
-use crate::ui::InstructionListItem;
+use crate::ui::{ConfiguredApiCard, InstructionListItem};
const OLLAMA_DOWNLOAD_URL: &str = "https://ollama.com/download";
const OLLAMA_LIBRARY_URL: &str = "https://ollama.com/library";
@@ -749,9 +749,14 @@ impl ConfigurationView {
))
}
- fn render_api_key_editor(&self, cx: &Context<Self>) -> Div {
+ fn render_api_key_editor(&self, cx: &Context<Self>) -> impl IntoElement {
let state = self.state.read(cx);
let env_var_set = state.api_key_state.is_from_env_var();
+ let configured_card_label = if env_var_set {
+ format!("API key set in {API_KEY_ENV_VAR_NAME} environment variable.")
+ } else {
+ "API key configured".to_string()
+ };
if !state.api_key_state.has_key() {
v_flex()
@@ -764,40 +769,15 @@ impl ConfigurationView {
.size(LabelSize::Small)
.color(Color::Muted),
)
+ .into_any_element()
} else {
- h_flex()
- .p_3()
- .justify_between()
- .rounded_md()
- .border_1()
- .border_color(cx.theme().colors().border)
- .bg(cx.theme().colors().elevated_surface_background)
- .child(
- h_flex()
- .gap_2()
- .child(Icon::new(IconName::Check).color(Color::Success))
- .child(
- Label::new(
- if env_var_set {
- format!("API key set in {API_KEY_ENV_VAR_NAME} environment variable.")
- } else {
- "API key configured".to_string()
- }
- )
- )
- )
- .child(
- Button::new("reset-api-key", "Reset API Key")
- .label_size(LabelSize::Small)
- .icon(IconName::Undo)
- .icon_size(IconSize::Small)
- .icon_position(IconPosition::Start)
- .layer(ElevationIndex::ModalSurface)
- .when(env_var_set, |this| {
- this.tooltip(Tooltip::text(format!("To reset your API key, unset the {API_KEY_ENV_VAR_NAME} environment variable.")))
- })
- .on_click(cx.listener(|this, _, window, cx| this.reset_api_key(window, cx))),
- )
+ ConfiguredApiCard::new(configured_card_label)
+ .disabled(env_var_set)
+ .on_click(cx.listener(|this, _, window, cx| this.reset_api_key(window, cx)))
+ .when(env_var_set, |this| {
+ this.tooltip_label(format!("To reset your API key, unset the {API_KEY_ENV_VAR_NAME} environment variable."))
+ })
+ .into_any_element()
}
}
@@ -909,7 +889,7 @@ impl Render for ConfigurationView {
)
.child(
IconButton::new("refresh-models", IconName::RotateCcw)
- .tooltip(Tooltip::text("Refresh models"))
+ .tooltip(Tooltip::text("Refresh Models"))
.on_click(cx.listener(|this, _, _, cx| {
this.state.update(cx, |state, _| {
state.fetched_models.clear();
@@ -20,11 +20,12 @@ use std::pin::Pin;
use std::str::FromStr as _;
use std::sync::{Arc, LazyLock};
use strum::IntoEnumIterator;
-use ui::{ElevationIndex, List, Tooltip, prelude::*};
+use ui::{List, prelude::*};
use ui_input::InputField;
-use util::{ResultExt, truncate_and_trailoff};
+use util::ResultExt;
use zed_env_vars::{EnvVar, env_var};
+use crate::ui::ConfiguredApiCard;
use crate::{api_key::ApiKeyState, ui::InstructionListItem};
const PROVIDER_ID: LanguageModelProviderId = language_model::OPEN_AI_PROVIDER_ID;
@@ -762,6 +763,16 @@ impl ConfigurationView {
impl Render for ConfigurationView {
fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let env_var_set = self.state.read(cx).api_key_state.is_from_env_var();
+ let configured_card_label = if env_var_set {
+ format!("API key set in {API_KEY_ENV_VAR_NAME} environment variable")
+ } else {
+ let api_url = OpenAiLanguageModelProvider::api_url(cx);
+ if api_url == OPEN_AI_API_URL {
+ "API key configured".to_string()
+ } else {
+ format!("API key configured for {}", api_url)
+ }
+ };
let api_key_section = if self.should_render_editor(cx) {
v_flex()
@@ -795,44 +806,15 @@ impl Render for ConfigurationView {
)
.size(LabelSize::Small).color(Color::Muted),
)
- .into_any()
+ .into_any_element()
} else {
- h_flex()
- .mt_1()
- .p_1()
- .justify_between()
- .rounded_md()
- .border_1()
- .border_color(cx.theme().colors().border)
- .bg(cx.theme().colors().background)
- .child(
- h_flex()
- .gap_1()
- .child(Icon::new(IconName::Check).color(Color::Success))
- .child(Label::new(if env_var_set {
- format!("API key set in {API_KEY_ENV_VAR_NAME} environment variable")
- } else {
- let api_url = OpenAiLanguageModelProvider::api_url(cx);
- if api_url == OPEN_AI_API_URL {
- "API key configured".to_string()
- } else {
- format!("API key configured for {}", truncate_and_trailoff(&api_url, 32))
- }
- })),
- )
- .child(
- Button::new("reset-api-key", "Reset API Key")
- .label_size(LabelSize::Small)
- .icon(IconName::Undo)
- .icon_size(IconSize::Small)
- .icon_position(IconPosition::Start)
- .layer(ElevationIndex::ModalSurface)
- .when(env_var_set, |this| {
- this.tooltip(Tooltip::text(format!("To reset your API key, unset the {API_KEY_ENV_VAR_NAME} environment variable.")))
- })
- .on_click(cx.listener(|this, _, window, cx| this.reset_api_key(window, cx))),
- )
- .into_any()
+ ConfiguredApiCard::new(configured_card_label)
+ .disabled(env_var_set)
+ .on_click(cx.listener(|this, _, window, cx| this.reset_api_key(window, cx)))
+ .when(env_var_set, |this| {
+ this.tooltip_label(format!("To reset your API key, unset the {API_KEY_ENV_VAR_NAME} environment variable."))
+ })
+ .into_any_element()
};
let compatible_api_section = h_flex()
@@ -15,7 +15,7 @@ use settings::{Settings, SettingsStore};
use std::sync::Arc;
use ui::{ElevationIndex, Tooltip, prelude::*};
use ui_input::InputField;
-use util::{ResultExt, truncate_and_trailoff};
+use util::ResultExt;
use zed_env_vars::EnvVar;
use crate::api_key::ApiKeyState;
@@ -455,25 +455,39 @@ impl Render for ConfigurationView {
.bg(cx.theme().colors().background)
.child(
h_flex()
+ .flex_1()
+ .min_w_0()
.gap_1()
.child(Icon::new(IconName::Check).color(Color::Success))
- .child(Label::new(if env_var_set {
- format!("API key set in {env_var_name} environment variable")
- } else {
- format!("API key configured for {}", truncate_and_trailoff(&state.settings.api_url, 32))
- })),
+ .child(
+ div()
+ .w_full()
+ .overflow_x_hidden()
+ .text_ellipsis()
+ .child(Label::new(
+ if env_var_set {
+ format!("API key set in {env_var_name} environment variable")
+ } else {
+ format!("API key configured for {}", &state.settings.api_url)
+ }
+ ))
+ ),
)
.child(
- Button::new("reset-api-key", "Reset API Key")
- .label_size(LabelSize::Small)
- .icon(IconName::Undo)
- .icon_size(IconSize::Small)
- .icon_position(IconPosition::Start)
- .layer(ElevationIndex::ModalSurface)
- .when(env_var_set, |this| {
- this.tooltip(Tooltip::text(format!("To reset your API key, unset the {env_var_name} environment variable.")))
- })
- .on_click(cx.listener(|this, _, window, cx| this.reset_api_key(window, cx))),
+ h_flex()
+ .flex_shrink_0()
+ .child(
+ Button::new("reset-api-key", "Reset API Key")
+ .label_size(LabelSize::Small)
+ .icon(IconName::Undo)
+ .icon_size(IconSize::Small)
+ .icon_position(IconPosition::Start)
+ .layer(ElevationIndex::ModalSurface)
+ .when(env_var_set, |this| {
+ this.tooltip(Tooltip::text(format!("To reset your API key, unset the {env_var_name} environment variable.")))
+ })
+ .on_click(cx.listener(|this, _, window, cx| this.reset_api_key(window, cx))),
+ ),
)
.into_any()
};
@@ -17,11 +17,12 @@ use settings::{OpenRouterAvailableModel as AvailableModel, Settings, SettingsSto
use std::pin::Pin;
use std::str::FromStr as _;
use std::sync::{Arc, LazyLock};
-use ui::{Icon, IconName, List, Tooltip, prelude::*};
+use ui::{List, prelude::*};
use ui_input::InputField;
-use util::{ResultExt, truncate_and_trailoff};
+use util::ResultExt;
use zed_env_vars::{EnvVar, env_var};
+use crate::ui::ConfiguredApiCard;
use crate::{api_key::ApiKeyState, ui::InstructionListItem};
const PROVIDER_ID: LanguageModelProviderId = LanguageModelProviderId::new("openrouter");
@@ -777,9 +778,21 @@ impl ConfigurationView {
impl Render for ConfigurationView {
fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let env_var_set = self.state.read(cx).api_key_state.is_from_env_var();
+ let configured_card_label = if env_var_set {
+ format!("API key set in {API_KEY_ENV_VAR_NAME} environment variable")
+ } else {
+ let api_url = OpenRouterLanguageModelProvider::api_url(cx);
+ if api_url == OPEN_ROUTER_API_URL {
+ "API key configured".to_string()
+ } else {
+ format!("API key configured for {}", api_url)
+ }
+ };
if self.load_credentials_task.is_some() {
- div().child(Label::new("Loading credentials...")).into_any()
+ div()
+ .child(Label::new("Loading credentials..."))
+ .into_any_element()
} else if self.should_render_editor(cx) {
v_flex()
.size_full()
@@ -806,44 +819,15 @@ impl Render for ConfigurationView {
)
.size(LabelSize::Small).color(Color::Muted),
)
- .into_any()
+ .into_any_element()
} else {
- h_flex()
- .mt_1()
- .p_1()
- .justify_between()
- .rounded_md()
- .border_1()
- .border_color(cx.theme().colors().border)
- .bg(cx.theme().colors().background)
- .child(
- h_flex()
- .gap_1()
- .child(Icon::new(IconName::Check).color(Color::Success))
- .child(Label::new(if env_var_set {
- format!("API key set in {API_KEY_ENV_VAR_NAME} environment variable")
- } else {
- let api_url = OpenRouterLanguageModelProvider::api_url(cx);
- if api_url == OPEN_ROUTER_API_URL {
- "API key configured".to_string()
- } else {
- format!("API key configured for {}", truncate_and_trailoff(&api_url, 32))
- }
- })),
- )
- .child(
- Button::new("reset-key", "Reset Key")
- .label_size(LabelSize::Small)
- .icon(Some(IconName::Trash))
- .icon_size(IconSize::Small)
- .icon_position(IconPosition::Start)
- .disabled(env_var_set)
- .when(env_var_set, |this| {
- this.tooltip(Tooltip::text(format!("To reset your API key, unset the {API_KEY_ENV_VAR_NAME} environment variable.")))
- })
- .on_click(cx.listener(|this, _, window, cx| this.reset_api_key(window, cx))),
- )
- .into_any()
+ ConfiguredApiCard::new(configured_card_label)
+ .disabled(env_var_set)
+ .on_click(cx.listener(|this, _, window, cx| this.reset_api_key(window, cx)))
+ .when(env_var_set, |this| {
+ this.tooltip_label(format!("To reset your API key, unset the {API_KEY_ENV_VAR_NAME} environment variable."))
+ })
+ .into_any_element()
}
}
}
@@ -14,13 +14,16 @@ pub use settings::VercelAvailableModel as AvailableModel;
use settings::{Settings, SettingsStore};
use std::sync::{Arc, LazyLock};
use strum::IntoEnumIterator;
-use ui::{ElevationIndex, List, Tooltip, prelude::*};
+use ui::{List, prelude::*};
use ui_input::InputField;
-use util::{ResultExt, truncate_and_trailoff};
+use util::ResultExt;
use vercel::{Model, VERCEL_API_URL};
use zed_env_vars::{EnvVar, env_var};
-use crate::{api_key::ApiKeyState, ui::InstructionListItem};
+use crate::{
+ api_key::ApiKeyState,
+ ui::{ConfiguredApiCard, InstructionListItem},
+};
const PROVIDER_ID: LanguageModelProviderId = LanguageModelProviderId::new("vercel");
const PROVIDER_NAME: LanguageModelProviderName = LanguageModelProviderName::new("Vercel");
@@ -448,6 +451,16 @@ impl ConfigurationView {
impl Render for ConfigurationView {
fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let env_var_set = self.state.read(cx).api_key_state.is_from_env_var();
+ let configured_card_label = if env_var_set {
+ format!("API key set in {API_KEY_ENV_VAR_NAME} environment variable")
+ } else {
+ let api_url = VercelLanguageModelProvider::api_url(cx);
+ if api_url == VERCEL_API_URL {
+ "API key configured".to_string()
+ } else {
+ format!("API key configured for {}", api_url)
+ }
+ };
let api_key_section = if self.should_render_editor(cx) {
v_flex()
@@ -477,44 +490,15 @@ impl Render for ConfigurationView {
.size(LabelSize::Small)
.color(Color::Muted),
)
- .into_any()
+ .into_any_element()
} else {
- h_flex()
- .mt_1()
- .p_1()
- .justify_between()
- .rounded_md()
- .border_1()
- .border_color(cx.theme().colors().border)
- .bg(cx.theme().colors().background)
- .child(
- h_flex()
- .gap_1()
- .child(Icon::new(IconName::Check).color(Color::Success))
- .child(Label::new(if env_var_set {
- format!("API key set in {API_KEY_ENV_VAR_NAME} environment variable")
- } else {
- let api_url = VercelLanguageModelProvider::api_url(cx);
- if api_url == VERCEL_API_URL {
- "API key configured".to_string()
- } else {
- format!("API key configured for {}", truncate_and_trailoff(&api_url, 32))
- }
- })),
- )
- .child(
- Button::new("reset-api-key", "Reset API Key")
- .label_size(LabelSize::Small)
- .icon(IconName::Undo)
- .icon_size(IconSize::Small)
- .icon_position(IconPosition::Start)
- .layer(ElevationIndex::ModalSurface)
- .when(env_var_set, |this| {
- this.tooltip(Tooltip::text(format!("To reset your API key, unset the {API_KEY_ENV_VAR_NAME} environment variable.")))
- })
- .on_click(cx.listener(|this, _, window, cx| this.reset_api_key(window, cx))),
- )
- .into_any()
+ ConfiguredApiCard::new(configured_card_label)
+ .disabled(env_var_set)
+ .when(env_var_set, |this| {
+ this.tooltip_label(format!("To reset your API key, unset the {API_KEY_ENV_VAR_NAME} environment variable."))
+ })
+ .on_click(cx.listener(|this, _, window, cx| this.reset_api_key(window, cx)))
+ .into_any_element()
};
if self.load_credentials_task.is_some() {
@@ -14,13 +14,16 @@ pub use settings::XaiAvailableModel as AvailableModel;
use settings::{Settings, SettingsStore};
use std::sync::{Arc, LazyLock};
use strum::IntoEnumIterator;
-use ui::{ElevationIndex, List, Tooltip, prelude::*};
+use ui::{List, prelude::*};
use ui_input::InputField;
-use util::{ResultExt, truncate_and_trailoff};
+use util::ResultExt;
use x_ai::{Model, XAI_API_URL};
use zed_env_vars::{EnvVar, env_var};
-use crate::{api_key::ApiKeyState, ui::InstructionListItem};
+use crate::{
+ api_key::ApiKeyState,
+ ui::{ConfiguredApiCard, InstructionListItem},
+};
const PROVIDER_ID: LanguageModelProviderId = LanguageModelProviderId::new("x_ai");
const PROVIDER_NAME: LanguageModelProviderName = LanguageModelProviderName::new("xAI");
@@ -445,6 +448,16 @@ impl ConfigurationView {
impl Render for ConfigurationView {
fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let env_var_set = self.state.read(cx).api_key_state.is_from_env_var();
+ let configured_card_label = if env_var_set {
+ format!("API key set in {API_KEY_ENV_VAR_NAME} environment variable")
+ } else {
+ let api_url = XAiLanguageModelProvider::api_url(cx);
+ if api_url == XAI_API_URL {
+ "API key configured".to_string()
+ } else {
+ format!("API key configured for {}", api_url)
+ }
+ };
let api_key_section = if self.should_render_editor(cx) {
v_flex()
@@ -474,44 +487,15 @@ impl Render for ConfigurationView {
.size(LabelSize::Small)
.color(Color::Muted),
)
- .into_any()
+ .into_any_element()
} else {
- h_flex()
- .mt_1()
- .p_1()
- .justify_between()
- .rounded_md()
- .border_1()
- .border_color(cx.theme().colors().border)
- .bg(cx.theme().colors().background)
- .child(
- h_flex()
- .gap_1()
- .child(Icon::new(IconName::Check).color(Color::Success))
- .child(Label::new(if env_var_set {
- format!("API key set in {API_KEY_ENV_VAR_NAME} environment variable")
- } else {
- let api_url = XAiLanguageModelProvider::api_url(cx);
- if api_url == XAI_API_URL {
- "API key configured".to_string()
- } else {
- format!("API key configured for {}", truncate_and_trailoff(&api_url, 32))
- }
- })),
- )
- .child(
- Button::new("reset-api-key", "Reset API Key")
- .label_size(LabelSize::Small)
- .icon(IconName::Undo)
- .icon_size(IconSize::Small)
- .icon_position(IconPosition::Start)
- .layer(ElevationIndex::ModalSurface)
- .when(env_var_set, |this| {
- this.tooltip(Tooltip::text(format!("To reset your API key, unset the {API_KEY_ENV_VAR_NAME} environment variable.")))
- })
- .on_click(cx.listener(|this, _, window, cx| this.reset_api_key(window, cx))),
- )
- .into_any()
+ ConfiguredApiCard::new(configured_card_label)
+ .disabled(env_var_set)
+ .when(env_var_set, |this| {
+ this.tooltip_label(format!("To reset your API key, unset the {API_KEY_ENV_VAR_NAME} environment variable."))
+ })
+ .on_click(cx.listener(|this, _, window, cx| this.reset_api_key(window, cx)))
+ .into_any_element()
};
if self.load_credentials_task.is_some() {
@@ -1,2 +1,4 @@
+pub mod configured_api_card;
pub mod instruction_list_item;
+pub use configured_api_card::ConfiguredApiCard;
pub use instruction_list_item::InstructionListItem;
@@ -0,0 +1,86 @@
+use gpui::{ClickEvent, IntoElement, ParentElement, SharedString};
+use ui::{Tooltip, prelude::*};
+
+#[derive(IntoElement)]
+pub struct ConfiguredApiCard {
+ label: SharedString,
+ button_label: Option<SharedString>,
+ tooltip_label: Option<SharedString>,
+ disabled: bool,
+ on_click: Option<Box<dyn Fn(&ClickEvent, &mut Window, &mut App) + 'static>>,
+}
+
+impl ConfiguredApiCard {
+ pub fn new(label: impl Into<SharedString>) -> Self {
+ Self {
+ label: label.into(),
+ button_label: None,
+ tooltip_label: None,
+ disabled: false,
+ on_click: None,
+ }
+ }
+
+ pub fn on_click(
+ mut self,
+ handler: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static,
+ ) -> Self {
+ self.on_click = Some(Box::new(handler));
+ self
+ }
+
+ pub fn button_label(mut self, button_label: impl Into<SharedString>) -> Self {
+ self.button_label = Some(button_label.into());
+ self
+ }
+
+ pub fn tooltip_label(mut self, tooltip_label: impl Into<SharedString>) -> Self {
+ self.tooltip_label = Some(tooltip_label.into());
+ self
+ }
+
+ pub fn disabled(mut self, disabled: bool) -> Self {
+ self.disabled = disabled;
+ self
+ }
+}
+
+impl RenderOnce for ConfiguredApiCard {
+ fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement {
+ let button_label = self.button_label.unwrap_or("Reset Key".into());
+ let button_id = SharedString::new(format!("id-{}", button_label));
+
+ h_flex()
+ .mt_0p5()
+ .p_1()
+ .justify_between()
+ .rounded_md()
+ .border_1()
+ .border_color(cx.theme().colors().border)
+ .bg(cx.theme().colors().background)
+ .child(
+ h_flex()
+ .flex_1()
+ .min_w_0()
+ .gap_1()
+ .child(Icon::new(IconName::Check).color(Color::Success))
+ .child(Label::new(self.label).truncate()),
+ )
+ .child(
+ Button::new(button_id, button_label)
+ .label_size(LabelSize::Small)
+ .icon(IconName::Undo)
+ .icon_size(IconSize::Small)
+ .icon_color(Color::Muted)
+ .icon_position(IconPosition::Start)
+ .disabled(self.disabled)
+ .when_some(self.tooltip_label, |this, label| {
+ this.tooltip(Tooltip::text(label))
+ })
+ .when_some(
+ self.on_click.filter(|_| !self.disabled),
+ |this, on_click| this.on_click(on_click),
+ ),
+ )
+ }
+}
@@ -98,7 +98,7 @@ impl LspInstaller for CLspAdapter {
})
.await
.inspect_err(|err| {
- log::warn!("Unable to run {binary_path:?} asset, redownloading: {err}",)
+ log::warn!("Unable to run {binary_path:?} asset, redownloading: {err:#}",)
})
};
if let (Some(actual_digest), Some(expected_digest)) =
@@ -0,0 +1,3 @@
+((comment) @injection.content
+ (#set! injection.language "comment")
+)
@@ -0,0 +1,2 @@
+((comment) @injection.content
+ (#set! injection.language "comment"))
@@ -0,0 +1,2 @@
+((comment) @injection.content
+ (#set! injection.language "comment"))
@@ -263,7 +263,7 @@ impl LspInstaller for TyLspAdapter {
})
.await
.inspect_err(|err| {
- log::warn!("Unable to run {server_path:?} asset, redownloading: {err}",)
+ log::warn!("Unable to run {server_path:?} asset, redownloading: {err:#}",)
})
};
if let (Some(actual_digest), Some(expected_digest)) =
@@ -2176,7 +2176,7 @@ impl LspInstaller for RuffLspAdapter {
})
.await
.inspect_err(|err| {
- log::warn!("Unable to run {server_path:?} asset, redownloading: {err}",)
+ log::warn!("Unable to run {server_path:?} asset, redownloading: {err:#}",)
})
};
if let (Some(actual_digest), Some(expected_digest)) =
@@ -529,7 +529,7 @@ impl LspInstaller for RustLspAdapter {
})
.await
.inspect_err(|err| {
- log::warn!("Unable to run {server_path:?} asset, redownloading: {err}",)
+ log::warn!("Unable to run {server_path:?} asset, redownloading: {err:#}",)
})
};
if let (Some(actual_digest), Some(expected_digest)) =
@@ -41,6 +41,9 @@
name: (identifier) @function.special)
])
+(macro_invocation
+ "!" @function.special)
+
(macro_definition
name: (identifier) @function.special.definition)
@@ -1,4 +1,7 @@
-((line_comment) @injection.content
+([
+ (line_comment)
+ (block_comment)
+] @injection.content
(#set! injection.language "comment"))
(macro_invocation
@@ -787,7 +787,6 @@ impl Element for MarkdownElement {
};
let mut code_block_ids = HashSet::default();
- let mut current_code_block_metadata = None;
let mut current_img_block_range: Option<Range<usize>> = None;
for (range, event) in parsed_markdown.events.iter() {
// Skip alt text for images that rendered
@@ -849,7 +848,7 @@ impl Element for MarkdownElement {
markdown_end,
);
}
- MarkdownTag::CodeBlock { kind, metadata } => {
+ MarkdownTag::CodeBlock { kind, .. } => {
let language = match kind {
CodeBlockKind::Fenced => None,
CodeBlockKind::FencedLang(language) => {
@@ -862,8 +861,6 @@ impl Element for MarkdownElement {
_ => None,
};
- current_code_block_metadata = Some(metadata.clone());
-
let is_indented = matches!(kind, CodeBlockKind::Indented);
let scroll_handle = if self.style.code_block_overflow_x_scroll {
code_block_ids.insert(range.start);
@@ -935,64 +932,7 @@ impl Element for MarkdownElement {
builder.push_code_block(language);
builder.push_div(code_block, range, markdown_end);
}
- (CodeBlockRenderer::Custom { render, .. }, _) => {
- let parent_container = render(
- kind,
- &parsed_markdown,
- range.clone(),
- metadata.clone(),
- window,
- cx,
- );
-
- let mut parent_container: AnyDiv = if let Some(scroll_handle) =
- scroll_handle.as_ref()
- {
- let scrollbars = Scrollbars::new(ScrollAxes::Horizontal)
- .id(("markdown-code-block-scrollbar", range.start))
- .tracked_scroll_handle(scroll_handle.clone())
- .with_track_along(
- ScrollAxes::Horizontal,
- cx.theme().colors().editor_background,
- )
- .notify_content();
-
- parent_container
- .rounded_b_lg()
- .custom_scrollbars(scrollbars, window, cx)
- .into()
- } else {
- parent_container.into()
- };
-
- parent_container.style().refine(&self.style.code_block);
- builder.push_div(parent_container, range, markdown_end);
-
- let code_block = div()
- .id(("code-block", range.start))
- .rounded_b_lg()
- .map(|mut code_block| {
- if let Some(scroll_handle) = scroll_handle.as_ref() {
- code_block.style().restrict_scroll_to_axis =
- Some(true);
- code_block
- .flex()
- .overflow_x_scroll()
- .overflow_y_hidden()
- .track_scroll(scroll_handle)
- } else {
- code_block.w_full().overflow_hidden()
- }
- });
-
- if let Some(code_block_text_style) = &self.style.code_block.text
- {
- builder.push_text_style(code_block_text_style.to_owned());
- }
-
- builder.push_code_block(language);
- builder.push_div(code_block, range, markdown_end);
- }
+ (CodeBlockRenderer::Custom { .. }, _) => {}
}
}
MarkdownTag::HtmlBlock => builder.push_div(div(), range, markdown_end),
@@ -1131,24 +1071,6 @@ impl Element for MarkdownElement {
builder.pop_text_style();
}
- let metadata = current_code_block_metadata.take();
-
- if let CodeBlockRenderer::Custom {
- transform: Some(transform),
- ..
- } = &self.code_block_renderer
- {
- builder.modify_current_div(|el| {
- transform(
- el,
- range.clone(),
- metadata.clone().unwrap_or_default(),
- window,
- cx,
- )
- });
- }
-
if let CodeBlockRenderer::Default {
copy_button: true, ..
} = &self.code_block_renderer
@@ -1148,9 +1148,9 @@ impl MultiBuffer {
let mut counts: Vec<usize> = Vec::new();
for range in expanded_ranges {
if let Some(last_range) = merged_ranges.last_mut() {
- debug_assert!(
+ assert!(
last_range.context.start <= range.context.start,
- "Last range: {last_range:?} Range: {range:?}"
+ "ranges must be sorted: {last_range:?} <= {range:?}"
);
if last_range.context.end >= range.context.start
|| last_range.context.end.row + 1 == range.context.start.row
@@ -172,7 +172,7 @@ impl MultiBuffer {
.into_iter()
.chunk_by(|id| self.paths_by_excerpt.get(id).cloned())
.into_iter()
- .flat_map(|(k, v)| Some((k?, v.into_iter().collect::<Vec<_>>())))
+ .filter_map(|(k, v)| Some((k?, v.into_iter().collect::<Vec<_>>())))
.collect::<Vec<_>>();
let snapshot = self.snapshot(cx);
@@ -280,7 +280,7 @@ impl MultiBuffer {
.excerpts_by_path
.range(..path.clone())
.next_back()
- .map(|(_, value)| *value.last().unwrap())
+ .and_then(|(_, value)| value.last().copied())
.unwrap_or(ExcerptId::min());
let existing = self
@@ -299,6 +299,7 @@ impl MultiBuffer {
let snapshot = self.snapshot(cx);
let mut next_excerpt_id =
+ // is this right? What if we remove the last excerpt, then we might reallocate with a wrong mapping?
if let Some(last_entry) = self.snapshot.borrow().excerpt_ids.last() {
last_entry.id.0 + 1
} else {
@@ -311,20 +312,16 @@ impl MultiBuffer {
excerpts_cursor.next();
loop {
- let new = new_iter.peek();
- let existing = if let Some(existing_id) = existing_iter.peek() {
- let locator = snapshot.excerpt_locator_for_id(*existing_id);
+ let existing = if let Some(&existing_id) = existing_iter.peek() {
+ let locator = snapshot.excerpt_locator_for_id(existing_id);
excerpts_cursor.seek_forward(&Some(locator), Bias::Left);
if let Some(excerpt) = excerpts_cursor.item() {
if excerpt.buffer_id != buffer_snapshot.remote_id() {
- to_remove.push(*existing_id);
+ to_remove.push(existing_id);
existing_iter.next();
continue;
}
- Some((
- *existing_id,
- excerpt.range.context.to_point(buffer_snapshot),
- ))
+ Some((existing_id, excerpt.range.context.to_point(buffer_snapshot)))
} else {
None
}
@@ -332,6 +329,7 @@ impl MultiBuffer {
None
};
+ let new = new_iter.peek();
if let Some((last_id, last)) = to_insert.last_mut() {
if let Some(new) = new
&& last.context.end >= new.context.start
@@ -4519,7 +4519,6 @@ impl LspStore {
) {
Ok(LspParamsOrResponse::Params(lsp_params)) => lsp_params,
Ok(LspParamsOrResponse::Response(response)) => return Task::ready(Ok(response)),
-
Err(err) => {
let message = format!(
"{} via {} failed: {}",
@@ -4527,7 +4526,10 @@ impl LspStore {
language_server.name(),
err
);
- log::warn!("{message}");
+ // rust-analyzer likes to error with this when its still loading up
+ if !message.ends_with("content modified") {
+ log::warn!("{message}");
+ }
return Task::ready(Err(anyhow!(message)));
}
};
@@ -4585,7 +4587,10 @@ impl LspStore {
language_server.name(),
err
);
- log::warn!("{message}");
+ // rust-analyzer likes to error with this when its still loading up
+ if !message.ends_with("content modified") {
+ log::warn!("{message}");
+ }
anyhow::anyhow!(message)
})?;
@@ -6900,6 +6905,8 @@ impl LspStore {
let mut responses = Vec::new();
match server_task.await {
Ok(response) => responses.push((server_id, response)),
+ // rust-analyzer likes to error with this when its still loading up
+ Err(e) if format!("{e:#}").ends_with("content modified") => (),
Err(e) => log::error!(
"Error handling response for inlay hints request: {e:#}"
),
@@ -8426,7 +8433,7 @@ impl LspStore {
match response_result {
Ok(response) => responses.push((server_id, response)),
// rust-analyzer likes to error with this when its still loading up
- Err(e) if e.to_string().ends_with("content modified") => (),
+ Err(e) if format!("{e:#}").ends_with("content modified") => (),
Err(e) => log::error!("Error handling response for request {request:?}: {e:#}"),
}
}
@@ -12421,7 +12428,7 @@ impl LspStore {
match server_task.await {
Ok(response) => responses.push((server_id, response)),
// rust-analyzer likes to error with this when its still loading up
- Err(e) if e.to_string().ends_with("content modified") => (),
+ Err(e) if format!("{e:#}").ends_with("content modified") => (),
Err(e) => log::error!(
"Error handling response for request {request:?}: {e:#}"
),
@@ -4060,7 +4060,7 @@ impl Project {
result_rx
}
- fn find_search_candidate_buffers(
+ pub fn find_search_candidate_buffers(
&mut self,
query: &SearchQuery,
limit: usize,
@@ -448,7 +448,7 @@ where
aggregate: &mut dyn SeekAggregate<'a, T>,
) -> bool {
assert!(
- target.cmp(&self.position, self.cx) >= Ordering::Equal,
+ target.cmp(&self.position, self.cx).is_ge(),
"cannot seek backward",
);
@@ -93,7 +93,9 @@ async fn capture_unix(
// Parse the JSON output from zed --printenv
let env_map: collections::HashMap<String, String> = serde_json::from_str(&env_output)
- .with_context(|| "Failed to deserialize environment variables from json: {env_output}")?;
+ .with_context(|| {
+ format!("Failed to deserialize environment variables from json: {env_output}")
+ })?;
Ok(env_map)
}
@@ -210,6 +212,7 @@ async fn capture_windows(
let env_output = String::from_utf8_lossy(&output.stdout);
// Parse the JSON output from zed --printenv
- serde_json::from_str(&env_output)
- .with_context(|| "Failed to deserialize environment variables from json: {env_output}")
+ serde_json::from_str(&env_output).with_context(|| {
+ format!("Failed to deserialize environment variables from json: {env_output}")
+ })
}
@@ -310,8 +310,8 @@ impl MarksState {
fn load(&mut self, cx: &mut Context<Self>) {
cx.spawn(async move |this, cx| {
- let Some(workspace_id) = this.update(cx, |this, cx| this.workspace_id(cx))? else {
- return Ok(());
+ let Some(workspace_id) = this.update(cx, |this, cx| this.workspace_id(cx)).ok()? else {
+ return None;
};
let (marks, paths) = cx
.background_spawn(async move {
@@ -319,10 +319,12 @@ impl MarksState {
let paths = DB.get_global_marks_paths(workspace_id)?;
anyhow::Ok((marks, paths))
})
- .await?;
+ .await
+ .log_err()?;
this.update(cx, |this, cx| this.loaded(marks, paths, cx))
+ .ok()
})
- .detach_and_log_err(cx);
+ .detach();
}
fn loaded(
@@ -2,7 +2,7 @@ use collab_ui::collab_panel;
use gpui::{App, Menu, MenuItem, OsAction};
use release_channel::ReleaseChannel;
use terminal_view::terminal_panel;
-use zed_actions::{ToggleFocus as ToggleDebugPanel, agent::AddSelectionToThread, dev};
+use zed_actions::{ToggleFocus as ToggleDebugPanel, dev};
pub fn app_menus(cx: &mut App) -> Vec<Menu> {
use zed_actions::Quit;
@@ -218,8 +218,6 @@ pub fn app_menus(cx: &mut App) -> Vec<Menu> {
MenuItem::action("Move Line Up", editor::actions::MoveLineUp),
MenuItem::action("Move Line Down", editor::actions::MoveLineDown),
MenuItem::action("Duplicate Selection", editor::actions::DuplicateLineDown),
- MenuItem::separator(),
- MenuItem::action("Add to Agent Thread", AddSelectionToThread),
],
},
Menu {
@@ -15,7 +15,7 @@ use gpui::{
FocusHandle, Focusable, InteractiveElement, ParentElement, Render, Styled, Subscription,
WeakEntity, Window, anchored, deferred, point,
};
-use project::project_settings::DiagnosticSeverity;
+use project::{DisableAiSettings, project_settings::DiagnosticSeverity};
use search::{BufferSearchBar, buffer_search};
use settings::{Settings, SettingsStore};
use ui::{
@@ -27,7 +27,7 @@ use workspace::item::ItemBufferKind;
use workspace::{
ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView, Workspace, item::ItemHandle,
};
-use zed_actions::{assistant::InlineAssist, outline::ToggleOutline};
+use zed_actions::{agent::AddSelectionToThread, assistant::InlineAssist, outline::ToggleOutline};
const MAX_CODE_ACTION_MENU_LINES: u32 = 16;
@@ -241,8 +241,14 @@ impl Render for QuickActionBar {
.read(cx)
.snapshot(cx)
.has_diff_hunks();
+ let has_selection = editor.update(cx, |editor, cx| {
+ editor.has_non_empty_selection(&editor.display_snapshot(cx))
+ });
+
let focus = editor.focus_handle(cx);
+ let disable_ai = DisableAiSettings::get_global(cx).disable_ai;
+
PopoverMenu::new("editor-selections-dropdown")
.trigger_with_tooltip(
IconButton::new("toggle_editor_selections_icon", IconName::CursorIBeam)
@@ -278,6 +284,13 @@ impl Render for QuickActionBar {
skip_soft_wrap: true,
}),
)
+ .when(!disable_ai, |this| {
+ this.separator().action_disabled_when(
+ !has_selection,
+ "Add to Agent Thread",
+ Box::new(AddSelectionToThread),
+ )
+ })
.separator()
.action("Go to Symbol", Box::new(ToggleOutline))
.action("Go to Line/Column", Box::new(ToggleGoToLine))
@@ -33,6 +33,7 @@ project.workspace = true
release_channel.workspace = true
serde.workspace = true
serde_json.workspace = true
+smol.workspace = true
thiserror.workspace = true
util.workspace = true
uuid.workspace = true
@@ -1,18 +1,19 @@
use std::ops::Range;
use anyhow::Result;
+use cloud_zeta2_prompt::retrieval_prompt::SearchToolQuery;
use collections::HashMap;
-use edit_prediction_context::{EditPredictionExcerpt, EditPredictionExcerptOptions};
use futures::{
StreamExt,
channel::mpsc::{self, UnboundedSender},
};
use gpui::{AppContext, AsyncApp, Entity};
-use language::{Anchor, Buffer, BufferSnapshot, OffsetRangeExt, ToPoint as _};
+use language::{Anchor, Buffer, BufferSnapshot, OffsetRangeExt, Point, ToOffset, ToPoint};
use project::{
Project, WorktreeSettings,
search::{SearchQuery, SearchResult},
};
+use smol::channel;
use util::{
ResultExt as _,
paths::{PathMatcher, PathStyle},
@@ -21,7 +22,7 @@ use workspace::item::Settings as _;
pub async fn run_retrieval_searches(
project: Entity<Project>,
- regex_by_glob: HashMap<String, String>,
+ queries: Vec<SearchToolQuery>,
cx: &mut AsyncApp,
) -> Result<HashMap<Entity<Buffer>, Vec<Range<Anchor>>>> {
let (exclude_matcher, path_style) = project.update(cx, |project, cx| {
@@ -37,14 +38,13 @@ pub async fn run_retrieval_searches(
let (results_tx, mut results_rx) = mpsc::unbounded();
- for (glob, regex) in regex_by_glob {
+ for query in queries {
let exclude_matcher = exclude_matcher.clone();
let results_tx = results_tx.clone();
let project = project.clone();
cx.spawn(async move |cx| {
run_query(
- &glob,
- ®ex,
+ query,
results_tx.clone(),
path_style,
exclude_matcher,
@@ -108,87 +108,442 @@ pub async fn run_retrieval_searches(
.await
}
-const MIN_EXCERPT_LEN: usize = 16;
const MAX_EXCERPT_LEN: usize = 768;
const MAX_RESULTS_LEN: usize = MAX_EXCERPT_LEN * 5;
+struct SearchJob {
+ buffer: Entity<Buffer>,
+ snapshot: BufferSnapshot,
+ ranges: Vec<Range<usize>>,
+ query_ix: usize,
+ jobs_tx: channel::Sender<SearchJob>,
+}
+
async fn run_query(
- glob: &str,
- regex: &str,
+ input_query: SearchToolQuery,
results_tx: UnboundedSender<(Entity<Buffer>, BufferSnapshot, Vec<(Range<Anchor>, usize)>)>,
path_style: PathStyle,
exclude_matcher: PathMatcher,
project: &Entity<Project>,
cx: &mut AsyncApp,
) -> Result<()> {
- let include_matcher = PathMatcher::new(vec![glob], path_style)?;
-
- let query = SearchQuery::regex(
- regex,
- false,
- true,
- false,
- true,
- include_matcher,
- exclude_matcher,
- true,
- None,
- )?;
-
- let results = project.update(cx, |project, cx| project.search(query, cx))?;
- futures::pin_mut!(results);
-
- while let Some(SearchResult::Buffer { buffer, ranges }) = results.next().await {
- if results_tx.is_closed() {
- break;
- }
+ let include_matcher = PathMatcher::new(vec![input_query.glob], path_style)?;
- if ranges.is_empty() {
- continue;
- }
+ let make_search = |regex: &str| -> Result<SearchQuery> {
+ SearchQuery::regex(
+ regex,
+ false,
+ true,
+ false,
+ true,
+ include_matcher.clone(),
+ exclude_matcher.clone(),
+ true,
+ None,
+ )
+ };
- let snapshot = buffer.read_with(cx, |buffer, _cx| buffer.snapshot())?;
- let results_tx = results_tx.clone();
+ if let Some(outer_syntax_regex) = input_query.syntax_node.first() {
+ let outer_syntax_query = make_search(outer_syntax_regex)?;
+ let nested_syntax_queries = input_query
+ .syntax_node
+ .into_iter()
+ .skip(1)
+ .map(|query| make_search(&query))
+ .collect::<Result<Vec<_>>>()?;
+ let content_query = input_query
+ .content
+ .map(|regex| make_search(®ex))
+ .transpose()?;
- cx.background_spawn(async move {
- let mut excerpts = Vec::with_capacity(ranges.len());
-
- for range in ranges {
- let offset_range = range.to_offset(&snapshot);
- let query_point = (offset_range.start + offset_range.len() / 2).to_point(&snapshot);
-
- let excerpt = EditPredictionExcerpt::select_from_buffer(
- query_point,
- &snapshot,
- &EditPredictionExcerptOptions {
- max_bytes: MAX_EXCERPT_LEN,
- min_bytes: MIN_EXCERPT_LEN,
- target_before_cursor_over_total_bytes: 0.5,
- },
- None,
- );
+ let (jobs_tx, jobs_rx) = channel::unbounded();
- if let Some(excerpt) = excerpt
- && !excerpt.line_range.is_empty()
- {
- excerpts.push((
- snapshot.anchor_after(excerpt.range.start)
- ..snapshot.anchor_before(excerpt.range.end),
- excerpt.range.len(),
- ));
- }
+ let outer_search_results_rx =
+ project.update(cx, |project, cx| project.search(outer_syntax_query, cx))?;
+
+ let outer_search_task = cx.spawn(async move |cx| {
+ futures::pin_mut!(outer_search_results_rx);
+ while let Some(SearchResult::Buffer { buffer, ranges }) =
+ outer_search_results_rx.next().await
+ {
+ buffer
+ .read_with(cx, |buffer, _| buffer.parsing_idle())?
+ .await;
+ let snapshot = buffer.read_with(cx, |buffer, _cx| buffer.snapshot())?;
+ let expanded_ranges: Vec<_> = ranges
+ .into_iter()
+ .filter_map(|range| expand_to_parent_range(&range, &snapshot))
+ .collect();
+ jobs_tx
+ .send(SearchJob {
+ buffer,
+ snapshot,
+ ranges: expanded_ranges,
+ query_ix: 0,
+ jobs_tx: jobs_tx.clone(),
+ })
+ .await?;
}
+ anyhow::Ok(())
+ });
- let send_result = results_tx.unbounded_send((buffer, snapshot, excerpts));
+ let n_workers = cx.background_executor().num_cpus();
+ let search_job_task = cx.background_executor().scoped(|scope| {
+ for _ in 0..n_workers {
+ scope.spawn(async {
+ while let Ok(job) = jobs_rx.recv().await {
+ process_nested_search_job(
+ &results_tx,
+ &nested_syntax_queries,
+ &content_query,
+ job,
+ )
+ .await;
+ }
+ });
+ }
+ });
+
+ search_job_task.await;
+ outer_search_task.await?;
+ } else if let Some(content_regex) = &input_query.content {
+ let search_query = make_search(&content_regex)?;
+
+ let results_rx = project.update(cx, |project, cx| project.search(search_query, cx))?;
+ futures::pin_mut!(results_rx);
+
+ while let Some(SearchResult::Buffer { buffer, ranges }) = results_rx.next().await {
+ let snapshot = buffer.read_with(cx, |buffer, _cx| buffer.snapshot())?;
+
+ let ranges = ranges
+ .into_iter()
+ .map(|range| {
+ let range = range.to_offset(&snapshot);
+ let range = expand_to_entire_lines(range, &snapshot);
+ let size = range.len();
+ let range =
+ snapshot.anchor_before(range.start)..snapshot.anchor_after(range.end);
+ (range, size)
+ })
+ .collect();
+
+ let send_result = results_tx.unbounded_send((buffer.clone(), snapshot.clone(), ranges));
if let Err(err) = send_result
&& !err.is_disconnected()
{
log::error!("{err}");
}
- })
- .detach();
+ }
+ } else {
+ log::warn!("Context gathering model produced a glob-only search");
}
anyhow::Ok(())
}
+
+async fn process_nested_search_job(
+ results_tx: &UnboundedSender<(Entity<Buffer>, BufferSnapshot, Vec<(Range<Anchor>, usize)>)>,
+ queries: &Vec<SearchQuery>,
+ content_query: &Option<SearchQuery>,
+ job: SearchJob,
+) {
+ if let Some(search_query) = queries.get(job.query_ix) {
+ let mut subranges = Vec::new();
+ for range in job.ranges {
+ let start = range.start;
+ let search_results = search_query.search(&job.snapshot, Some(range)).await;
+ for subrange in search_results {
+ let subrange = start + subrange.start..start + subrange.end;
+ subranges.extend(expand_to_parent_range(&subrange, &job.snapshot));
+ }
+ }
+ job.jobs_tx
+ .send(SearchJob {
+ buffer: job.buffer,
+ snapshot: job.snapshot,
+ ranges: subranges,
+ query_ix: job.query_ix + 1,
+ jobs_tx: job.jobs_tx.clone(),
+ })
+ .await
+ .ok();
+ } else {
+ let ranges = if let Some(content_query) = content_query {
+ let mut subranges = Vec::new();
+ for range in job.ranges {
+ let start = range.start;
+ let search_results = content_query.search(&job.snapshot, Some(range)).await;
+ for subrange in search_results {
+ let subrange = start + subrange.start..start + subrange.end;
+ subranges.push(subrange);
+ }
+ }
+ subranges
+ } else {
+ job.ranges
+ };
+
+ let matches = ranges
+ .into_iter()
+ .map(|range| {
+ let snapshot = &job.snapshot;
+ let range = expand_to_entire_lines(range, snapshot);
+ let size = range.len();
+ let range = snapshot.anchor_before(range.start)..snapshot.anchor_after(range.end);
+ (range, size)
+ })
+ .collect();
+
+ let send_result = results_tx.unbounded_send((job.buffer, job.snapshot, matches));
+
+ if let Err(err) = send_result
+ && !err.is_disconnected()
+ {
+ log::error!("{err}");
+ }
+ }
+}
+
+fn expand_to_entire_lines(range: Range<usize>, snapshot: &BufferSnapshot) -> Range<usize> {
+ let mut point_range = range.to_point(snapshot);
+ point_range.start.column = 0;
+ if point_range.end.column > 0 {
+ point_range.end = snapshot.max_point().min(point_range.end + Point::new(1, 0));
+ }
+ point_range.to_offset(snapshot)
+}
+
+fn expand_to_parent_range<T: ToPoint + ToOffset>(
+ range: &Range<T>,
+ snapshot: &BufferSnapshot,
+) -> Option<Range<usize>> {
+ let mut line_range = range.to_point(&snapshot);
+ line_range.start.column = snapshot.indent_size_for_line(line_range.start.row).len;
+ line_range.end.column = snapshot.line_len(line_range.end.row);
+ // TODO skip result if matched line isn't the first node line?
+
+ let node = snapshot.syntax_ancestor(line_range)?;
+ Some(node.byte_range())
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use crate::merge_excerpts::merge_excerpts;
+ use cloud_zeta2_prompt::write_codeblock;
+ use edit_prediction_context::Line;
+ use gpui::TestAppContext;
+ use indoc::indoc;
+ use language::{Language, LanguageConfig, LanguageMatcher, tree_sitter_rust};
+ use pretty_assertions::assert_eq;
+ use project::FakeFs;
+ use serde_json::json;
+ use settings::SettingsStore;
+ use std::path::Path;
+ use util::path;
+
+ #[gpui::test]
+ async fn test_retrieval(cx: &mut TestAppContext) {
+ init_test(cx);
+ let fs = FakeFs::new(cx.executor());
+ fs.insert_tree(
+ path!("/root"),
+ json!({
+ "user.rs": indoc!{"
+ pub struct Organization {
+ owner: Arc<User>,
+ }
+
+ pub struct User {
+ first_name: String,
+ last_name: String,
+ }
+
+ impl Organization {
+ pub fn owner(&self) -> Arc<User> {
+ self.owner.clone()
+ }
+ }
+
+ impl User {
+ pub fn new(first_name: String, last_name: String) -> Self {
+ Self {
+ first_name,
+ last_name
+ }
+ }
+
+ pub fn first_name(&self) -> String {
+ self.first_name.clone()
+ }
+
+ pub fn last_name(&self) -> String {
+ self.last_name.clone()
+ }
+ }
+ "},
+ "main.rs": indoc!{r#"
+ fn main() {
+ let user = User::new(FIRST_NAME.clone(), "doe".into());
+ println!("user {:?}", user);
+ }
+ "#},
+ }),
+ )
+ .await;
+
+ let project = Project::test(fs, vec![Path::new(path!("/root"))], cx).await;
+ project.update(cx, |project, _cx| {
+ project.languages().add(rust_lang().into())
+ });
+
+ assert_results(
+ &project,
+ SearchToolQuery {
+ glob: "user.rs".into(),
+ syntax_node: vec!["impl\\s+User".into(), "pub\\s+fn\\s+first_name".into()],
+ content: None,
+ },
+ indoc! {r#"
+ `````root/user.rs
+ …
+ impl User {
+ …
+ pub fn first_name(&self) -> String {
+ self.first_name.clone()
+ }
+ …
+ `````
+ "#},
+ cx,
+ )
+ .await;
+
+ assert_results(
+ &project,
+ SearchToolQuery {
+ glob: "user.rs".into(),
+ syntax_node: vec!["impl\\s+User".into()],
+ content: Some("\\.clone".into()),
+ },
+ indoc! {r#"
+ `````root/user.rs
+ …
+ impl User {
+ …
+ pub fn first_name(&self) -> String {
+ self.first_name.clone()
+ …
+ pub fn last_name(&self) -> String {
+ self.last_name.clone()
+ …
+ `````
+ "#},
+ cx,
+ )
+ .await;
+
+ assert_results(
+ &project,
+ SearchToolQuery {
+ glob: "*.rs".into(),
+ syntax_node: vec![],
+ content: Some("\\.clone".into()),
+ },
+ indoc! {r#"
+ `````root/main.rs
+ fn main() {
+ let user = User::new(FIRST_NAME.clone(), "doe".into());
+ …
+ `````
+
+ `````root/user.rs
+ …
+ impl Organization {
+ pub fn owner(&self) -> Arc<User> {
+ self.owner.clone()
+ …
+ impl User {
+ …
+ pub fn first_name(&self) -> String {
+ self.first_name.clone()
+ …
+ pub fn last_name(&self) -> String {
+ self.last_name.clone()
+ …
+ `````
+ "#},
+ cx,
+ )
+ .await;
+ }
+
+ async fn assert_results(
+ project: &Entity<Project>,
+ query: SearchToolQuery,
+ expected_output: &str,
+ cx: &mut TestAppContext,
+ ) {
+ let results = run_retrieval_searches(project.clone(), vec![query], &mut cx.to_async())
+ .await
+ .unwrap();
+
+ let mut results = results.into_iter().collect::<Vec<_>>();
+ results.sort_by_key(|results| {
+ results
+ .0
+ .read_with(cx, |buffer, _| buffer.file().unwrap().path().clone())
+ });
+
+ let mut output = String::new();
+ for (buffer, ranges) in results {
+ buffer.read_with(cx, |buffer, cx| {
+ let excerpts = ranges.into_iter().map(|range| {
+ let point_range = range.to_point(buffer);
+ if point_range.end.column > 0 {
+ Line(point_range.start.row)..Line(point_range.end.row + 1)
+ } else {
+ Line(point_range.start.row)..Line(point_range.end.row)
+ }
+ });
+
+ write_codeblock(
+ &buffer.file().unwrap().full_path(cx),
+ merge_excerpts(&buffer.snapshot(), excerpts).iter(),
+ &[],
+ Line(buffer.max_point().row),
+ false,
+ &mut output,
+ );
+ });
+ }
+ output.pop();
+
+ assert_eq!(output, expected_output);
+ }
+
+ fn rust_lang() -> Language {
+ Language::new(
+ LanguageConfig {
+ name: "Rust".into(),
+ matcher: LanguageMatcher {
+ path_suffixes: vec!["rs".to_string()],
+ ..Default::default()
+ },
+ ..Default::default()
+ },
+ Some(tree_sitter_rust::LANGUAGE.into()),
+ )
+ .with_outline_query(include_str!("../../languages/src/rust/outline.scm"))
+ .unwrap()
+ }
+
+ fn init_test(cx: &mut TestAppContext) {
+ cx.update(move |cx| {
+ let settings_store = SettingsStore::test(cx);
+ cx.set_global(settings_store);
+ zlog::init_test();
+ });
+ }
+}
@@ -18,10 +18,10 @@ use language::{Anchor, Buffer, BufferSnapshot, OffsetRangeExt as _, TextBufferSn
use project::Project;
pub async fn parse_diff<'a>(
- diff: &'a str,
+ diff_str: &'a str,
get_buffer: impl Fn(&Path) -> Option<(&'a BufferSnapshot, &'a [Range<Anchor>])> + Send,
) -> Result<(&'a BufferSnapshot, Vec<(Range<Anchor>, Arc<str>)>)> {
- let mut diff = DiffParser::new(diff);
+ let mut diff = DiffParser::new(diff_str);
let mut edited_buffer = None;
let mut edits = Vec::new();
@@ -41,7 +41,10 @@ pub async fn parse_diff<'a>(
Some(ref current) => current,
};
- edits.extend(resolve_hunk_edits_in_buffer(hunk, &buffer.text, ranges)?);
+ edits.extend(
+ resolve_hunk_edits_in_buffer(hunk, &buffer.text, ranges)
+ .with_context(|| format!("Diff:\n{diff_str}"))?,
+ );
}
DiffEvent::FileEnd { renamed_to } => {
let (buffer, _) = edited_buffer
@@ -69,13 +72,13 @@ pub struct OpenedBuffers<'a>(#[allow(unused)] HashMap<Cow<'a, str>, Entity<Buffe
#[must_use]
pub async fn apply_diff<'a>(
- diff: &'a str,
+ diff_str: &'a str,
project: &Entity<Project>,
cx: &mut AsyncApp,
) -> Result<OpenedBuffers<'a>> {
let mut included_files = HashMap::default();
- for line in diff.lines() {
+ for line in diff_str.lines() {
let diff_line = DiffLine::parse(line);
if let DiffLine::OldPath { path } = diff_line {
@@ -97,7 +100,7 @@ pub async fn apply_diff<'a>(
let ranges = [Anchor::MIN..Anchor::MAX];
- let mut diff = DiffParser::new(diff);
+ let mut diff = DiffParser::new(diff_str);
let mut current_file = None;
let mut edits = vec![];
@@ -120,7 +123,10 @@ pub async fn apply_diff<'a>(
};
buffer.read_with(cx, |buffer, _| {
- edits.extend(resolve_hunk_edits_in_buffer(hunk, buffer, ranges)?);
+ edits.extend(
+ resolve_hunk_edits_in_buffer(hunk, buffer, ranges)
+ .with_context(|| format!("Diff:\n{diff_str}"))?,
+ );
anyhow::Ok(())
})??;
}
@@ -328,13 +334,7 @@ fn resolve_hunk_edits_in_buffer(
offset = Some(range.start + ix);
}
}
- offset.ok_or_else(|| {
- anyhow!(
- "Failed to match context:\n{}\n\nBuffer:\n{}",
- hunk.context,
- buffer.text(),
- )
- })
+ offset.ok_or_else(|| anyhow!("Failed to match context:\n{}", hunk.context))
}?;
let iter = hunk.edits.into_iter().flat_map(move |edit| {
let old_text = buffer
@@ -7,7 +7,7 @@ use cloud_llm_client::{
ZED_VERSION_HEADER_NAME,
};
use cloud_zeta2_prompt::DEFAULT_MAX_PROMPT_BYTES;
-use cloud_zeta2_prompt::retrieval_prompt::SearchToolInput;
+use cloud_zeta2_prompt::retrieval_prompt::{SearchToolInput, SearchToolQuery};
use collections::HashMap;
use edit_prediction_context::{
DeclarationId, DeclarationStyle, EditPredictionContext, EditPredictionContextOptions,
@@ -35,7 +35,7 @@ use uuid::Uuid;
use std::ops::Range;
use std::path::Path;
use std::str::FromStr as _;
-use std::sync::Arc;
+use std::sync::{Arc, LazyLock};
use std::time::{Duration, Instant};
use thiserror::Error;
use util::rel_path::RelPathBuf;
@@ -88,6 +88,9 @@ pub const DEFAULT_OPTIONS: ZetaOptions = ZetaOptions {
buffer_change_grouping_interval: Duration::from_secs(1),
};
+static MODEL_ID: LazyLock<String> =
+ LazyLock::new(|| std::env::var("ZED_ZETA2_MODEL").unwrap_or("yqvev8r3".to_string()));
+
pub struct Zeta2FeatureFlag;
impl FeatureFlag for Zeta2FeatureFlag {
@@ -180,7 +183,7 @@ pub struct ZetaEditPredictionDebugInfo {
pub struct ZetaSearchQueryDebugInfo {
pub project: Entity<Project>,
pub timestamp: Instant,
- pub regex_by_glob: HashMap<String, String>,
+ pub search_queries: Vec<SearchToolQuery>,
}
pub type RequestDebugInfo = predict_edits_v3::DebugInfo;
@@ -883,7 +886,7 @@ impl Zeta {
let (prompt, _) = prompt_result?;
let request = open_ai::Request {
- model: std::env::var("ZED_ZETA2_MODEL").unwrap_or("yqvev8r3".to_string()),
+ model: MODEL_ID.clone(),
messages: vec![open_ai::RequestMessage::User {
content: open_ai::MessageContent::Plain(prompt),
}],
@@ -1226,10 +1229,24 @@ impl Zeta {
.ok();
}
- let (tool_schema, tool_description) = &*cloud_zeta2_prompt::retrieval_prompt::TOOL_SCHEMA;
+ pub static TOOL_SCHEMA: LazyLock<(serde_json::Value, String)> = LazyLock::new(|| {
+ let schema = language_model::tool_schema::root_schema_for::<SearchToolInput>(
+ language_model::LanguageModelToolSchemaFormat::JsonSchemaSubset,
+ );
+
+ let description = schema
+ .get("description")
+ .and_then(|description| description.as_str())
+ .unwrap()
+ .to_string();
+
+ (schema.into(), description)
+ });
+
+ let (tool_schema, tool_description) = TOOL_SCHEMA.clone();
let request = open_ai::Request {
- model: std::env::var("ZED_ZETA2_MODEL").unwrap_or("2327jz9q".to_string()),
+ model: MODEL_ID.clone(),
messages: vec![open_ai::RequestMessage::User {
content: open_ai::MessageContent::Plain(prompt),
}],
@@ -1242,8 +1259,8 @@ impl Zeta {
tools: vec![open_ai::ToolDefinition::Function {
function: FunctionDefinition {
name: cloud_zeta2_prompt::retrieval_prompt::TOOL_NAME.to_string(),
- description: Some(tool_description.clone()),
- parameters: Some(tool_schema.clone()),
+ description: Some(tool_description),
+ parameters: Some(tool_schema),
},
}],
prompt_cache_key: None,
@@ -1255,7 +1272,6 @@ impl Zeta {
let response =
Self::send_raw_llm_request(client, llm_token, app_version, request).await;
let mut response = Self::handle_api_response(&this, response, cx)?;
-
log::trace!("Got search planning response");
let choice = response
@@ -1270,7 +1286,7 @@ impl Zeta {
anyhow::bail!("Retrieval response didn't include an assistant message");
};
- let mut regex_by_glob: HashMap<String, String> = HashMap::default();
+ let mut queries: Vec<SearchToolQuery> = Vec::new();
for tool_call in tool_calls {
let open_ai::ToolCallContent::Function { function } = tool_call.content;
if function.name != cloud_zeta2_prompt::retrieval_prompt::TOOL_NAME {
@@ -1283,13 +1299,7 @@ impl Zeta {
}
let input: SearchToolInput = serde_json::from_str(&function.arguments)?;
- for query in input.queries {
- let regex = regex_by_glob.entry(query.glob).or_default();
- if !regex.is_empty() {
- regex.push('|');
- }
- regex.push_str(&query.regex);
- }
+ queries.extend(input.queries);
}
if let Some(debug_tx) = &debug_tx {
@@ -1298,16 +1308,16 @@ impl Zeta {
ZetaSearchQueryDebugInfo {
project: project.clone(),
timestamp: Instant::now(),
- regex_by_glob: regex_by_glob.clone(),
+ search_queries: queries.clone(),
},
))
.ok();
}
- log::trace!("Running retrieval search: {regex_by_glob:#?}");
+ log::trace!("Running retrieval search: {queries:#?}");
let related_excerpts_result =
- retrieval_search::run_retrieval_searches(project.clone(), regex_by_glob, cx).await;
+ retrieval_search::run_retrieval_searches(project.clone(), queries, cx).await;
log::trace!("Search queries executed");
@@ -1754,7 +1764,8 @@ mod tests {
arguments: serde_json::to_string(&SearchToolInput {
queries: Box::new([SearchToolQuery {
glob: "root/2.txt".to_string(),
- regex: ".".to_string(),
+ syntax_node: vec![],
+ content: Some(".".into()),
}]),
})
.unwrap(),
@@ -16,6 +16,7 @@ anyhow.workspace = true
chrono.workspace = true
client.workspace = true
cloud_llm_client.workspace = true
+cloud_zeta2_prompt.workspace = true
collections.workspace = true
edit_prediction_context.workspace = true
editor.workspace = true
@@ -27,7 +28,6 @@ log.workspace = true
multi_buffer.workspace = true
ordered-float.workspace = true
project.workspace = true
-regex-syntax = "0.8.8"
serde.workspace = true
serde_json.workspace = true
telemetry.workspace = true
@@ -8,6 +8,7 @@ use std::{
use anyhow::Result;
use client::{Client, UserStore};
+use cloud_zeta2_prompt::retrieval_prompt::SearchToolQuery;
use editor::{Editor, PathKey};
use futures::StreamExt as _;
use gpui::{
@@ -41,19 +42,13 @@ pub struct Zeta2ContextView {
#[derive(Debug)]
struct RetrievalRun {
editor: Entity<Editor>,
- search_queries: Vec<GlobQueries>,
+ search_queries: Vec<SearchToolQuery>,
started_at: Instant,
search_results_generated_at: Option<Instant>,
search_results_executed_at: Option<Instant>,
finished_at: Option<Instant>,
}
-#[derive(Debug)]
-struct GlobQueries {
- glob: String,
- alternations: Vec<String>,
-}
-
actions!(
dev,
[
@@ -210,23 +205,7 @@ impl Zeta2ContextView {
};
run.search_results_generated_at = Some(info.timestamp);
- run.search_queries = info
- .regex_by_glob
- .into_iter()
- .map(|(glob, regex)| {
- let mut regex_parser = regex_syntax::ast::parse::Parser::new();
-
- GlobQueries {
- glob,
- alternations: match regex_parser.parse(®ex) {
- Ok(regex_syntax::ast::Ast::Alternation(ref alt)) => {
- alt.asts.iter().map(|ast| ast.to_string()).collect()
- }
- _ => vec![regex],
- },
- }
- })
- .collect();
+ run.search_queries = info.search_queries;
cx.notify();
}
@@ -292,18 +271,28 @@ impl Zeta2ContextView {
.enumerate()
.flat_map(|(ix, query)| {
std::iter::once(ListHeader::new(query.glob.clone()).into_any_element())
- .chain(query.alternations.iter().enumerate().map(
- move |(alt_ix, alt)| {
- ListItem::new(ix * 100 + alt_ix)
+ .chain(query.syntax_node.iter().enumerate().map(
+ move |(regex_ix, regex)| {
+ ListItem::new(ix * 100 + regex_ix)
.start_slot(
Icon::new(IconName::MagnifyingGlass)
.color(Color::Muted)
.size(IconSize::Small),
)
- .child(alt.clone())
+ .child(regex.clone())
.into_any_element()
},
))
+ .chain(query.content.as_ref().map(move |regex| {
+ ListItem::new(ix * 100 + query.syntax_node.len())
+ .start_slot(
+ Icon::new(IconName::MagnifyingGlass)
+ .color(Color::Muted)
+ .size(IconSize::Small),
+ )
+ .child(regex.clone())
+ .into_any_element()
+ }))
}),
),
)
@@ -2,11 +2,11 @@ use crate::example::{ActualExcerpt, NamedExample};
use crate::headless::ZetaCliAppState;
use crate::paths::LOGS_DIR;
use ::serde::Serialize;
-use anyhow::{Context as _, Result, anyhow};
+use anyhow::{Result, anyhow};
use clap::Args;
use cloud_zeta2_prompt::{CURSOR_MARKER, write_codeblock};
use futures::StreamExt as _;
-use gpui::AsyncApp;
+use gpui::{AppContext, AsyncApp};
use project::Project;
use serde::Deserialize;
use std::cell::Cell;
@@ -14,6 +14,7 @@ use std::fs;
use std::io::Write;
use std::path::PathBuf;
use std::sync::Arc;
+use std::sync::Mutex;
use std::time::{Duration, Instant};
#[derive(Debug, Args)]
@@ -103,112 +104,126 @@ pub async fn zeta2_predict(
let _edited_buffers = example.apply_edit_history(&project, cx).await?;
let (cursor_buffer, cursor_anchor) = example.cursor_position(&project, cx).await?;
+ let result = Arc::new(Mutex::new(PredictionDetails::default()));
let mut debug_rx = zeta.update(cx, |zeta, _| zeta.debug_info())?;
- let refresh_task = zeta.update(cx, |zeta, cx| {
- zeta.refresh_context(project.clone(), cursor_buffer.clone(), cursor_anchor, cx)
- })?;
+ let debug_task = cx.background_spawn({
+ let result = result.clone();
+ async move {
+ let mut context_retrieval_started_at = None;
+ let mut context_retrieval_finished_at = None;
+ let mut search_queries_generated_at = None;
+ let mut search_queries_executed_at = None;
+ while let Some(event) = debug_rx.next().await {
+ match event {
+ zeta2::ZetaDebugInfo::ContextRetrievalStarted(info) => {
+ context_retrieval_started_at = Some(info.timestamp);
+ fs::write(LOGS_DIR.join("search_prompt.md"), &info.search_prompt)?;
+ }
+ zeta2::ZetaDebugInfo::SearchQueriesGenerated(info) => {
+ search_queries_generated_at = Some(info.timestamp);
+ fs::write(
+ LOGS_DIR.join("search_queries.json"),
+ serde_json::to_string_pretty(&info.search_queries).unwrap(),
+ )?;
+ }
+ zeta2::ZetaDebugInfo::SearchQueriesExecuted(info) => {
+ search_queries_executed_at = Some(info.timestamp);
+ }
+ zeta2::ZetaDebugInfo::ContextRetrievalFinished(info) => {
+ context_retrieval_finished_at = Some(info.timestamp);
+ }
+ zeta2::ZetaDebugInfo::EditPredictionRequested(request) => {
+ let prediction_started_at = Instant::now();
+ fs::write(
+ LOGS_DIR.join("prediction_prompt.md"),
+ &request.local_prompt.unwrap_or_default(),
+ )?;
- let mut context_retrieval_started_at = None;
- let mut context_retrieval_finished_at = None;
- let mut search_queries_generated_at = None;
- let mut search_queries_executed_at = None;
- let mut prediction_started_at = None;
- let mut prediction_finished_at = None;
- let mut excerpts_text = String::new();
- let mut prediction_task = None;
- let mut result = PredictionDetails::default();
- while let Some(event) = debug_rx.next().await {
- match event {
- zeta2::ZetaDebugInfo::ContextRetrievalStarted(info) => {
- context_retrieval_started_at = Some(info.timestamp);
- fs::write(LOGS_DIR.join("search_prompt.md"), &info.search_prompt)?;
- }
- zeta2::ZetaDebugInfo::SearchQueriesGenerated(info) => {
- search_queries_generated_at = Some(info.timestamp);
- fs::write(
- LOGS_DIR.join("search_queries.json"),
- serde_json::to_string_pretty(&info.regex_by_glob).unwrap(),
- )?;
- }
- zeta2::ZetaDebugInfo::SearchQueriesExecuted(info) => {
- search_queries_executed_at = Some(info.timestamp);
- }
- zeta2::ZetaDebugInfo::ContextRetrievalFinished(info) => {
- context_retrieval_finished_at = Some(info.timestamp);
+ {
+ let mut result = result.lock().unwrap();
- prediction_task = Some(zeta.update(cx, |zeta, cx| {
- zeta.request_prediction(&project, &cursor_buffer, cursor_anchor, cx)
- })?);
- }
- zeta2::ZetaDebugInfo::EditPredictionRequested(request) => {
- prediction_started_at = Some(Instant::now());
- fs::write(
- LOGS_DIR.join("prediction_prompt.md"),
- &request.local_prompt.unwrap_or_default(),
- )?;
+ for included_file in request.request.included_files {
+ let insertions =
+ vec![(request.request.cursor_point, CURSOR_MARKER)];
+ result.excerpts.extend(included_file.excerpts.iter().map(
+ |excerpt| ActualExcerpt {
+ path: included_file.path.components().skip(1).collect(),
+ text: String::from(excerpt.text.as_ref()),
+ },
+ ));
+ write_codeblock(
+ &included_file.path,
+ included_file.excerpts.iter(),
+ if included_file.path == request.request.excerpt_path {
+ &insertions
+ } else {
+ &[]
+ },
+ included_file.max_row,
+ false,
+ &mut result.excerpts_text,
+ );
+ }
+ }
- for included_file in request.request.included_files {
- let insertions = vec![(request.request.cursor_point, CURSOR_MARKER)];
- result
- .excerpts
- .extend(included_file.excerpts.iter().map(|excerpt| ActualExcerpt {
- path: included_file.path.components().skip(1).collect(),
- text: String::from(excerpt.text.as_ref()),
- }));
- write_codeblock(
- &included_file.path,
- included_file.excerpts.iter(),
- if included_file.path == request.request.excerpt_path {
- &insertions
- } else {
- &[]
- },
- included_file.max_row,
- false,
- &mut excerpts_text,
- );
- }
+ let response = request.response_rx.await?.0.map_err(|err| anyhow!(err))?;
+ let response = zeta2::text_from_response(response).unwrap_or_default();
+ let prediction_finished_at = Instant::now();
+ fs::write(LOGS_DIR.join("prediction_response.md"), &response)?;
- let response = request.response_rx.await?.0.map_err(|err| anyhow!(err))?;
- let response = zeta2::text_from_response(response).unwrap_or_default();
- prediction_finished_at = Some(Instant::now());
- fs::write(LOGS_DIR.join("prediction_response.md"), &response)?;
+ let mut result = result.lock().unwrap();
- break;
+ result.planning_search_time = search_queries_generated_at.unwrap()
+ - context_retrieval_started_at.unwrap();
+ result.running_search_time = search_queries_executed_at.unwrap()
+ - search_queries_generated_at.unwrap();
+ result.filtering_search_time = context_retrieval_finished_at.unwrap()
+ - search_queries_executed_at.unwrap();
+ result.prediction_time = prediction_finished_at - prediction_started_at;
+ result.total_time =
+ prediction_finished_at - context_retrieval_started_at.unwrap();
+
+ break;
+ }
+ }
}
+ anyhow::Ok(())
}
- }
+ });
+
+ zeta.update(cx, |zeta, cx| {
+ zeta.refresh_context(project.clone(), cursor_buffer.clone(), cursor_anchor, cx)
+ })?
+ .await?;
- refresh_task.await.context("context retrieval failed")?;
- let prediction = prediction_task.unwrap().await?;
+ let prediction = zeta
+ .update(cx, |zeta, cx| {
+ zeta.request_prediction(&project, &cursor_buffer, cursor_anchor, cx)
+ })?
+ .await?;
+ debug_task.await?;
+
+ let mut result = Arc::into_inner(result).unwrap().into_inner().unwrap();
result.diff = prediction
.map(|prediction| {
let old_text = prediction.snapshot.text();
- let new_text = prediction.buffer.update(cx, |buffer, cx| {
- buffer.edit(prediction.edits.iter().cloned(), None, cx);
- buffer.text()
- })?;
- anyhow::Ok(language::unified_diff(&old_text, &new_text))
+ let new_text = prediction
+ .buffer
+ .update(cx, |buffer, cx| {
+ buffer.edit(prediction.edits.iter().cloned(), None, cx);
+ buffer.text()
+ })
+ .unwrap();
+ language::unified_diff(&old_text, &new_text)
})
- .transpose()?
.unwrap_or_default();
- result.excerpts_text = excerpts_text;
-
- result.planning_search_time =
- search_queries_generated_at.unwrap() - context_retrieval_started_at.unwrap();
- result.running_search_time =
- search_queries_executed_at.unwrap() - search_queries_generated_at.unwrap();
- result.filtering_search_time =
- context_retrieval_finished_at.unwrap() - search_queries_executed_at.unwrap();
- result.prediction_time = prediction_finished_at.unwrap() - prediction_started_at.unwrap();
- result.total_time = prediction_finished_at.unwrap() - context_retrieval_started_at.unwrap();
anyhow::Ok(result)
}
-#[derive(Debug, Default, Serialize, Deserialize)]
+#[derive(Clone, Debug, Default, Serialize, Deserialize)]
pub struct PredictionDetails {
pub diff: String,
pub excerpts: Vec<ActualExcerpt>,
@@ -2,9 +2,12 @@
## Overview {#overview}
-Text threads in the [Agent Panel](./agent-panel.md) function similarly to any other editor. You can use custom key bindings and work with multiple cursors, allowing for seamless transitions between coding and engaging in discussions with the language models.
+Text threads in the [Agent Panel](./agent-panel.md) function similarly to any other editor.
+You can use custom key bindings and work with multiple cursors, allowing for seamless transitions between coding and engaging in discussions with the language models.
-However, the text threads differ with the inclusion of message blocks. These blocks serve as containers for text that correspond to different roles within the context. These roles include:
+However, the text threads differ in the inclusion of message blocks.
+These blocks serve as containers for text that correspond to different roles within the context.
+These roles include:
- `You`
- `Assistant`
@@ -20,24 +23,29 @@ Inserting text from an editor is as simple as highlighting the text and running

-To submit a message, use {#kb assistant::Assist}(`assistant: assist`). Unlike normal threads, where pressing <kbd>enter</kbd> would submit the message, in text threads, our goal is to make it feel as close to a regular editor as possible. So, pressing {#kb editor::Newline} simply inserts a new line.
+To submit a message, use {#kb assistant::Assist}(`assistant: assist`).
+Unlike normal threads, where pressing <kbd>enter</kbd> would submit the message, in text threads, our goal is to make it feel as close to a regular editor as possible.
+So, pressing {#kb editor::Newline} simply inserts a new line.
After submitting a message, the response will be streamed below, in an `Assistant` message block.

-The stream can be canceled at any point with <kbd>escape</kbd>. This is useful if you realize early on that the response is not what you were looking for.
+The stream can be canceled at any point with <kbd>escape</kbd>.
+This is useful if you realize early on that the response is not what you were looking for.
If you want to start a new conversation at any time, you can hit <kbd>cmd-n|ctrl-n</kbd> or use the `New Chat` menu option in the hamburger menu at the top left of the panel.
-Simple back-and-forth conversations work well with the text threads. However, there may come a time when you want to modify the previous text in the conversation and steer it in a different direction.
+Simple back-and-forth conversations work well with the text threads.
+However, there may come a time when you want to modify the previous text in the conversation and steer it in a different direction.
## Editing a Text Thread {#edit-text-thread}
Text threads give you the flexibility to have control over the context.
You can freely edit any previous text, including the responses from the LLM.
If you want to remove a message block entirely, simply place your cursor at the beginning of the block and use the `delete` key.
-A typical workflow might involve making edits and adjustments throughout the context to refine your inquiry or provide additional information. Here's an example:
+A typical workflow might involve making edits and adjustments throughout the context to refine your inquiry or provide additional information.
+Here's an example:
1. Write text in a `You` block.
2. Submit the message with {#kb assistant::Assist}.
@@ -47,7 +55,8 @@ A typical workflow might involve making edits and adjustments throughout the con
6. Add additional context to your original message.
7. Submit the message with {#kb assistant::Assist}.
-Being able to edit previous messages gives you control over how tokens are used. You don't need to start up a new chat to correct a mistake or to add additional information, and you don't have to waste tokens by submitting follow-up corrections.
+Being able to edit previous messages gives you control over how tokens are used.
+You don't need to start up a new chat to correct a mistake or to add additional information, and you don't have to waste tokens by submitting follow-up corrections.
> **Note**: The act of editing past messages is often referred to as "Rewriting History" in the context of the language models.
@@ -57,7 +66,8 @@ Some additional points to keep in mind:
## Commands Overview {#commands}
-Slash commands enhance the assistant's capabilities. Begin by typing a `/` at the beginning of the line to see a list of available commands:
+Slash commands enhance the assistant's capabilities.
+Begin by typing a `/` at the beginning of the line to see a list of available commands:
- `/default`: Inserts the default rule
- `/diagnostics`: Injects errors reported by the project's language server
@@ -80,7 +90,8 @@ Usage: `/default`
### `/diagnostics`
-The `/diagnostics` command injects errors reported by the project's language server into the context. This is useful for getting an overview of current issues in your project.
+The `/diagnostics` command injects errors reported by the project's language server into the context.
+This is useful for getting an overview of current issues in your project.
Usage: `/diagnostics [--include-warnings] [path]`
@@ -89,7 +100,8 @@ Usage: `/diagnostics [--include-warnings] [path]`
### `/file`
-The `/file` command inserts the content of a single file or a directory of files into the context. This allows you to reference specific parts of your project in your conversation with the assistant.
+The `/file` command inserts the content of a single file or a directory of files into the context.
+This allows you to reference specific parts of your project in your conversation with the assistant.
Usage: `/file <path>`
@@ -103,13 +115,15 @@ Examples:
### `/now`
-The `/now` command inserts the current date and time into the context. This can be useful letting the language model know the current time (and by extension, how old their current knowledge base is).
+The `/now` command inserts the current date and time into the context.
+This can be useful for letting the language model know the current time (and by extension, how old their current knowledge base is).
Usage: `/now`
### `/prompt`
-The `/prompt` command inserts a prompt from the prompt library into the context. It can also be used to nest prompts within prompts.
+The `/prompt` command inserts a prompt from the prompt library into the context.
+It can also be used to nest prompts within prompts.
Usage: `/prompt <prompt_name>`
@@ -117,13 +131,15 @@ Related: `/default`
### `/symbols`
-The `/symbols` command inserts the active symbols (functions, classes, etc.) from the current tab into the context. This is useful for getting an overview of the structure of the current file.
+The `/symbols` command inserts the active symbols (functions, classes, etc.) from the current tab into the context.
+This is useful for getting an overview of the structure of the current file.
Usage: `/symbols`
### `/tab`
-The `/tab` command inserts the content of the active tab or all open tabs into the context. This allows you to reference the content you're currently working on.
+The `/tab` command inserts the content of the active tab or all open tabs into the context.
+This allows you to reference the content you're currently working on.
Usage: `/tab [tab_name|all]`
@@ -138,15 +154,17 @@ Examples:
### `/terminal`
-The `/terminal` command inserts a select number of lines of output from the terminal into the context. This is useful for referencing recent command outputs or logs.
+The `/terminal` command inserts a select number of lines of output from the terminal into the context.
+This is useful for referencing recent command outputs or logs.
Usage: `/terminal [<number>]`
-- `<number>`: Optional parameter to specify the number of lines to insert (default is a 50).
+- `<number>`: Optional parameter to specify the number of lines to insert (default is 50).
### `/selection`
-The `/selection` command inserts the selected text in the editor into the context. This is useful for referencing specific parts of your code.
+The `/selection` command inserts the selected text in the editor into the context.
+This is useful for referencing specific parts of your code.
This is equivalent to the `agent: add selection to thread` command ({#kb agent::AddSelectionToThread}).
@@ -173,7 +191,7 @@ Here is some information about their project:
/file Cargo.toml
```
-In the above example, the `@file` command is used to insert the contents of the `Cargo.toml` file (or all `Cargo.toml` files present in the project) into the rule.
+In the above example, the `/file` command is used to insert the contents of the `Cargo.toml` file (or all `Cargo.toml` files present in the project) into the rule.
## Nesting Rules
@@ -185,7 +203,7 @@ You might want to nest rules to:
- Break collections like docs or references into smaller, mix-and-matchable parts
- Create variants of a similar rule (e.g., `Async Rust - Tokio` vs. `Async Rust - Async-std`)
-### Example:
+### Example
```plaintext
Title: Zed-Flavored Rust
@@ -215,6 +233,17 @@ Additional slash commands can be provided by extensions.
See [Extension: Slash Commands](../extensions/slash-commands.md) to learn how to create your own.
+## Text Threads vs. Threads
+
+For a while, text threads were the only way to interact with AI in Zed.
+We have since introduced, back in May 2025, a new take on the agent panel, which, as opposed to being editor-driven, optimizes for readability.
+You can read more about it in [the Agent Panel page](./agent-panel.md).
+
+However, aside from many interaction differences, the major difference between one vs. the other is that tool calls don't work in Text Threads.
+So, it's accurate to say that Text Threads aren't necessarily "agentic", as they can't perform any action on your behalf.
+Think of it more like a regular and "traditional" AI chat, where what you'll get out of the model is simply just text.
+Consequently, [external agents](./external-agents.md) are also not available in Text Threads.
+
## Advanced Concepts
### Rule Templates {#rule-templates}
@@ -240,9 +269,11 @@ The following templates can be overridden:
2. [`terminal_assistant_prompt.hbs`](https://github.com/zed-industries/zed/tree/main/assets/prompts/terminal_assistant_prompt.hbs): Used for the terminal assistant feature.
-> **Note:** Be sure you want to override these, as you'll miss out on iteration on our built-in features. This should be primarily used when developing Zed.
+> **Note:** Be sure you want to override these, as you'll miss out on iteration on our built-in features.
+> This should be primarily used when developing Zed.
-You can customize these templates to better suit your needs while maintaining the core structure and variables used by Zed. Zed will automatically reload your prompt overrides when they change on disk.
+You can customize these templates to better suit your needs while maintaining the core structure and variables used by Zed.
+Zed will automatically reload your prompt overrides when they change on disk.
Consult Zed's [assets/prompts](https://github.com/zed-industries/zed/tree/main/assets/prompts) directory for current versions you can play with.
@@ -1,12 +1,14 @@
# Tools
-Zed's Agent has access to a variety of tools that allow it to interact with your codebase and perform tasks.
+Zed's built-in agent has access to a variety of tools that allow it to interact with your codebase and perform tasks.
## Read & Search Tools
### `diagnostics`
Gets errors and warnings for either a specific file or the entire project, useful after making edits to determine if further changes are needed.
+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.
### `fetch`
@@ -54,10 +56,6 @@ Copies a file or directory recursively in the project, more efficient than manua
Creates a new directory at the specified path within the project, creating all necessary parent directories (similar to `mkdir -p`).
-### `create_file`
-
-Creates a new file at a specified path with given text content, the most efficient way to create new files or completely replace existing ones.
-
### `delete_path`
Deletes a file or directory (including contents recursively) at the specified path and confirms the deletion.
@@ -929,6 +929,8 @@ List of `string` values
- Setting: `cursors`
- Default: `true`
+Cursor indicators appear as small marks on the scrollbar showing where other collaborators' cursors are positioned in the file.
+
**Options**
`boolean` values
@@ -939,6 +941,8 @@ List of `string` values
- Setting: `git_diff`
- Default: `true`
+Git diff indicators appear as colored marks showing lines that have been added, modified, or deleted compared to the git HEAD.
+
**Options**
`boolean` values
@@ -949,6 +953,8 @@ List of `string` values
- Setting: `search_results`
- Default: `true`
+Search result indicators appear as marks showing all locations in the file where your current search query matches.
+
**Options**
`boolean` values
@@ -959,6 +965,8 @@ List of `string` values
- Setting: `selected_text`
- Default: `true`
+Selected text indicators appear as marks showing all occurrences of the currently selected text throughout the file.
+
**Options**
`boolean` values
@@ -969,6 +977,8 @@ List of `string` values
- Setting: `selected_symbol`
- Default: `true`
+Selected symbol indicators appear as marks showing all occurrences of the currently selected symbol (like a function or variable name) throughout the file.
+
**Options**
`boolean` values
@@ -979,6 +989,8 @@ List of `string` values
- Setting: `diagnostics`
- Default: `all`
+Diagnostic indicators appear as colored marks showing errors, warnings, and other language server diagnostics at their corresponding line positions in the file.
+
**Options**
1. Show all diagnostics: