Compare commits

..

25 Commits

Author SHA1 Message Date
Conrad Irwin
c48382d1a2 Fix panic in git conflict parsing 2025-06-04 16:59:58 -06:00
Oleksiy Syvokon
cde47e60cd assistant_tools: Disallow extra tool parameters by default (#32081)
This prevents models from hallucinating tool parameters.


Release Notes:

- Prevent models from hallucinating tool parameters
2025-06-04 16:11:40 +00:00
Peter Tripp
79f96a5afe docs: Improve LuaLS formatter example (#32084)
- Closes https://github.com/zed-extensions/lua/issues/4

Release Notes:

- N/A
2025-06-04 11:51:53 -04:00
Tommy D. Rossi
81058ee172 Make alt-left and alt-right skip punctuation like VSCode (#31977)
Closes https://github.com/zed-industries/zed/discussions/25526
Follow up of #29872

Release Notes:

- Make `alt-left` and `alt-right` skip punctuation on Mac OS to respect
the Mac default behaviour. When pressing alt-left and the first
character is a punctuation character like a dot, this character should
be skipped. For example: `hello.|` goes to `|hello.`

This change makes the editor feels much snappier, it now follows the
same behaviour as VSCode and any other Mac OS native application.


@ConradIrwin
2025-06-04 09:48:20 -06:00
Alejandro Fernández Gómez
89743117c6 vim: Add Ctrl-w ] and Ctrl-w Ctrl-] keymaps (#31990)
Closes #31989

Release Notes:

- Added support for `Ctrl-w ]` and `Ctrl-w Ctrl-]` to go to a definition
in a new split
2025-06-04 09:47:42 -06:00
Conrad Irwin
6de37fa57c Don't show squiggles on unnecesarry code (#32082)
Co-Authored-By: @davidhewitt <mail@davidhewitt.dev>

Closes #31747
Closes https://github.com/zed-industries/zed/issues/32080

Release Notes:

- Fixed a recently introduced bug where unnecessary code was underlined
with blue squiggles

Co-authored-by: @davidhewitt <mail@davidhewitt.dev>
2025-06-04 09:46:06 -06:00
Bennet Bo Fenner
beb0d49dc4 agent: Introduce ModelUsageContext (#32076)
This PR is a refactor of the existing `ModelType` in
`agent_model_selector`.

In #31848 we also need to know which context we are operating in, to
check if the configured model has image support.
In order to deduplicate the logic needed, I introduced a new type called
`ModelUsageContext` which can be used throughout the agent crate


Release Notes:

- N/A
2025-06-04 15:35:50 +00:00
Conrad Irwin
c9aadadc4b Add a script to connect to the database. (#32023)
This avoids needing passwords in plaintext on the command line....

Release Notes:

- N/A
2025-06-04 09:23:23 -06:00
Conrad Irwin
bcd182f480 A script to help with PR naggery (#32025)
Release Notes:

- N/A
2025-06-04 09:23:14 -06:00
Joseph T. Lyons
3987b60738 Set upstream tracking when pushing preview branch (#32075)
Release Notes:

- N/A
2025-06-04 10:42:50 -04:00
Joseph T. Lyons
827103908e Bump Zed to v0.191 (#32073)
Release Notes:

-N/A
2025-06-04 14:34:01 +00:00
Vitaly Slobodin
8e9e3ba1a5 ruby: Add sorbet and steep to the list of available language servers (#32008)
Hi, this pull request adds `sorbet` and `steep` to the list of available
language servers for the Ruby language in order to prepare default Ruby
language settings for these LS. Both language servers are disabled by
default. We plan to add both in #104 and #102. Thanks!

Release Notes:

- ruby: Added `sorbet` and `steep` to the list of available language servers.
2025-06-04 10:19:33 -04:00
Danilo Leal
676ed8fb8a agent: Use new has_pending_edit_tool_use state for toolbar review buttons (#32071)
Follow up to https://github.com/zed-industries/zed/pull/31971. Now, the
toolbar review buttons will also appear/be available at the same time as
the panel buttons.

Release Notes:

- N/A
2025-06-04 11:14:34 -03:00
Ben Brandt
4304521655 Remove unused load_model method from LanguageModelProvider (#32070)
Removes the load_model trait method and its implementations in Ollama
and LM Studio providers, along with associated preload_model functions
and unused imports.

Release Notes:

- N/A
2025-06-04 14:07:01 +00:00
Oleksiy Syvokon
04716a0e4a edit_file_tool: Fail when edit location is not unique (#32056)
When `<old_text>` points to more than one location in a file, we used to
edit the first match, confusing the agent along the way. Now we will
return an error, asking to expand `<old_text>` selection.

Closes #ISSUE

Release Notes:

- agent: Fixed incorrect file edits when edit locations are ambiguous
2025-06-04 13:04:01 +03:00
Kirill Bulatov
5e38915d45 Properly register buffers with reused language servers (#32057)
Follow-up of https://github.com/zed-industries/zed/pull/30707

The old code does something odd, re-accessing essentially the same
adapter-server pair for every language server initialized; but that was
done before for "incorrect", non-reused worktree_id hence never resulted
in external worktrees' files registration in this code path.

Release Notes:

- Fixed certain external worktrees' files sometimes not registered with
language servers
2025-06-04 09:59:57 +00:00
Alex
f9257b0efe debugger: Use UUID for Go debug binary names, do not rely on OUT_DIR (#32004)
It seems that there was a regression. `build_config` no longer has an
`OUT_DIR` in it.
On way to mitigate it is to stop relying on it and just use `cwd` as dir
for the test binary to be placed in.

Release Notes:
- N/A

---------

Co-authored-by: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com>
2025-06-04 11:18:04 +02:00
Wanten
5d0c96872b editor: Stabilize IME candidate box position during pre-edit on Wayland (#28429)
Modify the `replace_and_mark_text_in_range` method in the `Editor` to
keep the cursor at the start of the preedit range during IME
composition. Previously, the cursor would move to the end of the preedit
text with each update, causing the IME candidate box to shift (e.g.,
when typing pinyin with Fcitx5 on Wayland). This change ensures the
cursor and candidate box remain fixed until the composition is
committed, improving the IME experience.

Closes #21004

Release Notes:

- N/A

---------

Co-authored-by: Mikayla Maki <mikayla.c.maki@gmail.com>
Co-authored-by: 张小白 <364772080@qq.com>
2025-06-04 09:14:01 +00:00
Umesh Yadav
071e684be4 bedrock: Fix ci failure due model enum and model name mismatch (#32049)
Release Notes:

- N/A
2025-06-04 10:41:12 +03:00
Shardul Vaidya
2280594408 bedrock: Allow users to pick Thinking vs. Non-Thinking models (#31600)
Release Notes:

- bedrock: Added ability to pick between Thinking and Non-Thinking models
2025-06-04 09:00:41 +03:00
Shardul Vaidya
09a1d51e9a bedrock: Fix Claude 4 output token bug (#31599)
Release Notes:

- Fixed an issue preventing the use of Claude 4 Thinking models with Bedrock
2025-06-04 08:57:31 +03:00
Umesh Yadav
ac15194d11 docs: Add OpenRouter agent support (#32011)
Update few other docs as well. Like recently tool support was added for
deepseek. Also there was recent thinking and images support for ollama
model.

Release Notes:

- N/A
2025-06-04 08:54:00 +03:00
Smit Barmase
988d834c33 project_panel: When initiating a drag the highlight selection should jump to the item you've picked up (#32044)
Closes #14496.

In https://github.com/zed-industries/zed/pull/31976, we modified the
highlighting behavior for entries when certain entries or paths are
being dragged over them. Instead of relying on marked entries for
highlighting, we introduced the `highlight_entry_id` parameter, which
determines which entry and its children should be highlighted when an
item is being dragged over it.

The rationale behind that is that we can now utilize marked entries for
various other functions, such as:

1. When dragging multiple items, we use marked entried to show which
items are being dragged. (This is already covered because to drag
multiple items, you need to use marked entries.)
2. When dragging a single item, set that item to marked entries. (This
PR)


https://github.com/user-attachments/assets/8a03bdd4-b5db-467d-b70f-53d9766fec52

Release Notes:

- Added highlighting to entries being dragged in the Project Panel,
indicating which items are being moved.
2025-06-04 08:30:51 +05:30
Michael Sloan
48eacf3f2a Add #[track_caller] to test utilities that involve marked text (#32043)
Release Notes:

- N/A
2025-06-04 02:37:27 +00:00
Smit Barmase
030d4d2631 project_panel: Holding alt or shift to copy the file should adds a green (+) icon to the mouse cursor (#32040)
Part of https://github.com/zed-industries/zed/issues/14496

Depends on new API https://github.com/zed-industries/zed/pull/32028

Holding `alt` or `shift` to copy the file should add a green (+) icon to
the mouse cursor to indicate this is a copy operation.

1. Press `option` first, then drag:


https://github.com/user-attachments/assets/ae58c441-f1ab-423e-be59-a8ec5cba33b0

2. Drag first, then press `option`:


https://github.com/user-attachments/assets/5136329f-9396-4ab9-a799-07d69cec89e2

Release Notes:

- Added copy-drag cursor when pressing Alt or Shift to copy the file in
Project Panel.
2025-06-04 07:16:56 +05:30
41 changed files with 1008 additions and 497 deletions

3
Cargo.lock generated
View File

@@ -12113,6 +12113,7 @@ dependencies = [
"unindent",
"url",
"util",
"uuid",
"which 6.0.3",
"workspace-hack",
"worktree",
@@ -19707,7 +19708,7 @@ dependencies = [
[[package]]
name = "zed"
version = "0.190.0"
version = "0.191.0"
dependencies = [
"activity_indicator",
"agent",

View File

@@ -198,6 +198,8 @@
"9": ["vim::Number", 9],
"ctrl-w d": "editor::GoToDefinitionSplit",
"ctrl-w g d": "editor::GoToDefinitionSplit",
"ctrl-w ]": "editor::GoToDefinitionSplit",
"ctrl-w ctrl-]": "editor::GoToDefinitionSplit",
"ctrl-w shift-d": "editor::GoToTypeDefinitionSplit",
"ctrl-w g shift-d": "editor::GoToTypeDefinitionSplit",
"ctrl-w space": "editor::OpenExcerptsSplit",

View File

@@ -1525,7 +1525,7 @@
"allow_rewrap": "anywhere"
},
"Ruby": {
"language_servers": ["solargraph", "!ruby-lsp", "!rubocop", "..."]
"language_servers": ["solargraph", "!ruby-lsp", "!rubocop", "!sorbet", "!steep", "..."]
},
"SCSS": {
"prettier": {

View File

@@ -33,9 +33,11 @@ use assistant_slash_command::SlashCommandRegistry;
use client::Client;
use feature_flags::FeatureFlagAppExt as _;
use fs::Fs;
use gpui::{App, actions, impl_actions};
use gpui::{App, Entity, actions, impl_actions};
use language::LanguageRegistry;
use language_model::{LanguageModelId, LanguageModelProviderId, LanguageModelRegistry};
use language_model::{
ConfiguredModel, LanguageModelId, LanguageModelProviderId, LanguageModelRegistry,
};
use prompt_store::PromptBuilder;
use schemars::JsonSchema;
use serde::Deserialize;
@@ -115,6 +117,23 @@ impl ManageProfiles {
impl_actions!(agent, [NewThread, ManageProfiles]);
#[derive(Clone)]
pub(crate) enum ModelUsageContext {
Thread(Entity<Thread>),
InlineAssistant,
}
impl ModelUsageContext {
pub fn configured_model(&self, cx: &App) -> Option<ConfiguredModel> {
match self {
Self::Thread(thread) => thread.read(cx).configured_model(),
Self::InlineAssistant => {
LanguageModelRegistry::read_global(cx).inline_assistant_model()
}
}
}
}
/// Initializes the `agent` crate.
pub fn init(
fs: Arc<dyn Fs>,

View File

@@ -1086,7 +1086,7 @@ impl Render for AgentDiffToolbar {
.child(vertical_divider())
.when_some(editor.read(cx).workspace(), |this, _workspace| {
this.child(
IconButton::new("review", IconName::ListCollapse)
IconButton::new("review", IconName::ListTodo)
.icon_size(IconSize::Small)
.tooltip(Tooltip::for_action_title_in(
"Review All Files",
@@ -1116,8 +1116,13 @@ impl Render for AgentDiffToolbar {
return Empty.into_any();
};
let is_generating = agent_diff.read(cx).thread.read(cx).is_generating();
if is_generating {
let has_pending_edit_tool_use = agent_diff
.read(cx)
.thread
.read(cx)
.has_pending_edit_tool_uses();
if has_pending_edit_tool_use {
return div().px_2().child(spinner_icon).into_any();
}
@@ -1507,7 +1512,7 @@ impl AgentDiff {
multibuffer.add_diff(diff_handle.clone(), cx);
});
let new_state = if thread.read(cx).is_generating() {
let new_state = if thread.read(cx).has_pending_edit_tool_uses() {
EditorState::Generating
} else {
EditorState::Reviewing

View File

@@ -3,7 +3,7 @@ use fs::Fs;
use gpui::{Entity, FocusHandle, SharedString};
use picker::popover_menu::PickerPopoverMenu;
use crate::Thread;
use crate::ModelUsageContext;
use assistant_context_editor::language_model_selector::{
LanguageModelSelector, ToggleModelSelector, language_model_selector,
};
@@ -12,12 +12,6 @@ use settings::update_settings_file;
use std::sync::Arc;
use ui::{PopoverMenuHandle, Tooltip, prelude::*};
#[derive(Clone)]
pub enum ModelType {
Default(Entity<Thread>),
InlineAssistant,
}
pub struct AgentModelSelector {
selector: Entity<LanguageModelSelector>,
menu_handle: PopoverMenuHandle<LanguageModelSelector>,
@@ -29,7 +23,7 @@ impl AgentModelSelector {
fs: Arc<dyn Fs>,
menu_handle: PopoverMenuHandle<LanguageModelSelector>,
focus_handle: FocusHandle,
model_type: ModelType,
model_usage_context: ModelUsageContext,
window: &mut Window,
cx: &mut Context<Self>,
) -> Self {
@@ -38,19 +32,14 @@ impl AgentModelSelector {
let fs = fs.clone();
language_model_selector(
{
let model_type = model_type.clone();
move |cx| match &model_type {
ModelType::Default(thread) => thread.read(cx).configured_model(),
ModelType::InlineAssistant => {
LanguageModelRegistry::read_global(cx).inline_assistant_model()
}
}
let model_context = model_usage_context.clone();
move |cx| model_context.configured_model(cx)
},
move |model, cx| {
let provider = model.provider_id().0.to_string();
let model_id = model.id().0.to_string();
match &model_type {
ModelType::Default(thread) => {
match &model_usage_context {
ModelUsageContext::Thread(thread) => {
thread.update(cx, |thread, cx| {
let registry = LanguageModelRegistry::read_global(cx);
if let Some(provider) = registry.provider(&model.provider_id())
@@ -72,7 +61,7 @@ impl AgentModelSelector {
},
);
}
ModelType::InlineAssistant => {
ModelUsageContext::InlineAssistant => {
update_settings_file::<AgentSettings>(
fs.clone(),
cx,

View File

@@ -1,4 +1,4 @@
use crate::agent_model_selector::{AgentModelSelector, ModelType};
use crate::agent_model_selector::AgentModelSelector;
use crate::buffer_codegen::BufferCodegen;
use crate::context::ContextCreasesAddon;
use crate::context_picker::{ContextPicker, ContextPickerCompletionProvider};
@@ -7,7 +7,7 @@ use crate::context_strip::{ContextStrip, ContextStripEvent, SuggestContextKind};
use crate::message_editor::{extract_message_creases, insert_message_creases};
use crate::terminal_codegen::TerminalCodegen;
use crate::thread_store::{TextThreadStore, ThreadStore};
use crate::{CycleNextInlineAssist, CyclePreviousInlineAssist};
use crate::{CycleNextInlineAssist, CyclePreviousInlineAssist, ModelUsageContext};
use crate::{RemoveAllContext, ToggleContextPicker};
use assistant_context_editor::language_model_selector::ToggleModelSelector;
use client::ErrorExt;
@@ -930,7 +930,7 @@ impl PromptEditor<BufferCodegen> {
fs,
model_selector_menu_handle,
prompt_editor.focus_handle(cx),
ModelType::InlineAssistant,
ModelUsageContext::InlineAssistant,
window,
cx,
)
@@ -1101,7 +1101,7 @@ impl PromptEditor<TerminalCodegen> {
fs,
model_selector_menu_handle.clone(),
prompt_editor.focus_handle(cx),
ModelType::InlineAssistant,
ModelUsageContext::InlineAssistant,
window,
cx,
)

View File

@@ -2,7 +2,7 @@ use std::collections::BTreeMap;
use std::rc::Rc;
use std::sync::Arc;
use crate::agent_model_selector::{AgentModelSelector, ModelType};
use crate::agent_model_selector::AgentModelSelector;
use crate::context::{AgentContextKey, ContextCreasesAddon, ContextLoadResult, load_context};
use crate::tool_compatibility::{IncompatibleToolsState, IncompatibleToolsTooltip};
use crate::ui::{
@@ -52,8 +52,8 @@ use crate::thread::{MessageCrease, Thread, TokenUsageRatio};
use crate::thread_store::{TextThreadStore, ThreadStore};
use crate::{
ActiveThread, AgentDiffPane, Chat, ChatWithFollow, ExpandMessageEditor, Follow, KeepAll,
NewThread, OpenAgentDiff, RejectAll, RemoveAllContext, ToggleBurnMode, ToggleContextPicker,
ToggleProfileSelector, register_agent_preview,
ModelUsageContext, NewThread, OpenAgentDiff, RejectAll, RemoveAllContext, ToggleBurnMode,
ToggleContextPicker, ToggleProfileSelector, register_agent_preview,
};
#[derive(RegisterComponent)]
@@ -197,7 +197,7 @@ impl MessageEditor {
fs.clone(),
model_selector_menu_handle,
editor.focus_handle(cx),
ModelType::Default(thread.clone()),
ModelUsageContext::Thread(thread.clone()),
window,
cx,
)

View File

@@ -16,11 +16,24 @@ pub fn adapt_schema_to_format(
}
match format {
LanguageModelToolSchemaFormat::JsonSchema => Ok(()),
LanguageModelToolSchemaFormat::JsonSchema => preprocess_json_schema(json),
LanguageModelToolSchemaFormat::JsonSchemaSubset => adapt_to_json_schema_subset(json),
}
}
fn preprocess_json_schema(json: &mut Value) -> Result<()> {
// `additionalProperties` defaults to `false` unless explicitly specified.
// This prevents models from hallucinating tool parameters.
if let Value::Object(obj) = json {
if let Some(Value::String(type_str)) = obj.get("type") {
if type_str == "object" && !obj.contains_key("additionalProperties") {
obj.insert("additionalProperties".to_string(), Value::Bool(false));
}
}
}
Ok(())
}
/// Tries to adapt the json schema so that it is compatible with https://ai.google.dev/api/caching#Schema
fn adapt_to_json_schema_subset(json: &mut Value) -> Result<()> {
if let Value::Object(obj) = json {
@@ -237,4 +250,59 @@ mod tests {
assert!(adapt_to_json_schema_subset(&mut json).is_err());
}
#[test]
fn test_preprocess_json_schema_adds_additional_properties() {
let mut json = json!({
"type": "object",
"properties": {
"name": {
"type": "string"
}
}
});
preprocess_json_schema(&mut json).unwrap();
assert_eq!(
json,
json!({
"type": "object",
"properties": {
"name": {
"type": "string"
}
},
"additionalProperties": false
})
);
}
#[test]
fn test_preprocess_json_schema_preserves_additional_properties() {
let mut json = json!({
"type": "object",
"properties": {
"name": {
"type": "string"
}
},
"additionalProperties": true
});
preprocess_json_schema(&mut json).unwrap();
assert_eq!(
json,
json!({
"type": "object",
"properties": {
"name": {
"type": "string"
}
},
"additionalProperties": true
})
);
}
}

View File

@@ -126,6 +126,7 @@ mod tests {
}
},
"required": ["location"],
"additionalProperties": false
})
);
}

View File

@@ -54,6 +54,7 @@ impl Template for EditFilePromptTemplate {
pub enum EditAgentOutputEvent {
ResolvingEditRange(Range<Anchor>),
UnresolvedEditRange,
AmbiguousEditRange(Vec<Range<usize>>),
Edited,
}
@@ -269,16 +270,29 @@ impl EditAgent {
}
}
let (edit_events_, resolved_old_text) = resolve_old_text.await?;
let (edit_events_, mut resolved_old_text) = resolve_old_text.await?;
edit_events = edit_events_;
// If we can't resolve the old text, restart the loop waiting for a
// new edit (or for the stream to end).
let Some(resolved_old_text) = resolved_old_text else {
output_events
.unbounded_send(EditAgentOutputEvent::UnresolvedEditRange)
.ok();
continue;
let resolved_old_text = match resolved_old_text.len() {
1 => resolved_old_text.pop().unwrap(),
0 => {
output_events
.unbounded_send(EditAgentOutputEvent::UnresolvedEditRange)
.ok();
continue;
}
_ => {
let ranges = resolved_old_text
.into_iter()
.map(|text| text.range)
.collect();
output_events
.unbounded_send(EditAgentOutputEvent::AmbiguousEditRange(ranges))
.ok();
continue;
}
};
// Compute edits in the background and apply them as they become
@@ -405,7 +419,7 @@ impl EditAgent {
mut edit_events: T,
cx: &mut AsyncApp,
) -> (
Task<Result<(T, Option<ResolvedOldText>)>>,
Task<Result<(T, Vec<ResolvedOldText>)>>,
async_watch::Receiver<Option<Range<usize>>>,
)
where
@@ -425,21 +439,29 @@ impl EditAgent {
}
}
let old_range = matcher.finish();
old_range_tx.send(old_range.clone())?;
if let Some(old_range) = old_range {
let line_indent =
LineIndent::from_iter(matcher.query_lines().first().unwrap().chars());
Ok((
edit_events,
Some(ResolvedOldText {
range: old_range,
indent: line_indent,
}),
))
let matches = matcher.finish();
let old_range = if matches.len() == 1 {
matches.first()
} else {
Ok((edit_events, None))
}
// No matches or multiple ambiguous matches
None
};
old_range_tx.send(old_range.cloned())?;
let indent = LineIndent::from_iter(
matcher
.query_lines()
.first()
.unwrap_or(&String::new())
.chars(),
);
let resolved_old_texts = matches
.into_iter()
.map(|range| ResolvedOldText { range, indent })
.collect::<Vec<_>>();
Ok((edit_events, resolved_old_texts))
});
(task, old_range_rx)
@@ -1322,6 +1344,76 @@ mod tests {
EditAgent::new(model, project, action_log, Templates::new())
}
#[gpui::test(iterations = 10)]
async fn test_non_unique_text_error(cx: &mut TestAppContext, mut rng: StdRng) {
let agent = init_test(cx).await;
let original_text = indoc! {"
function foo() {
return 42;
}
function bar() {
return 42;
}
function baz() {
return 42;
}
"};
let buffer = cx.new(|cx| Buffer::local(original_text, cx));
let (apply, mut events) = agent.edit(
buffer.clone(),
String::new(),
&LanguageModelRequest::default(),
&mut cx.to_async(),
);
cx.run_until_parked();
// When <old_text> matches text in more than one place
simulate_llm_output(
&agent,
indoc! {"
<old_text>
return 42;
</old_text>
<new_text>
return 100;
</new_text>
"},
&mut rng,
cx,
);
apply.await.unwrap();
// Then the text should remain unchanged
let result_text = buffer.read_with(cx, |buffer, _| buffer.snapshot().text());
assert_eq!(
result_text,
indoc! {"
function foo() {
return 42;
}
function bar() {
return 42;
}
function baz() {
return 42;
}
"},
"Text should remain unchanged when there are multiple matches"
);
// And AmbiguousEditRange even should be emitted
let events = drain_events(&mut events);
let ambiguous_ranges = vec![17..31, 52..66, 87..101];
assert!(
events.contains(&EditAgentOutputEvent::AmbiguousEditRange(ambiguous_ranges)),
"Should emit AmbiguousEditRange for non-unique text"
);
}
fn drain_events(
stream: &mut UnboundedReceiver<EditAgentOutputEvent>,
) -> Vec<EditAgentOutputEvent> {

View File

@@ -11,7 +11,7 @@ pub struct StreamingFuzzyMatcher {
snapshot: TextBufferSnapshot,
query_lines: Vec<String>,
incomplete_line: String,
best_match: Option<Range<usize>>,
best_matches: Vec<Range<usize>>,
matrix: SearchMatrix,
}
@@ -22,7 +22,7 @@ impl StreamingFuzzyMatcher {
snapshot,
query_lines: Vec::new(),
incomplete_line: String::new(),
best_match: None,
best_matches: Vec::new(),
matrix: SearchMatrix::new(buffer_line_count + 1),
}
}
@@ -55,31 +55,41 @@ impl StreamingFuzzyMatcher {
self.incomplete_line.replace_range(..last_pos + 1, "");
self.best_match = self.resolve_location_fuzzy();
}
self.best_matches = self.resolve_location_fuzzy();
self.best_match.clone()
if let Some(first_match) = self.best_matches.first() {
Some(first_match.clone())
} else {
None
}
} else {
if let Some(first_match) = self.best_matches.first() {
Some(first_match.clone())
} else {
None
}
}
}
/// Finish processing and return the final best match.
/// Finish processing and return the final best match(es).
///
/// This processes any remaining incomplete line before returning the final
/// match result.
pub fn finish(&mut self) -> Option<Range<usize>> {
pub fn finish(&mut self) -> Vec<Range<usize>> {
// Process any remaining incomplete line
if !self.incomplete_line.is_empty() {
self.query_lines.push(self.incomplete_line.clone());
self.best_match = self.resolve_location_fuzzy();
self.incomplete_line.clear();
self.best_matches = self.resolve_location_fuzzy();
}
self.best_match.clone()
self.best_matches.clone()
}
fn resolve_location_fuzzy(&mut self) -> Option<Range<usize>> {
fn resolve_location_fuzzy(&mut self) -> Vec<Range<usize>> {
let new_query_line_count = self.query_lines.len();
let old_query_line_count = self.matrix.rows.saturating_sub(1);
if new_query_line_count == old_query_line_count {
return None;
return Vec::new();
}
self.matrix.resize_rows(new_query_line_count + 1);
@@ -132,53 +142,61 @@ impl StreamingFuzzyMatcher {
}
}
// Traceback to find the best match
// Find all matches with the best cost
let buffer_line_count = self.snapshot.max_point().row as usize + 1;
let mut buffer_row_end = buffer_line_count as u32;
let mut best_cost = u32::MAX;
let mut matches_with_best_cost = Vec::new();
for col in 1..=buffer_line_count {
let cost = self.matrix.get(new_query_line_count, col).cost;
if cost < best_cost {
best_cost = cost;
buffer_row_end = col as u32;
matches_with_best_cost.clear();
matches_with_best_cost.push(col as u32);
} else if cost == best_cost {
matches_with_best_cost.push(col as u32);
}
}
let mut matched_lines = 0;
let mut query_row = new_query_line_count;
let mut buffer_row_start = buffer_row_end;
while query_row > 0 && buffer_row_start > 0 {
let current = self.matrix.get(query_row, buffer_row_start as usize);
match current.direction {
SearchDirection::Diagonal => {
query_row -= 1;
buffer_row_start -= 1;
matched_lines += 1;
}
SearchDirection::Up => {
query_row -= 1;
}
SearchDirection::Left => {
buffer_row_start -= 1;
// Find ranges for the matches
let mut valid_matches = Vec::new();
for &buffer_row_end in &matches_with_best_cost {
let mut matched_lines = 0;
let mut query_row = new_query_line_count;
let mut buffer_row_start = buffer_row_end;
while query_row > 0 && buffer_row_start > 0 {
let current = self.matrix.get(query_row, buffer_row_start as usize);
match current.direction {
SearchDirection::Diagonal => {
query_row -= 1;
buffer_row_start -= 1;
matched_lines += 1;
}
SearchDirection::Up => {
query_row -= 1;
}
SearchDirection::Left => {
buffer_row_start -= 1;
}
}
}
let matched_buffer_row_count = buffer_row_end - buffer_row_start;
let matched_ratio = matched_lines as f32
/ (matched_buffer_row_count as f32).max(new_query_line_count as f32);
if matched_ratio >= 0.8 {
let buffer_start_ix = self
.snapshot
.point_to_offset(Point::new(buffer_row_start, 0));
let buffer_end_ix = self.snapshot.point_to_offset(Point::new(
buffer_row_end - 1,
self.snapshot.line_len(buffer_row_end - 1),
));
valid_matches.push((buffer_row_start, buffer_start_ix..buffer_end_ix));
}
}
let matched_buffer_row_count = buffer_row_end - buffer_row_start;
let matched_ratio = matched_lines as f32
/ (matched_buffer_row_count as f32).max(new_query_line_count as f32);
if matched_ratio >= 0.8 {
let buffer_start_ix = self
.snapshot
.point_to_offset(Point::new(buffer_row_start, 0));
let buffer_end_ix = self.snapshot.point_to_offset(Point::new(
buffer_row_end - 1,
self.snapshot.line_len(buffer_row_end - 1),
));
Some(buffer_start_ix..buffer_end_ix)
} else {
None
}
valid_matches.into_iter().map(|(_, range)| range).collect()
}
}
@@ -638,28 +656,35 @@ mod tests {
matcher.push(chunk);
}
let result = matcher.finish();
let actual_ranges = matcher.finish();
// If no expected ranges, we expect no match
if expected_ranges.is_empty() {
assert_eq!(
result, None,
assert!(
actual_ranges.is_empty(),
"Expected no match for query: {:?}, but found: {:?}",
query, result
query,
actual_ranges
);
} else {
let mut actual_ranges = Vec::new();
if let Some(range) = result {
actual_ranges.push(range);
}
let text_with_actual_range = generate_marked_text(&text, &actual_ranges, false);
pretty_assertions::assert_eq!(
text_with_actual_range,
text_with_expected_range,
"Query: {:?}, Chunks: {:?}",
indoc! {"
Query: {:?}
Chunks: {:?}
Expected marked text: {}
Actual marked text: {}
Expected ranges: {:?}
Actual ranges: {:?}"
},
query,
chunks
chunks,
text_with_expected_range,
text_with_actual_range,
expected_ranges,
actual_ranges
);
}
}
@@ -687,8 +712,11 @@ mod tests {
fn finish(mut finder: StreamingFuzzyMatcher) -> Option<String> {
let snapshot = finder.snapshot.clone();
finder
.finish()
.map(|range| snapshot.text_for_range(range).collect::<String>())
let matches = finder.finish();
if let Some(range) = matches.first() {
Some(snapshot.text_for_range(range.clone()).collect::<String>())
} else {
None
}
}
}

View File

@@ -239,6 +239,7 @@ impl Tool for EditFileTool {
};
let mut hallucinated_old_text = false;
let mut ambiguous_ranges = Vec::new();
while let Some(event) = events.next().await {
match event {
EditAgentOutputEvent::Edited => {
@@ -247,6 +248,7 @@ impl Tool for EditFileTool {
}
}
EditAgentOutputEvent::UnresolvedEditRange => hallucinated_old_text = true,
EditAgentOutputEvent::AmbiguousEditRange(ranges) => ambiguous_ranges = ranges,
EditAgentOutputEvent::ResolvingEditRange(range) => {
if let Some(card) = card_clone.as_ref() {
card.update(cx, |card, cx| card.reveal_range(range, cx))?;
@@ -329,6 +331,17 @@ impl Tool for EditFileTool {
I can perform the requested edits.
"}
);
anyhow::ensure!(
ambiguous_ranges.is_empty(),
// TODO: Include ambiguous_ranges, converted to line numbers.
// This would work best if we add `line_hint` parameter
// to edit_file_tool
formatdoc! {"
<old_text> matches more than one position in the file. Read the
relevant sections of {input_path} again and extend <old_text> so
that I can perform the requested edits.
"}
);
Ok(ToolResultOutput {
content: ToolResultContent::Text("No edits were made.".into()),
output: serde_json::to_value(output).ok(),

View File

@@ -71,20 +71,22 @@ pub enum Model {
// DeepSeek
DeepSeekR1,
// Meta models
MetaLlama3_8BInstruct,
MetaLlama3_70BInstruct,
MetaLlama31_8BInstruct,
MetaLlama31_70BInstruct,
MetaLlama31_405BInstruct,
MetaLlama32_1BInstruct,
MetaLlama32_3BInstruct,
MetaLlama32_11BMultiModal,
MetaLlama32_90BMultiModal,
MetaLlama33_70BInstruct,
MetaLlama38BInstructV1,
MetaLlama370BInstructV1,
MetaLlama318BInstructV1_128k,
MetaLlama318BInstructV1,
MetaLlama3170BInstructV1_128k,
MetaLlama3170BInstructV1,
MetaLlama31405BInstructV1,
MetaLlama321BInstructV1,
MetaLlama323BInstructV1,
MetaLlama3211BInstructV1,
MetaLlama3290BInstructV1,
MetaLlama3370BInstructV1,
#[allow(non_camel_case_types)]
MetaLlama4Scout_17BInstruct,
MetaLlama4Scout17BInstructV1,
#[allow(non_camel_case_types)]
MetaLlama4Maverick_17BInstruct,
MetaLlama4Maverick17BInstructV1,
// Mistral models
MistralMistral7BInstructV0,
MistralMixtral8x7BInstructV0,
@@ -129,6 +131,64 @@ impl Model {
}
pub fn id(&self) -> &str {
match self {
Model::ClaudeSonnet4 => "claude-4-sonnet",
Model::ClaudeSonnet4Thinking => "claude-4-sonnet-thinking",
Model::ClaudeOpus4 => "claude-4-opus",
Model::ClaudeOpus4Thinking => "claude-4-opus-thinking",
Model::Claude3_5SonnetV2 => "claude-3-5-sonnet-v2",
Model::Claude3_5Sonnet => "claude-3-5-sonnet",
Model::Claude3Opus => "claude-3-opus",
Model::Claude3Sonnet => "claude-3-sonnet",
Model::Claude3Haiku => "claude-3-haiku",
Model::Claude3_5Haiku => "claude-3-5-haiku",
Model::Claude3_7Sonnet => "claude-3-7-sonnet",
Model::Claude3_7SonnetThinking => "claude-3-7-sonnet-thinking",
Model::AmazonNovaLite => "amazon-nova-lite",
Model::AmazonNovaMicro => "amazon-nova-micro",
Model::AmazonNovaPro => "amazon-nova-pro",
Model::AmazonNovaPremier => "amazon-nova-premier",
Model::DeepSeekR1 => "deepseek-r1",
Model::AI21J2GrandeInstruct => "ai21-j2-grande-instruct",
Model::AI21J2JumboInstruct => "ai21-j2-jumbo-instruct",
Model::AI21J2Mid => "ai21-j2-mid",
Model::AI21J2MidV1 => "ai21-j2-mid-v1",
Model::AI21J2Ultra => "ai21-j2-ultra",
Model::AI21J2UltraV1_8k => "ai21-j2-ultra-v1-8k",
Model::AI21J2UltraV1 => "ai21-j2-ultra-v1",
Model::AI21JambaInstructV1 => "ai21-jamba-instruct-v1",
Model::AI21Jamba15LargeV1 => "ai21-jamba-1-5-large-v1",
Model::AI21Jamba15MiniV1 => "ai21-jamba-1-5-mini-v1",
Model::CohereCommandTextV14_4k => "cohere-command-text-v14-4k",
Model::CohereCommandRV1 => "cohere-command-r-v1",
Model::CohereCommandRPlusV1 => "cohere-command-r-plus-v1",
Model::CohereCommandLightTextV14_4k => "cohere-command-light-text-v14-4k",
Model::MetaLlama38BInstructV1 => "meta-llama3-8b-instruct-v1",
Model::MetaLlama370BInstructV1 => "meta-llama3-70b-instruct-v1",
Model::MetaLlama318BInstructV1_128k => "meta-llama3-1-8b-instruct-v1-128k",
Model::MetaLlama318BInstructV1 => "meta-llama3-1-8b-instruct-v1",
Model::MetaLlama3170BInstructV1_128k => "meta-llama3-1-70b-instruct-v1-128k",
Model::MetaLlama3170BInstructV1 => "meta-llama3-1-70b-instruct-v1",
Model::MetaLlama31405BInstructV1 => "meta-llama3-1-405b-instruct-v1",
Model::MetaLlama321BInstructV1 => "meta-llama3-2-1b-instruct-v1",
Model::MetaLlama323BInstructV1 => "meta-llama3-2-3b-instruct-v1",
Model::MetaLlama3211BInstructV1 => "meta-llama3-2-11b-instruct-v1",
Model::MetaLlama3290BInstructV1 => "meta-llama3-2-90b-instruct-v1",
Model::MetaLlama3370BInstructV1 => "meta-llama3-3-70b-instruct-v1",
Model::MetaLlama4Scout17BInstructV1 => "meta-llama4-scout-17b-instruct-v1",
Model::MetaLlama4Maverick17BInstructV1 => "meta-llama4-maverick-17b-instruct-v1",
Model::MistralMistral7BInstructV0 => "mistral-7b-instruct-v0",
Model::MistralMixtral8x7BInstructV0 => "mistral-mixtral-8x7b-instruct-v0",
Model::MistralMistralLarge2402V1 => "mistral-large-2402-v1",
Model::MistralMistralSmall2402V1 => "mistral-small-2402-v1",
Model::MistralPixtralLarge2502V1 => "mistral-pixtral-large-2502-v1",
Model::PalmyraWriterX4 => "palmyra-writer-x4",
Model::PalmyraWriterX5 => "palmyra-writer-x5",
Self::Custom { name, .. } => name,
}
}
pub fn request_id(&self) -> &str {
match self {
Model::ClaudeSonnet4 | Model::ClaudeSonnet4Thinking => {
"anthropic.claude-sonnet-4-20250514-v1:0"
@@ -164,18 +224,20 @@ impl Model {
Model::CohereCommandRV1 => "cohere.command-r-v1:0",
Model::CohereCommandRPlusV1 => "cohere.command-r-plus-v1:0",
Model::CohereCommandLightTextV14_4k => "cohere.command-light-text-v14:7:4k",
Model::MetaLlama3_8BInstruct => "meta.llama3-8b-instruct-v1:0",
Model::MetaLlama3_70BInstruct => "meta.llama3-70b-instruct-v1:0",
Model::MetaLlama31_8BInstruct => "meta.llama3-1-8b-instruct-v1:0",
Model::MetaLlama31_70BInstruct => "meta.llama3-1-70b-instruct-v1:0",
Model::MetaLlama31_405BInstruct => "meta.llama3-1-405b-instruct-v1:0",
Model::MetaLlama32_11BMultiModal => "meta.llama3-2-11b-instruct-v1:0",
Model::MetaLlama32_90BMultiModal => "meta.llama3-2-90b-instruct-v1:0",
Model::MetaLlama32_1BInstruct => "meta.llama3-2-1b-instruct-v1:0",
Model::MetaLlama32_3BInstruct => "meta.llama3-2-3b-instruct-v1:0",
Model::MetaLlama33_70BInstruct => "meta.llama3-3-70b-instruct-v1:0",
Model::MetaLlama4Scout_17BInstruct => "meta.llama4-scout-17b-instruct-v1:0",
Model::MetaLlama4Maverick_17BInstruct => "meta.llama4-maverick-17b-instruct-v1:0",
Model::MetaLlama38BInstructV1 => "meta.llama3-8b-instruct-v1:0",
Model::MetaLlama370BInstructV1 => "meta.llama3-70b-instruct-v1:0",
Model::MetaLlama318BInstructV1_128k => "meta.llama3-1-8b-instruct-v1:0",
Model::MetaLlama318BInstructV1 => "meta.llama3-1-8b-instruct-v1:0",
Model::MetaLlama3170BInstructV1_128k => "meta.llama3-1-70b-instruct-v1:0",
Model::MetaLlama3170BInstructV1 => "meta.llama3-1-70b-instruct-v1:0",
Model::MetaLlama31405BInstructV1 => "meta.llama3-1-405b-instruct-v1:0",
Model::MetaLlama3211BInstructV1 => "meta.llama3-2-11b-instruct-v1:0",
Model::MetaLlama3290BInstructV1 => "meta.llama3-2-90b-instruct-v1:0",
Model::MetaLlama321BInstructV1 => "meta.llama3-2-1b-instruct-v1:0",
Model::MetaLlama323BInstructV1 => "meta.llama3-2-3b-instruct-v1:0",
Model::MetaLlama3370BInstructV1 => "meta.llama3-3-70b-instruct-v1:0",
Model::MetaLlama4Scout17BInstructV1 => "meta.llama4-scout-17b-instruct-v1:0",
Model::MetaLlama4Maverick17BInstructV1 => "meta.llama4-maverick-17b-instruct-v1:0",
Model::MistralMistral7BInstructV0 => "mistral.mistral-7b-instruct-v0:2",
Model::MistralMixtral8x7BInstructV0 => "mistral.mixtral-8x7b-instruct-v0:1",
Model::MistralMistralLarge2402V1 => "mistral.mistral-large-2402-v1:0",
@@ -220,18 +282,20 @@ impl Model {
Self::CohereCommandRV1 => "Cohere Command R V1",
Self::CohereCommandRPlusV1 => "Cohere Command R Plus V1",
Self::CohereCommandLightTextV14_4k => "Cohere Command Light Text V14 4K",
Self::MetaLlama3_8BInstruct => "Meta Llama 3 8B Instruct",
Self::MetaLlama3_70BInstruct => "Meta Llama 3 70B Instruct",
Self::MetaLlama31_8BInstruct => "Meta Llama 3.1 8B Instruct",
Self::MetaLlama31_70BInstruct => "Meta Llama 3.1 70B Instruct",
Self::MetaLlama31_405BInstruct => "Meta Llama 3.1 405B Instruct",
Self::MetaLlama32_11BMultiModal => "Meta Llama 3.2 11B Vision Instruct",
Self::MetaLlama32_90BMultiModal => "Meta Llama 3.2 90B Vision Instruct",
Self::MetaLlama32_1BInstruct => "Meta Llama 3.2 1B Instruct",
Self::MetaLlama32_3BInstruct => "Meta Llama 3.2 3B Instruct",
Self::MetaLlama33_70BInstruct => "Meta Llama 3.3 70B Instruct",
Self::MetaLlama4Scout_17BInstruct => "Meta Llama 4 Scout 17B Instruct",
Self::MetaLlama4Maverick_17BInstruct => "Meta Llama 4 Maverick 17B Instruct",
Self::MetaLlama38BInstructV1 => "Meta Llama 3 8B Instruct",
Self::MetaLlama370BInstructV1 => "Meta Llama 3 70B Instruct",
Self::MetaLlama318BInstructV1_128k => "Meta Llama 3.1 8B Instruct 128K",
Self::MetaLlama318BInstructV1 => "Meta Llama 3.1 8B Instruct",
Self::MetaLlama3170BInstructV1_128k => "Meta Llama 3.1 70B Instruct 128K",
Self::MetaLlama3170BInstructV1 => "Meta Llama 3.1 70B Instruct",
Self::MetaLlama31405BInstructV1 => "Meta Llama 3.1 405B Instruct",
Self::MetaLlama3211BInstructV1 => "Meta Llama 3.2 11B Instruct",
Self::MetaLlama3290BInstructV1 => "Meta Llama 3.2 90B Instruct",
Self::MetaLlama321BInstructV1 => "Meta Llama 3.2 1B Instruct",
Self::MetaLlama323BInstructV1 => "Meta Llama 3.2 3B Instruct",
Self::MetaLlama3370BInstructV1 => "Meta Llama 3.3 70B Instruct",
Self::MetaLlama4Scout17BInstructV1 => "Meta Llama 4 Scout 17B Instruct",
Self::MetaLlama4Maverick17BInstructV1 => "Meta Llama 4 Maverick 17B Instruct",
Self::MistralMistral7BInstructV0 => "Mistral 7B Instruct V0",
Self::MistralMixtral8x7BInstructV0 => "Mistral Mixtral 8x7B Instruct V0",
Self::MistralMistralLarge2402V1 => "Mistral Large 2402 V1",
@@ -253,7 +317,9 @@ impl Model {
| Self::Claude3_5Haiku
| Self::Claude3_7Sonnet
| Self::ClaudeSonnet4
| Self::ClaudeOpus4 => 200_000,
| Self::ClaudeOpus4
| Self::ClaudeSonnet4Thinking
| Self::ClaudeOpus4Thinking => 200_000,
Self::AmazonNovaPremier => 1_000_000,
Self::PalmyraWriterX5 => 1_000_000,
Self::PalmyraWriterX4 => 128_000,
@@ -362,11 +428,11 @@ impl Model {
anyhow::bail!("Unsupported Region {region}");
};
let model_id = self.id();
let model_id = self.request_id();
match (self, region_group) {
// Custom models can't have CRI IDs
(Model::Custom { .. }, _) => Ok(self.id().into()),
(Model::Custom { .. }, _) => Ok(self.request_id().into()),
// Models with US Gov only
(Model::Claude3_5Sonnet, "us-gov") | (Model::Claude3Haiku, "us-gov") => {
@@ -390,16 +456,18 @@ impl Model {
| Model::Claude3Opus
| Model::Claude3Sonnet
| Model::DeepSeekR1
| Model::MetaLlama31_405BInstruct
| Model::MetaLlama31_70BInstruct
| Model::MetaLlama31_8BInstruct
| Model::MetaLlama32_11BMultiModal
| Model::MetaLlama32_1BInstruct
| Model::MetaLlama32_3BInstruct
| Model::MetaLlama32_90BMultiModal
| Model::MetaLlama33_70BInstruct
| Model::MetaLlama4Maverick_17BInstruct
| Model::MetaLlama4Scout_17BInstruct
| Model::MetaLlama31405BInstructV1
| Model::MetaLlama3170BInstructV1_128k
| Model::MetaLlama3170BInstructV1
| Model::MetaLlama318BInstructV1_128k
| Model::MetaLlama318BInstructV1
| Model::MetaLlama3211BInstructV1
| Model::MetaLlama321BInstructV1
| Model::MetaLlama323BInstructV1
| Model::MetaLlama3290BInstructV1
| Model::MetaLlama3370BInstructV1
| Model::MetaLlama4Maverick17BInstructV1
| Model::MetaLlama4Scout17BInstructV1
| Model::MistralPixtralLarge2502V1
| Model::PalmyraWriterX4
| Model::PalmyraWriterX5,
@@ -413,8 +481,8 @@ impl Model {
| Model::Claude3_7SonnetThinking
| Model::Claude3Haiku
| Model::Claude3Sonnet
| Model::MetaLlama32_1BInstruct
| Model::MetaLlama32_3BInstruct
| Model::MetaLlama321BInstructV1
| Model::MetaLlama323BInstructV1
| Model::MistralPixtralLarge2502V1,
"eu",
) => Ok(format!("{}.{}", region_group, model_id)),
@@ -429,7 +497,7 @@ impl Model {
) => Ok(format!("{}.{}", region_group, model_id)),
// Any other combination is not supported
_ => Ok(self.id().into()),
_ => Ok(self.request_id().into()),
}
}
}
@@ -506,15 +574,15 @@ mod tests {
fn test_meta_models_inference_ids() -> anyhow::Result<()> {
// Test Meta models
assert_eq!(
Model::MetaLlama3_70BInstruct.cross_region_inference_id("us-east-1")?,
Model::MetaLlama370BInstructV1.cross_region_inference_id("us-east-1")?,
"meta.llama3-70b-instruct-v1:0"
);
assert_eq!(
Model::MetaLlama31_70BInstruct.cross_region_inference_id("us-east-1")?,
Model::MetaLlama3170BInstructV1.cross_region_inference_id("us-east-1")?,
"us.meta.llama3-1-70b-instruct-v1:0"
);
assert_eq!(
Model::MetaLlama32_1BInstruct.cross_region_inference_id("eu-west-1")?,
Model::MetaLlama321BInstructV1.cross_region_inference_id("eu-west-1")?,
"eu.meta.llama3-2-1b-instruct-v1:0"
);
Ok(())
@@ -584,4 +652,39 @@ mod tests {
Ok(())
}
#[test]
fn test_friendly_id_vs_request_id() {
// Test that id() returns friendly identifiers
assert_eq!(Model::Claude3_5SonnetV2.id(), "claude-3-5-sonnet-v2");
assert_eq!(Model::AmazonNovaLite.id(), "amazon-nova-lite");
assert_eq!(Model::DeepSeekR1.id(), "deepseek-r1");
assert_eq!(
Model::MetaLlama38BInstructV1.id(),
"meta-llama3-8b-instruct-v1"
);
// Test that request_id() returns actual backend model IDs
assert_eq!(
Model::Claude3_5SonnetV2.request_id(),
"anthropic.claude-3-5-sonnet-20241022-v2:0"
);
assert_eq!(Model::AmazonNovaLite.request_id(), "amazon.nova-lite-v1:0");
assert_eq!(Model::DeepSeekR1.request_id(), "deepseek.r1-v1:0");
assert_eq!(
Model::MetaLlama38BInstructV1.request_id(),
"meta.llama3-8b-instruct-v1:0"
);
// Test thinking models have different friendly IDs but same request IDs
assert_eq!(Model::ClaudeSonnet4.id(), "claude-4-sonnet");
assert_eq!(
Model::ClaudeSonnet4Thinking.id(),
"claude-4-sonnet-thinking"
);
assert_eq!(
Model::ClaudeSonnet4.request_id(),
Model::ClaudeSonnet4Thinking.request_id()
);
}
}

View File

@@ -961,7 +961,10 @@ impl DisplaySnapshot {
if chunk.is_unnecessary {
diagnostic_highlight.fade_out = Some(editor_style.unnecessary_code_fade);
}
if chunk.underline && editor_style.show_underlines {
if chunk.underline
&& editor_style.show_underlines
&& !(chunk.is_unnecessary && severity > lsp::DiagnosticSeverity::WARNING)
{
let diagnostic_color = super::diagnostic_style(severity, &editor_style.status);
diagnostic_highlight.underline = Some(UnderlineStyle {
color: Some(diagnostic_color),

View File

@@ -1912,19 +1912,19 @@ fn test_prev_next_word_boundary(cx: &mut TestAppContext) {
assert_selection_ranges("use std::ˇstr::{foo, bar}\n\n {ˇbaz.qux()}", editor, cx);
editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
assert_selection_ranges("use stdˇ::str::{foo, bar}\n\n ˇ{baz.qux()}", editor, cx);
assert_selection_ranges("use stdˇ::str::{foo, bar}\n\nˇ {baz.qux()}", editor, cx);
editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
assert_selection_ranges("use ˇstd::str::{foo, bar}\n\nˇ {baz.qux()}", editor, cx);
editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
assert_selection_ranges("ˇuse std::str::{foo, bar}\nˇ\n {baz.qux()}", editor, cx);
assert_selection_ranges("use ˇstd::str::{foo, bar}\nˇ\n {baz.qux()}", editor, cx);
editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
assert_selection_ranges("ˇuse std::str::{foo, barˇ}\n\n {baz.qux()}", editor, cx);
editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
assert_selection_ranges("ˇuse std::str::{foo, ˇbar}\n\n {baz.qux()}", editor, cx);
editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
assert_selection_ranges("useˇ std::str::{foo, bar}ˇ\n\n {baz.qux()}", editor, cx);
assert_selection_ranges("useˇ std::str::{foo, barˇ}\n\n {baz.qux()}", editor, cx);
editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
assert_selection_ranges("use stdˇ::str::{foo, bar}\nˇ\n {baz.qux()}", editor, cx);
@@ -1942,7 +1942,7 @@ fn test_prev_next_word_boundary(cx: &mut TestAppContext) {
editor.select_to_previous_word_start(&SelectToPreviousWordStart, window, cx);
assert_selection_ranges(
"use std«ˇ::s»tr::{foo, bar}\n\n «ˇ{b»az.qux()}",
"use std«ˇ::s»tr::{foo, bar}\n\n«ˇ {b»az.qux()}",
editor,
cx,
);
@@ -21227,6 +21227,7 @@ fn empty_range(row: usize, column: usize) -> Range<DisplayPoint> {
point..point
}
#[track_caller]
fn assert_selection_ranges(marked_text: &str, editor: &mut Editor, cx: &mut Context<Editor>) {
let (text, ranges) = marked_text_ranges(marked_text, true);
assert_eq!(editor.text(cx), text);

View File

@@ -264,7 +264,18 @@ pub fn previous_word_start(map: &DisplaySnapshot, point: DisplayPoint) -> Displa
let raw_point = point.to_point(map);
let classifier = map.buffer_snapshot.char_classifier_at(raw_point);
let mut is_first_iteration = true;
find_preceding_boundary_display_point(map, point, FindRange::MultiLine, |left, right| {
// Make alt-left skip punctuation on Mac OS to respect Mac VSCode behaviour. For example: hello.| goes to |hello.
if is_first_iteration
&& classifier.is_punctuation(right)
&& !classifier.is_punctuation(left)
{
is_first_iteration = false;
return false;
}
is_first_iteration = false;
(classifier.kind(left) != classifier.kind(right) && !classifier.is_whitespace(right))
|| left == '\n'
})
@@ -305,8 +316,18 @@ pub fn previous_subword_start(map: &DisplaySnapshot, point: DisplayPoint) -> Dis
pub fn next_word_end(map: &DisplaySnapshot, point: DisplayPoint) -> DisplayPoint {
let raw_point = point.to_point(map);
let classifier = map.buffer_snapshot.char_classifier_at(raw_point);
let mut is_first_iteration = true;
find_boundary(map, point, FindRange::MultiLine, |left, right| {
// Make alt-right skip punctuation on Mac OS to respect the Mac behaviour. For example: |.hello goes to .hello|
if is_first_iteration
&& classifier.is_punctuation(left)
&& !classifier.is_punctuation(right)
{
is_first_iteration = false;
return false;
}
is_first_iteration = false;
(classifier.kind(left) != classifier.kind(right) && !classifier.is_whitespace(left))
|| right == '\n'
})
@@ -782,10 +803,15 @@ mod tests {
fn assert(marked_text: &str, cx: &mut gpui::App) {
let (snapshot, display_points) = marked_display_snapshot(marked_text, cx);
assert_eq!(
previous_word_start(&snapshot, display_points[1]),
display_points[0]
);
let actual = previous_word_start(&snapshot, display_points[1]);
let expected = display_points[0];
if actual != expected {
eprintln!(
"previous_word_start mismatch for '{}': actual={:?}, expected={:?}",
marked_text, actual, expected
);
}
assert_eq!(actual, expected);
}
assert("\nˇ ˇlorem", cx);
@@ -796,12 +822,17 @@ mod tests {
assert("\nlorem\nˇ ˇipsum", cx);
assert("\n\nˇ\nˇ", cx);
assert(" ˇlorem ˇipsum", cx);
assert("loremˇ-ˇipsum", cx);
assert("ˇlorem-ˇipsum", cx);
assert("loremˇ-#$@ˇipsum", cx);
assert("ˇlorem_ˇipsum", cx);
assert(" ˇdefγˇ", cx);
assert(" ˇbcΔˇ", cx);
assert(" abˇ——ˇcd", cx);
// Test punctuation skipping behavior
assert("ˇhello.ˇ", cx);
assert("helloˇ...ˇ", cx);
assert("helloˇ.---..ˇtest", cx);
assert("test ˇ.--ˇtest", cx);
assert("oneˇ,;:!?ˇtwo", cx);
}
#[gpui::test]
@@ -955,10 +986,15 @@ mod tests {
fn assert(marked_text: &str, cx: &mut gpui::App) {
let (snapshot, display_points) = marked_display_snapshot(marked_text, cx);
assert_eq!(
next_word_end(&snapshot, display_points[0]),
display_points[1]
);
let actual = next_word_end(&snapshot, display_points[0]);
let expected = display_points[1];
if actual != expected {
eprintln!(
"next_word_end mismatch for '{}': actual={:?}, expected={:?}",
marked_text, actual, expected
);
}
assert_eq!(actual, expected);
}
assert("\nˇ loremˇ", cx);
@@ -967,11 +1003,18 @@ mod tests {
assert(" loremˇ ˇ\nipsum\n", cx);
assert("\nˇ\nˇ\n\n", cx);
assert("loremˇ ipsumˇ ", cx);
assert("loremˇ-ˇipsum", cx);
assert("loremˇ-ipsumˇ", cx);
assert("loremˇ#$@-ˇipsum", cx);
assert("loremˇ_ipsumˇ", cx);
assert(" ˇbcΔˇ", cx);
assert(" abˇ——ˇcd", cx);
// Test punctuation skipping behavior
assert("ˇ.helloˇ", cx);
assert("display_pointsˇ[0ˇ]", cx);
assert("ˇ...ˇhello", cx);
assert("helloˇ.---..ˇtest", cx);
assert("testˇ.--ˇ test", cx);
assert("oneˇ,;:!?ˇtwo", cx);
}
#[gpui::test]

View File

@@ -45,6 +45,7 @@ pub fn test_font() -> Font {
}
// Returns a snapshot from text containing '|' character markers with the markers removed, and DisplayPoints for each one.
#[track_caller]
pub fn marked_display_snapshot(
text: &str,
cx: &mut gpui::App,
@@ -83,6 +84,7 @@ pub fn marked_display_snapshot(
(snapshot, markers)
}
#[track_caller]
pub fn select_ranges(
editor: &mut Editor,
marked_text: &str,

View File

@@ -109,6 +109,7 @@ impl EditorTestContext {
}
}
#[track_caller]
pub fn new_multibuffer<const COUNT: usize>(
cx: &mut gpui::TestAppContext,
excerpts: [&str; COUNT],
@@ -351,6 +352,7 @@ impl EditorTestContext {
/// editor state was needed to cause the failure.
///
/// See the `util::test::marked_text_ranges` function for more information.
#[track_caller]
pub fn set_state(&mut self, marked_text: &str) -> ContextHandle {
let state_context = self.add_assertion_context(format!(
"Initial Editor State: \"{}\"",
@@ -367,6 +369,7 @@ impl EditorTestContext {
}
/// Only change the editor's selections
#[track_caller]
pub fn set_selections_state(&mut self, marked_text: &str) -> ContextHandle {
let state_context = self.add_assertion_context(format!(
"Initial Editor State: \"{}\"",

View File

@@ -635,12 +635,8 @@ impl WaylandWindowStatePtr {
let mut bounds: Option<Bounds<Pixels>> = None;
if let Some(mut input_handler) = state.input_handler.take() {
drop(state);
if let Some(selection) = input_handler.selected_text_range(true) {
bounds = input_handler.bounds_for_range(if selection.reversed {
selection.range.start..selection.range.start
} else {
selection.range.end..selection.range.end
});
if let Some(selection) = input_handler.marked_text_range() {
bounds = input_handler.bounds_for_range(selection.start..selection.start);
}
self.state.borrow_mut().input_handler = Some(input_handler);
}

View File

@@ -3701,6 +3701,7 @@ fn get_tree_sexp(buffer: &Entity<Buffer>, cx: &mut gpui::TestAppContext) -> Stri
}
// Assert that the enclosing bracket ranges around the selection match the pairs indicated by the marked text in `range_markers`
#[track_caller]
fn assert_bracket_pairs(
selection_text: &'static str,
bracket_pair_texts: Vec<&'static str>,

View File

@@ -1317,6 +1317,7 @@ fn assert_layers_for_range(
}
}
#[track_caller]
fn assert_capture_ranges(
syntax_map: &SyntaxMap,
buffer: &BufferSnapshot,

View File

@@ -374,7 +374,6 @@ pub trait LanguageModelProvider: 'static {
fn recommended_models(&self, _cx: &App) -> Vec<Arc<dyn LanguageModel>> {
Vec::new()
}
fn load_model(&self, _model: Arc<dyn LanguageModel>, _cx: &App) {}
fn is_authenticated(&self, cx: &App) -> bool;
fn authenticate(&self, cx: &mut App) -> Task<Result<(), AuthenticateError>>;
fn configuration_view(&self, window: &mut Window, cx: &mut App) -> AnyView;

View File

@@ -15,7 +15,7 @@ use language_model::{
LanguageModelRequest, RateLimiter, Role,
};
use lmstudio::{
ChatCompletionRequest, ChatMessage, ModelType, ResponseStreamEvent, get_models, preload_model,
ChatCompletionRequest, ChatMessage, ModelType, ResponseStreamEvent, get_models,
stream_chat_completion,
};
use schemars::JsonSchema;
@@ -216,15 +216,6 @@ impl LanguageModelProvider for LmStudioLanguageModelProvider {
.collect()
}
fn load_model(&self, model: Arc<dyn LanguageModel>, cx: &App) {
let settings = &AllLanguageModelSettings::get_global(cx).lmstudio;
let http_client = self.http_client.clone();
let api_url = settings.api_url.clone();
let id = model.id().0.to_string();
cx.spawn(async move |_| preload_model(http_client, &api_url, &id).await)
.detach_and_log_err(cx);
}
fn is_authenticated(&self, cx: &App) -> bool {
self.state.read(cx).is_authenticated()
}

View File

@@ -12,7 +12,7 @@ use language_model::{
};
use ollama::{
ChatMessage, ChatOptions, ChatRequest, ChatResponseDelta, KeepAlive, OllamaFunctionTool,
OllamaToolCall, get_models, preload_model, show_model, stream_chat_completion,
OllamaToolCall, get_models, show_model, stream_chat_completion,
};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
@@ -243,15 +243,6 @@ impl LanguageModelProvider for OllamaLanguageModelProvider {
models
}
fn load_model(&self, model: Arc<dyn LanguageModel>, cx: &App) {
let settings = &AllLanguageModelSettings::get_global(cx).ollama;
let http_client = self.http_client.clone();
let api_url = settings.api_url.clone();
let id = model.id().0.to_string();
cx.spawn(async move |_| preload_model(http_client, &api_url, &id).await)
.detach_and_log_err(cx);
}
fn is_authenticated(&self, cx: &App) -> bool {
self.state.read(cx).is_authenticated()
}

View File

@@ -3,7 +3,7 @@ use futures::{AsyncBufReadExt, AsyncReadExt, StreamExt, io::BufReader, stream::B
use http_client::{AsyncBody, HttpClient, Method, Request as HttpRequest, http};
use serde::{Deserialize, Serialize};
use serde_json::Value;
use std::{convert::TryFrom, sync::Arc, time::Duration};
use std::{convert::TryFrom, time::Duration};
pub const LMSTUDIO_API_URL: &str = "http://localhost:1234/api/v0";
@@ -391,34 +391,3 @@ pub async fn get_models(
serde_json::from_str(&body).context("Unable to parse LM Studio models response")?;
Ok(response.data)
}
/// Sends an empty request to LM Studio to trigger loading the model
pub async fn preload_model(client: Arc<dyn HttpClient>, api_url: &str, model: &str) -> Result<()> {
let uri = format!("{api_url}/completions");
let request = HttpRequest::builder()
.method(Method::POST)
.uri(uri)
.header("Content-Type", "application/json")
.body(AsyncBody::from(serde_json::to_string(
&serde_json::json!({
"model": model,
"messages": [],
"stream": false,
"max_tokens": 0,
}),
)?))?;
let mut response = client.send(request).await?;
if response.status().is_success() {
Ok(())
} else {
let mut body = String::new();
response.body_mut().read_to_string(&mut body).await?;
anyhow::bail!(
"Failed to connect to LM Studio API: {} {}",
response.status(),
body,
);
}
}

View File

@@ -3,7 +3,7 @@ use futures::{AsyncBufReadExt, AsyncReadExt, StreamExt, io::BufReader, stream::B
use http_client::{AsyncBody, HttpClient, Method, Request as HttpRequest, http};
use serde::{Deserialize, Serialize};
use serde_json::Value;
use std::{sync::Arc, time::Duration};
use std::time::Duration;
pub const OLLAMA_API_URL: &str = "http://localhost:11434";
@@ -357,36 +357,6 @@ pub async fn show_model(client: &dyn HttpClient, api_url: &str, model: &str) ->
Ok(details)
}
/// Sends an empty request to Ollama to trigger loading the model
pub async fn preload_model(client: Arc<dyn HttpClient>, api_url: &str, model: &str) -> Result<()> {
let uri = format!("{api_url}/api/generate");
let request = HttpRequest::builder()
.method(Method::POST)
.uri(uri)
.header("Content-Type", "application/json")
.body(AsyncBody::from(
serde_json::json!({
"model": model,
"keep_alive": "15m",
})
.to_string(),
))?;
let mut response = client.send(request).await?;
if response.status().is_success() {
Ok(())
} else {
let mut body = String::new();
response.body_mut().read_to_string(&mut body).await?;
anyhow::bail!(
"Failed to connect to Ollama API: {} {}",
response.status(),
body,
);
}
}
#[cfg(test)]
mod tests {
use super::*;

View File

@@ -82,6 +82,7 @@ text.workspace = true
toml.workspace = true
url.workspace = true
util.workspace = true
uuid.workspace = true
which.workspace = true
worktree.workspace = true
zlog.workspace = true

View File

@@ -8,6 +8,7 @@ use task::{
BuildTaskDefinition, DebugScenario, RevealStrategy, RevealTarget, Shell, SpawnInTerminal,
TaskTemplate,
};
use uuid::Uuid;
pub(crate) struct GoLocator;
@@ -31,11 +32,7 @@ impl DapLocator for GoLocator {
match go_action.as_str() {
"test" => {
let binary_path = if build_config.env.contains_key("OUT_DIR") {
"${OUT_DIR}/__debug".to_string()
} else {
"__debug".to_string()
};
let binary_path = format!("__debug_{}", Uuid::new_v4().simple());
let build_task = TaskTemplate {
label: "go test debug".into(),
@@ -133,14 +130,15 @@ impl DapLocator for GoLocator {
match go_action.as_str() {
"test" => {
let program = if let Some(out_dir) = build_config.env.get("OUT_DIR") {
format!("{}/__debug", out_dir)
} else {
PathBuf::from(&cwd)
.join("__debug")
.to_string_lossy()
.to_string()
};
let binary_arg = build_config
.args
.get(4)
.ok_or_else(|| anyhow::anyhow!("can't locate debug binary"))?;
let program = PathBuf::from(&cwd)
.join(binary_arg)
.to_string_lossy()
.into_owned();
Ok(DebugRequest::Launch(task::LaunchRequest {
program,
@@ -171,7 +169,7 @@ impl DapLocator for GoLocator {
#[cfg(test)]
mod tests {
use super::*;
use task::{HideStrategy, RevealStrategy, RevealTarget, Shell, TaskTemplate};
use task::{HideStrategy, RevealStrategy, RevealTarget, Shell, TaskId, TaskTemplate};
#[test]
fn test_create_scenario_for_go_run() {
@@ -318,7 +316,12 @@ mod tests {
.contains(&"-gcflags \"all=-N -l\"".into())
);
assert!(task_template.args.contains(&"-o".into()));
assert!(task_template.args.contains(&"__debug".into()));
assert!(
task_template
.args
.iter()
.any(|arg| arg.starts_with("__debug_"))
);
} else {
panic!("Expected BuildTaskDefinition::Template");
}
@@ -330,16 +333,14 @@ mod tests {
}
#[test]
fn test_create_scenario_for_go_test_with_out_dir() {
fn test_create_scenario_for_go_test_with_cwd_binary() {
let locator = GoLocator;
let mut env = FxHashMap::default();
env.insert("OUT_DIR".to_string(), "/tmp/build".to_string());
let task = TaskTemplate {
label: "go test".into(),
command: "go".into(),
args: vec!["test".into(), ".".into()],
env,
env: Default::default(),
cwd: Some("${ZED_WORKTREE_ROOT}".into()),
use_new_terminal: false,
allow_concurrent_runs: false,
@@ -359,7 +360,12 @@ mod tests {
let scenario = scenario.unwrap();
if let Some(BuildTaskDefinition::Template { task_template, .. }) = &scenario.build {
assert!(task_template.args.contains(&"${OUT_DIR}/__debug".into()));
assert!(
task_template
.args
.iter()
.any(|arg| arg.starts_with("__debug_"))
);
} else {
panic!("Expected BuildTaskDefinition::Template");
}
@@ -389,4 +395,42 @@ mod tests {
locator.create_scenario(&task, "test label", DebugAdapterName("Delve".into()));
assert!(scenario.is_none());
}
#[test]
fn test_run_go_test_missing_binary_path() {
let locator = GoLocator;
let build_config = SpawnInTerminal {
id: TaskId("test_task".to_string()),
full_label: "go test".to_string(),
label: "go test".to_string(),
command: "go".into(),
args: vec![
"test".into(),
"-c".into(),
"-gcflags \"all=-N -l\"".into(),
"-o".into(),
], // Missing the binary path (arg 4)
command_label: "go test -c -gcflags \"all=-N -l\" -o".to_string(),
env: Default::default(),
cwd: Some(PathBuf::from("/test/path")),
use_new_terminal: false,
allow_concurrent_runs: false,
reveal: RevealStrategy::Always,
reveal_target: RevealTarget::Dock,
hide: HideStrategy::Never,
shell: Shell::System,
show_summary: true,
show_command: true,
show_rerun: true,
};
let result = futures::executor::block_on(locator.run(build_config));
assert!(result.is_err());
assert!(
result
.unwrap_err()
.to_string()
.contains("can't locate debug binary")
);
}
}

View File

@@ -212,7 +212,7 @@ impl ConflictSet {
&& theirs_start.is_some()
{
let theirs_end = line_pos;
let conflict_end = line_end + 1;
let conflict_end = (line_end + 1).min(buffer.len());
let range = buffer.anchor_after(conflict_start.unwrap())
..buffer.anchor_before(conflict_end);

View File

@@ -2308,7 +2308,7 @@ impl LocalLspStore {
});
(false, lsp_delegate, servers)
});
let servers = servers
let servers_and_adapters = servers
.into_iter()
.filter_map(|server_node| {
if reused && server_node.server_id().is_none() {
@@ -2384,14 +2384,14 @@ impl LocalLspStore {
},
)?;
let server_state = self.language_servers.get(&server_id)?;
if let LanguageServerState::Running { server, .. } = server_state {
Some(server.clone())
if let LanguageServerState::Running { server, adapter, .. } = server_state {
Some((server.clone(), adapter.clone()))
} else {
None
}
})
.collect::<Vec<_>>();
for server in servers {
for (server, adapter) in servers_and_adapters {
buffer_handle.update(cx, |buffer, cx| {
buffer.set_completion_triggers(
server.server_id(),
@@ -2409,47 +2409,26 @@ impl LocalLspStore {
cx,
);
});
}
for adapter in self.languages.lsp_adapters(&language.name()) {
let servers = self
.language_server_ids
.get(&(worktree_id, adapter.name.clone()))
.map(|ids| {
ids.iter().flat_map(|id| {
self.language_servers.get(id).and_then(|server_state| {
if let LanguageServerState::Running { server, .. } = server_state {
Some(server.clone())
} else {
None
}
})
})
});
let servers = match servers {
Some(server) => server,
None => continue,
let snapshot = LspBufferSnapshot {
version: 0,
snapshot: initial_snapshot.clone(),
};
for server in servers {
let snapshot = LspBufferSnapshot {
version: 0,
snapshot: initial_snapshot.clone(),
};
self.buffer_snapshots
.entry(buffer_id)
.or_default()
.entry(server.server_id())
.or_insert_with(|| {
server.register_buffer(
uri.clone(),
adapter.language_id(&language.name()),
0,
initial_snapshot.text(),
);
self.buffer_snapshots
.entry(buffer_id)
.or_default()
.entry(server.server_id())
.or_insert_with(|| {
server.register_buffer(
uri.clone(),
adapter.language_id(&language.name()),
0,
initial_snapshot.text(),
);
vec![snapshot]
});
}
vec![snapshot]
});
}
}

View File

@@ -18,11 +18,12 @@ use file_icons::FileIcons;
use git::status::GitSummary;
use gpui::{
Action, AnyElement, App, ArcCow, AsyncWindowContext, Bounds, ClipboardItem, Context,
DismissEvent, Div, DragMoveEvent, Entity, EventEmitter, ExternalPaths, FocusHandle, Focusable,
Hsla, InteractiveElement, KeyContext, ListHorizontalSizingBehavior, ListSizingBehavior,
MouseButton, MouseDownEvent, ParentElement, Pixels, Point, PromptLevel, Render, ScrollStrategy,
Stateful, Styled, Subscription, Task, UniformListScrollHandle, WeakEntity, Window, actions,
anchored, deferred, div, impl_actions, point, px, size, transparent_white, uniform_list,
CursorStyle, DismissEvent, Div, DragMoveEvent, Entity, EventEmitter, ExternalPaths,
FocusHandle, Focusable, Hsla, InteractiveElement, KeyContext, ListHorizontalSizingBehavior,
ListSizingBehavior, Modifiers, ModifiersChangedEvent, MouseButton, MouseDownEvent,
ParentElement, Pixels, Point, PromptLevel, Render, ScrollStrategy, Stateful, Styled,
Subscription, Task, UniformListScrollHandle, WeakEntity, Window, actions, anchored, deferred,
div, impl_actions, point, px, size, transparent_white, uniform_list,
};
use indexmap::IndexMap;
use language::DiagnosticSeverity;
@@ -109,6 +110,7 @@ pub struct ProjectPanel {
// in case a user clicks to open a file.
mouse_down: bool,
hover_expand_task: Option<Task<()>>,
previous_drag_position: Option<Point<Pixels>>,
}
struct DragTargetEntry {
@@ -503,6 +505,7 @@ impl ProjectPanel {
scroll_handle,
mouse_down: false,
hover_expand_task: None,
previous_drag_position: None,
};
this.update_visible_entries(None, cx);
@@ -3106,6 +3109,29 @@ impl ProjectPanel {
.detach();
}
fn refresh_drag_cursor_style(
&self,
modifiers: &Modifiers,
window: &mut Window,
cx: &mut Context<Self>,
) {
if let Some(existing_cursor) = cx.active_drag_cursor_style() {
let new_cursor = if Self::is_copy_modifier_set(modifiers) {
CursorStyle::DragCopy
} else {
CursorStyle::PointingHand
};
if existing_cursor != new_cursor {
cx.set_active_drag_cursor_style(new_cursor, window);
}
}
}
fn is_copy_modifier_set(modifiers: &Modifiers) -> bool {
cfg!(target_os = "macos") && modifiers.alt
|| cfg!(not(target_os = "macos")) && modifiers.control
}
fn drag_onto(
&mut self,
selections: &DraggedSelection,
@@ -3114,9 +3140,7 @@ impl ProjectPanel {
window: &mut Window,
cx: &mut Context<Self>,
) {
let should_copy = cfg!(target_os = "macos") && window.modifiers().alt
|| cfg!(not(target_os = "macos")) && window.modifiers().control;
if should_copy {
if Self::is_copy_modifier_set(&window.modifiers()) {
let _ = maybe!({
let project = self.project.read(cx);
let target_worktree = project.worktree_for_entry(target_entry_id, cx)?;
@@ -3731,18 +3755,18 @@ impl ProjectPanel {
&self,
target_entry: &Entry,
target_worktree: &Worktree,
dragged_selection: &DraggedSelection,
drag_state: &DraggedSelection,
cx: &Context<Self>,
) -> Option<ProjectEntryId> {
let target_parent_path = target_entry.path.parent();
// In case of single item drag, we do not highlight existing
// directory which item belongs too
if dragged_selection.items().count() == 1 {
if drag_state.items().count() == 1 {
let active_entry_path = self
.project
.read(cx)
.path_for_entry(dragged_selection.active_selection.entry_id, cx)?;
.path_for_entry(drag_state.active_selection.entry_id, cx)?;
if let Some(active_parent_path) = active_entry_path.path.parent() {
// Do not highlight active entry parent
@@ -3962,11 +3986,11 @@ impl ProjectPanel {
return;
}
let drag_state = event.drag(cx);
let Some((entry_id, highlight_entry_id)) = maybe!({
let target_worktree = this.project.read(cx).worktree_for_id(selection.worktree_id, cx)?.read(cx);
let target_entry = target_worktree.entry_for_path(&path_for_dragged_selection)?;
let dragged_selection = event.drag(cx);
let highlight_entry_id = this.highlight_entry_for_selection_drag(target_entry, target_worktree, dragged_selection, cx);
let highlight_entry_id = this.highlight_entry_for_selection_drag(target_entry, target_worktree, drag_state, cx);
Some((target_entry.id, highlight_entry_id))
}) else {
return;
@@ -3976,7 +4000,10 @@ impl ProjectPanel {
entry_id,
highlight_entry_id,
});
this.marked_entries.clear();
if drag_state.items().count() == 1 {
this.marked_entries.clear();
this.marked_entries.insert(drag_state.active_selection);
}
this.hover_expand_task.take();
if !kind.is_dir()
@@ -4682,6 +4709,15 @@ impl Render for ProjectPanel {
window: &mut Window,
cx: &mut Context<ProjectPanel>,
) {
if let Some(previous_position) = this.previous_drag_position {
// Refresh cursor only when an actual drag happens,
// because modifiers are not updated when the cursor is not moved.
if e.event.position != previous_position {
this.refresh_drag_cursor_style(&e.event.modifiers, window, cx);
}
}
this.previous_drag_position = Some(e.event.position);
if !e.bounds.contains(&e.event.position) {
this.drag_target_entry = None;
return;
@@ -4741,6 +4777,11 @@ impl Render for ProjectPanel {
.on_drag_move(cx.listener(handle_drag_move::<DraggedSelection>))
.size_full()
.relative()
.on_modifiers_changed(cx.listener(
|this, event: &ModifiersChangedEvent, window, cx| {
this.refresh_drag_cursor_style(&event.modifiers, window, cx);
},
))
.on_hover(cx.listener(|this, hovered, window, cx| {
if *hovered {
this.show_scrollbar = true;

View File

@@ -237,30 +237,6 @@ impl TaskTemplate {
env
};
// We filter out env variables here that aren't set so we don't have extra white space in args
let args = self
.args
.iter()
.filter(|arg| {
shellexpand::env_with_context(arg, |var| {
let colon_position = var.find(':').unwrap_or(var.len());
let (variable_name, default) = var.split_at(colon_position);
if env
.get(variable_name)
.is_some_and(|arg| !arg.trim().is_empty())
|| !default.is_empty()
{
Ok(Some(""))
} else {
Err(anyhow::anyhow!("Empty argument should be filtered out"))
}
})
.is_ok()
})
.cloned()
.collect();
Some(ResolvedTask {
id: id.clone(),
substituted_variables,
@@ -280,7 +256,7 @@ impl TaskTemplate {
},
),
command,
args,
args: self.args.clone(),
env,
use_new_terminal: self.use_new_terminal,
allow_concurrent_runs: self.allow_concurrent_runs,
@@ -727,7 +703,6 @@ mod tests {
label: "My task".into(),
command: "echo".into(),
args: vec!["$PATH".into()],
env: HashMap::from_iter([("PATH".to_owned(), "non-empty".to_owned())]),
..TaskTemplate::default()
};
let resolved_task = task
@@ -740,32 +715,6 @@ mod tests {
assert_eq!(resolved.args, task.args);
}
#[test]
fn test_empty_env_variables_excluded_from_args() {
let task = TaskTemplate {
label: "My task".into(),
command: "echo".into(),
args: vec![
"$EMPTY_VAR".into(),
"hello".into(),
"$WHITESPACE_VAR".into(),
"$UNDEFINED_VAR".into(),
"$WORLD".into(),
],
env: HashMap::from_iter([
("EMPTY_VAR".to_owned(), "".to_owned()),
("WHITESPACE_VAR".to_owned(), " ".to_owned()),
("WORLD".to_owned(), "non-empty".to_owned()),
]),
..TaskTemplate::default()
};
let resolved_task = task
.resolve_task(TEST_ID_BASE, &TaskContext::default())
.unwrap();
let resolved = resolved_task.resolved;
assert_eq!(resolved.args, vec!["hello", "$WORLD"]);
}
#[test]
fn test_errors_on_missing_zed_variable() {
let task = TaskTemplate {
@@ -780,85 +729,6 @@ mod tests {
);
}
// The fish shell doesn't handle white space well so we want to filter out empty environment variables
// this test ensures that we maintain this behavior
#[test]
fn test_mixed_env_variable_formats_in_args() {
let task = TaskTemplate {
label: "Mixed env test".into(),
command: "echo".into(),
args: vec![
"start".into(),
"$DEFINED_VAR".into(),
"${ANOTHER_DEFINED}".into(),
"$UNDEFINED_VAR".into(),
"${UNDEFINED_BRACES}".into(),
"${UNDEFINED_BRACES: 5}".into(),
"$EMPTY_VAR".into(),
"${WHITESPACE_VAR}".into(),
"middle".into(),
"${ZED_WORKTREE_ROOT}".into(),
"${UNDEFINED_VAR}/bin".into(),
"$PATH".into(),
"end".into(),
],
env: HashMap::from_iter([
("DEFINED_VAR".to_owned(), "value1".to_owned()),
("ANOTHER_DEFINED".to_owned(), "value2".to_owned()),
("EMPTY_VAR".to_owned(), "".to_owned()),
("WHITESPACE_VAR".to_owned(), " ".to_owned()),
("PATH".to_owned(), "/usr/bin:/usr/local/bin".to_owned()),
]),
..TaskTemplate::default()
};
let context = TaskContext {
cwd: Some(PathBuf::from("/project")),
task_variables: TaskVariables::from_iter([(
VariableName::WorktreeRoot,
"/project".into(),
)]),
..TaskContext::default()
};
let resolved_task = task.resolve_task(TEST_ID_BASE, &context).unwrap();
let resolved = resolved_task.resolved;
// Verify that:
// - Regular args like "start", "middle", "end" remain
// - Defined env vars ($DEFINED_VAR, ${ANOTHER_DEFINED}, $PATH) remain
// - Undefined env vars ($UNDEFINED_VAR, ${UNDEFINED_BRACES}) are filtered out
// - Empty/whitespace env vars ($EMPTY_VAR, ${WHITESPACE_VAR}) are filtered out
// - Zed variables (${ZED_WORKTREE_ROOT}) remain as they're resolved to task variables
assert_eq!(
resolved.args,
vec![
"start",
"$DEFINED_VAR",
"${ANOTHER_DEFINED}",
"${UNDEFINED_BRACES: 5}",
"middle",
"${ZED_WORKTREE_ROOT}",
"$PATH",
"end"
]
);
assert_eq!(resolved.env.get("DEFINED_VAR"), Some(&"value1".to_owned()));
assert_eq!(
resolved.env.get("ANOTHER_DEFINED"),
Some(&"value2".to_owned())
);
assert_eq!(
resolved.env.get("PATH"),
Some(&"/usr/bin:/usr/local/bin".to_owned())
);
assert_eq!(
resolved.env.get("ZED_WORKTREE_ROOT"),
Some(&"/project".to_owned())
);
}
#[test]
fn test_symbol_dependent_tasks() {
let task_with_all_properties = TaskTemplate {

View File

@@ -1662,11 +1662,13 @@ impl Buffer {
#[cfg(any(test, feature = "test-support"))]
impl Buffer {
#[track_caller]
pub fn edit_via_marked_text(&mut self, marked_string: &str) {
let edits = self.edits_for_marked_text(marked_string);
self.edit(edits);
}
#[track_caller]
pub fn edits_for_marked_text(&self, marked_string: &str) -> Vec<(Range<usize>, String)> {
let old_text = self.text();
let (new_text, mut ranges) = util::test::marked_text_ranges(marked_string, false);

View File

@@ -109,6 +109,7 @@ pub fn marked_text_ranges_by(
/// Any • characters in the input string will be replaced with spaces. This makes
/// it easier to test cases with trailing spaces, which tend to get trimmed from the
/// source code.
#[track_caller]
pub fn marked_text_ranges(
marked_text: &str,
ranges_are_directed: bool,
@@ -176,6 +177,7 @@ pub fn marked_text_ranges(
(unmarked_text, ranges)
}
#[track_caller]
pub fn marked_text_offsets(marked_text: &str) -> (String, Vec<usize>) {
let (text, ranges) = marked_text_ranges(marked_text, false);
(

View File

@@ -2,7 +2,7 @@
description = "The fast, collaborative code editor."
edition.workspace = true
name = "zed"
version = "0.190.0"
version = "0.191.0"
publish.workspace = true
license = "GPL-3.0-or-later"
authors = ["Zed Team <hi@zed.dev>"]

View File

@@ -13,13 +13,14 @@ Here's an overview of the supported providers and tool call support:
| ----------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| [Amazon Bedrock](#amazon-bedrock) | Depends on the model |
| [Anthropic](#anthropic) | ✅ |
| [DeepSeek](#deepseek) | 🚫 |
| [DeepSeek](#deepseek) | |
| [GitHub Copilot Chat](#github-copilot-chat) | For Some Models ([link](https://github.com/zed-industries/zed/blob/9e0330ba7d848755c9734bf456c716bddf0973f3/crates/language_models/src/provider/copilot_chat.rs#L189-L198)) |
| [Google AI](#google-ai) | ✅ |
| [LM Studio](#lmstudio) | ✅ |
| [Mistral](#mistral) | ✅ |
| [Ollama](#ollama) | ✅ |
| [OpenAI](#openai) | ✅ |
| [OpenRouter](#openrouter) | ✅ |
| [OpenAI API Compatible](#openai-api-compatible) | 🚫 |
## Use Your Own Keys {#use-your-own-keys}
@@ -164,7 +165,7 @@ You can configure a model to use [extended thinking](https://docs.anthropic.com/
### DeepSeek {#deepseek}
> 🚫 Does not support tool use
> ✅ Supports tool use
1. Visit the DeepSeek platform and [create an API key](https://platform.deepseek.com/api_keys)
2. Open the settings view (`agent: open configuration`) and go to the DeepSeek section
@@ -351,7 +352,9 @@ Depending on your hardware or use-case you may wish to limit or increase the con
"name": "qwen2.5-coder",
"display_name": "qwen 2.5 coder 32K",
"max_tokens": 32768,
"supports_tools": true
"supports_tools": true,
"supports_thinking": true,
"supports_images": true
}
]
}
@@ -371,6 +374,12 @@ The `supports_tools` option controls whether or not the model will use additiona
If the model is tagged with `tools` in the Ollama catalog this option should be supplied, and built in profiles `Ask` and `Write` can be used.
If the model is not tagged with `tools` in the Ollama catalog, this option can still be supplied with value `true`; however be aware that only the `Minimal` built in profile will work.
The `supports_thinking` option controls whether or not the model will perform an explicit “thinking” (reasoning) pass before producing its final answer.
If the model is tagged with `thinking` in the Ollama catalog, set this option and you can use it in zed.
The `supports_images` option enables the models vision capabilities, allowing it to process images included in the conversation context.
If the model is tagged with `vision` in the Ollama catalog, set this option and you can use it in zed.
### OpenAI {#openai}
> ✅ Supports tool use
@@ -416,6 +425,21 @@ You must provide the model's Context Window in the `max_tokens` parameter; this
OpenAI `o1` models should set `max_completion_tokens` as well to avoid incurring high reasoning token costs.
Custom models will be listed in the model dropdown in the Agent Panel.
### OpenRouter {#openrouter}
> ✅ Supports tool use
OpenRouter provides access to multiple AI models through a single API. It supports tool use for compatible models.
1. Visit [OpenRouter](https://openrouter.ai) and create an account
2. Generate an API key from your [OpenRouter keys page](https://openrouter.ai/keys)
3. Open the settings view (`agent: open configuration`) and go to the OpenRouter section
4. Enter your OpenRouter API key
The OpenRouter API key will be saved in your keychain.
Zed will also use the `OPENROUTER_API_KEY` environment variable if it's defined.
### OpenAI API Compatible {#openai-api-compatible}
Zed supports using OpenAI compatible APIs by specifying a custom `endpoint` and `available_models` for the OpenAI provider.

View File

@@ -107,9 +107,18 @@ To enable [Inlay Hints](../configuring-languages#inlay-hints) for LuaLS in Zed
## Formatting
### LuaLS
### LuaLS Formatting
To enable auto-formatting with your LuaLS (provided by [CppCXY/EmmyLuaCodeStyle](https://github.com/CppCXY/EmmyLuaCodeStyle)) make sure you have `"format.enable": true,` in your .luarc.json add the following to your Zed `settings.json`:
To enable auto-formatting with your LuaLS (provided by [CppCXY/EmmyLuaCodeStyle](https://github.com/CppCXY/EmmyLuaCodeStyle)) make sure you have `"format.enable": true,` in your .luarc.json:
```json
{
"$schema": "https://raw.githubusercontent.com/sumneko/vscode-lua/master/setting/schema.json",
"format.enable": true
}
```
Then add the following to your Zed `settings.json`:
```json
{
@@ -124,7 +133,7 @@ To enable auto-formatting with your LuaLS (provided by [CppCXY/EmmyLuaCodeStyle]
You can customize various EmmyLuaCodeStyle style options via `.editorconfig`, see [lua.template.editorconfig](https://github.com/CppCXY/EmmyLuaCodeStyle/blob/master/lua.template.editorconfig) for all available options.
### StyLua
### StyLua Formatting
Alternatively to use [StyLua](https://github.com/JohnnyMorganz/StyLua) for auto-formatting:

View File

@@ -97,7 +97,7 @@ Prepared new Zed versions locally. You will need to push the branches and open a
# To push and open a PR to update main:
git push origin \\
git push -u origin \\
${preview_tag_name} \\
${stable_tag_name} \\
${minor_branch_name} \\

38
script/digital-ocean-db.sh Executable file
View File

@@ -0,0 +1,38 @@
#!/bin/bash
set -e
# Check if database name is provided
if [ $# -eq 0 ]; then
echo "Usage: $0 <database-name>"
doctl databases list
exit 1
fi
DATABASE_NAME="$1"
DATABASE_ID=$(doctl databases list --format ID,Name --no-header | grep "$DATABASE_NAME" | awk '{print $1}')
if [ -z "$DATABASE_ID" ]; then
echo "Error: Database '$DATABASE_NAME' not found"
exit 1
fi
CURRENT_IP=$(curl -s https://api.ipify.org)
if [ -z "$CURRENT_IP" ]; then
echo "Error: Failed to get current IP address"
exit 1
fi
EXISTING_RULE=$(doctl databases firewalls list "$DATABASE_ID" | grep "ip_addr" | grep "$CURRENT_IP")
if [ -z "$EXISTING_RULE" ]; then
echo "IP not found in whitelist. Adding $CURRENT_IP to database firewall..."
doctl databases firewalls append "$DATABASE_ID" --rule ip_addr:"$CURRENT_IP"
fi
CONNECTION_URL=$(doctl databases connection "$DATABASE_ID" --format URI --no-header)
if [ -z "$CONNECTION_URL" ]; then
echo "Error: Failed to get database connection details"
exit 1
fi
psql "$CONNECTION_URL"

210
script/github-pr-status Executable file
View File

@@ -0,0 +1,210 @@
#!/usr/bin/env python3
"""
GitHub PR Analyzer for zed-industries/zed repository
Downloads all PRs and groups them by first assignee with status, open date, and last updated date.
"""
import urllib.request
import urllib.parse
import urllib.error
import json
from datetime import datetime
from collections import defaultdict
import sys
import os
# GitHub API configuration
GITHUB_API_BASE = "https://api.github.com"
REPO_OWNER = "zed-industries"
REPO_NAME = "zed"
GITHUB_TOKEN = os.getenv("GITHUB_TOKEN")
def make_github_request(url, params=None):
"""Make a request to GitHub API with proper headers and pagination support."""
if params:
url_parts = list(urllib.parse.urlparse(url))
query = dict(urllib.parse.parse_qsl(url_parts[4]))
query.update(params)
url_parts[4] = urllib.parse.urlencode(query)
url = urllib.parse.urlunparse(url_parts)
req = urllib.request.Request(url)
req.add_header("Accept", "application/vnd.github.v3+json")
req.add_header("User-Agent", "GitHub-PR-Analyzer")
if GITHUB_TOKEN:
req.add_header("Authorization", f"token {GITHUB_TOKEN}")
try:
response = urllib.request.urlopen(req)
return response
except urllib.error.URLError as e:
print(f"Error making request to {url}: {e}")
return None
except urllib.error.HTTPError as e:
print(f"HTTP error {e.code} for {url}: {e.reason}")
return None
def fetch_all_prs():
"""Fetch all PRs from the repository using pagination."""
prs = []
page = 1
per_page = 100
print("Fetching PRs from GitHub API...")
while True:
url = f"{GITHUB_API_BASE}/repos/{REPO_OWNER}/{REPO_NAME}/pulls"
params = {
"state": "open",
"sort": "updated",
"direction": "desc",
"per_page": per_page,
"page": page
}
response = make_github_request(url, params)
if not response:
break
try:
data = response.read().decode('utf-8')
page_prs = json.loads(data)
except (json.JSONDecodeError, UnicodeDecodeError) as e:
print(f"Error parsing response: {e}")
break
if not page_prs:
break
prs.extend(page_prs)
print(f"Fetched page {page}: {len(page_prs)} PRs (Total: {len(prs)})")
# Check if we have more pages
link_header = response.getheader('Link', '')
if 'rel="next"' not in link_header:
break
page += 1
print(f"Total PRs fetched: {len(prs)}")
return prs
def format_date_as_days_ago(date_string):
"""Format ISO date string as 'X days ago'."""
if not date_string:
return "N/A days ago"
try:
dt = datetime.fromisoformat(date_string.replace('Z', '+00:00'))
now = datetime.now(dt.tzinfo)
days_diff = (now - dt).days
if days_diff == 0:
return "today"
elif days_diff == 1:
return "1 day ago"
else:
return f"{days_diff} days ago"
except:
return "N/A days ago"
def get_first_assignee(pr):
"""Get the first assignee from a PR, or return 'Unassigned' if none."""
assignees = pr.get('assignees', [])
if assignees:
return assignees[0].get('login', 'Unknown')
return 'Unassigned'
def get_pr_status(pr):
"""Determine if PR is draft or ready for review."""
if pr.get('draft', False):
return "Draft"
return "Ready"
def analyze_prs(prs):
"""Group PRs by first assignee and organize the data."""
grouped_prs = defaultdict(list)
for pr in prs:
assignee = get_first_assignee(pr)
pr_info = {
'number': pr['number'],
'title': pr['title'],
'status': get_pr_status(pr),
'state': pr['state'],
'created_at': format_date_as_days_ago(pr['created_at']),
'updated_at': format_date_as_days_ago(pr['updated_at']),
'updated_at_raw': pr['updated_at'],
'url': pr['html_url'],
'author': pr['user']['login']
}
grouped_prs[assignee].append(pr_info)
# Sort PRs within each group by update date (newest first)
for assignee in grouped_prs:
grouped_prs[assignee].sort(key=lambda x: x['updated_at_raw'], reverse=True)
return dict(grouped_prs)
def print_pr_report(grouped_prs):
"""Print formatted report of PRs grouped by assignee."""
print(f"OPEN PR REPORT FOR {REPO_OWNER}/{REPO_NAME}")
print()
# Sort assignees alphabetically, but put 'Unassigned' last
assignees = sorted(grouped_prs.keys())
if 'Unassigned' in assignees:
assignees.remove('Unassigned')
assignees.append('Unassigned')
total_prs = sum(len(prs) for prs in grouped_prs.values())
print(f"Total Open PRs: {total_prs}")
print()
for assignee in assignees:
prs = grouped_prs[assignee]
assignee_display = f"@{assignee}" if assignee != 'Unassigned' else assignee
print(f"assigned to {assignee_display} ({len(prs)} PRs):")
for pr in prs:
print(f"- {pr['author']}: [{pr['title']}]({pr['url']}) opened:{pr['created_at']} updated:{pr['updated_at']}")
print()
def save_json_report(grouped_prs, filename="pr_report.json"):
"""Save the PR data to a JSON file."""
try:
with open(filename, 'w') as f:
json.dump(grouped_prs, f, indent=2)
print(f"📄 Report saved to {filename}")
except Exception as e:
print(f"Error saving JSON report: {e}")
def main():
"""Main function to orchestrate the PR analysis."""
print("GitHub PR Analyzer")
print("==================")
if not GITHUB_TOKEN:
print("⚠️ Warning: GITHUB_TOKEN not set. You may hit rate limits.")
print(" Set GITHUB_TOKEN environment variable for authenticated requests.")
print()
# Fetch all PRs
prs = fetch_all_prs()
if not prs:
print("❌ Failed to fetch PRs. Please check your connection and try again.")
sys.exit(1)
# Analyze and group PRs
grouped_prs = analyze_prs(prs)
# Print report
print_pr_report(grouped_prs)
if __name__ == "__main__":
main()