Compare commits

..

2 Commits

Author SHA1 Message Date
Nate Butler
a7cb23fe78 Update component preview 2025-04-16 16:44:01 -04:00
Nate Butler
c294b4d0b8 Add ui_parking_lot and agent/usage_banner
Add ui_parking_lot and agent/usage_banner
2025-04-16 14:49:56 -04:00
119 changed files with 2221 additions and 3233 deletions

View File

@@ -17,7 +17,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout repo
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
uses: actions/checkout@v4
with:
clean: false

166
Cargo.lock generated
View File

@@ -324,7 +324,7 @@ dependencies = [
"schemars",
"serde",
"serde_json",
"strum 0.27.1",
"strum",
"thiserror 2.0.12",
"workspace-hack",
]
@@ -337,9 +337,9 @@ checksum = "34cd60c5e3152cef0a592f1b296f1cc93715d89d2551d85315828c3a09575ff4"
[[package]]
name = "anyhow"
version = "1.0.98"
version = "1.0.97"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e16d2d3311acee920a9eb8d33b8cbc1787ce4a264e85f964c2404b969bdcd487"
checksum = "dcfed56ad506cb2c684a14971b8861fdc3baaaae314b9e5f9bb532cbe3ba7a4f"
[[package]]
name = "approx"
@@ -448,6 +448,7 @@ dependencies = [
"smol",
"tempfile",
"util",
"which 6.0.3",
"workspace-hack",
]
@@ -566,7 +567,7 @@ dependencies = [
"settings",
"smallvec",
"smol",
"strum 0.27.1",
"strum",
"telemetry_events",
"text",
"theme",
@@ -703,7 +704,6 @@ dependencies = [
"assistant_tool",
"chrono",
"collections",
"feature_flags",
"futures 0.3.31",
"gpui",
"html_to_markdown",
@@ -721,11 +721,9 @@ dependencies = [
"ui",
"unindent",
"util",
"web_search",
"workspace",
"workspace-hack",
"worktree",
"zed_llm_client",
]
[[package]]
@@ -1883,7 +1881,7 @@ dependencies = [
"schemars",
"serde",
"serde_json",
"strum 0.27.1",
"strum",
"thiserror 2.0.12",
"tokio",
"workspace-hack",
@@ -3030,7 +3028,7 @@ dependencies = [
"settings",
"sha2",
"sqlx",
"strum 0.27.1",
"strum",
"subtle",
"supermaven_api",
"telemetry_events",
@@ -3050,7 +3048,6 @@ dependencies = [
"workspace",
"workspace-hack",
"worktree",
"zed_llm_client",
]
[[package]]
@@ -3200,6 +3197,7 @@ dependencies = [
"serde",
"ui",
"ui_input",
"ui_parking_lot",
"workspace",
"workspace-hack",
]
@@ -3363,7 +3361,7 @@ dependencies = [
"serde",
"serde_json",
"settings",
"strum 0.27.1",
"strum",
"task",
"theme",
"ui",
@@ -4480,7 +4478,7 @@ dependencies = [
"optfield",
"proc-macro2",
"quote",
"strum 0.26.3",
"strum",
"syn 2.0.100",
]
@@ -4919,6 +4917,36 @@ dependencies = [
"workspace-hack",
]
[[package]]
name = "evals"
version = "0.1.0"
dependencies = [
"anyhow",
"clap",
"client",
"clock",
"collections",
"dap",
"env_logger 0.11.8",
"feature_flags",
"fs",
"gpui",
"http_client",
"language",
"languages",
"node_runtime",
"open_ai",
"project",
"reqwest_client",
"semantic_index",
"serde",
"serde_json",
"settings",
"smol",
"util",
"workspace-hack",
]
[[package]]
name = "event-listener"
version = "2.5.3"
@@ -5095,7 +5123,7 @@ dependencies = [
"serde",
"settings",
"smallvec",
"strum 0.27.1",
"strum",
"telemetry",
"theme",
"ui",
@@ -5946,7 +5974,7 @@ dependencies = [
"serde_derive",
"serde_json",
"settings",
"strum 0.27.1",
"strum",
"telemetry",
"theme",
"time",
@@ -6039,7 +6067,7 @@ dependencies = [
"schemars",
"serde",
"serde_json",
"strum 0.27.1",
"strum",
"workspace-hack",
]
@@ -6145,7 +6173,7 @@ dependencies = [
"slotmap",
"smallvec",
"smol",
"strum 0.27.1",
"strum",
"sum_tree",
"taffy",
"thiserror 2.0.12",
@@ -6793,7 +6821,7 @@ name = "icons"
version = "0.1.0"
dependencies = [
"serde",
"strum 0.27.1",
"strum",
"workspace-hack",
]
@@ -7061,7 +7089,7 @@ dependencies = [
"paths",
"pretty_assertions",
"serde",
"strum 0.27.1",
"strum",
"util",
"workspace-hack",
]
@@ -7647,7 +7675,7 @@ dependencies = [
"serde",
"serde_json",
"smol",
"strum 0.27.1",
"strum",
"telemetry_events",
"thiserror 2.0.12",
"util",
@@ -7707,7 +7735,7 @@ dependencies = [
"serde_json",
"settings",
"smol",
"strum 0.27.1",
"strum",
"theme",
"thiserror 2.0.12",
"tiktoken-rs",
@@ -7715,7 +7743,6 @@ dependencies = [
"ui",
"util",
"workspace-hack",
"zed_llm_client",
]
[[package]]
@@ -8680,7 +8707,7 @@ dependencies = [
"schemars",
"serde",
"serde_json",
"strum 0.27.1",
"strum",
"workspace-hack",
]
@@ -9527,7 +9554,7 @@ dependencies = [
"schemars",
"serde",
"serde_json",
"strum 0.27.1",
"strum",
"workspace-hack",
]
@@ -11721,7 +11748,6 @@ name = "remote_server"
version = "0.1.0"
dependencies = [
"anyhow",
"askpass",
"async-watch",
"backtrace",
"cargo_toml",
@@ -12107,7 +12133,7 @@ dependencies = [
"serde",
"serde_json",
"sha2",
"strum 0.27.1",
"strum",
"tracing",
"util",
"workspace-hack",
@@ -12635,7 +12661,7 @@ dependencies = [
"serde",
"serde_json",
"sqlx",
"strum 0.26.3",
"strum",
"thiserror 2.0.12",
"time",
"tracing",
@@ -13300,7 +13326,6 @@ dependencies = [
"fs",
"futures 0.3.31",
"gpui",
"indoc",
"parking_lot",
"paths",
"schemars",
@@ -13681,7 +13706,7 @@ dependencies = [
"settings",
"simplelog",
"story",
"strum 0.27.1",
"strum",
"theme",
"title_bar",
"ui",
@@ -13763,16 +13788,7 @@ version = "0.26.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06"
dependencies = [
"strum_macros 0.26.4",
]
[[package]]
name = "strum"
version = "0.27.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f64def088c51c9510a8579e3c5d67c65349dcf755e5479ad3d010aa6454e2c32"
dependencies = [
"strum_macros 0.27.1",
"strum_macros",
]
[[package]]
@@ -13788,19 +13804,6 @@ dependencies = [
"syn 2.0.100",
]
[[package]]
name = "strum_macros"
version = "0.27.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c77a8c5abcaf0f9ce05d62342b7d298c346515365c36b673df4ebe3ced01fde8"
dependencies = [
"heck 0.5.0",
"proc-macro2",
"quote",
"rustversion",
"syn 2.0.100",
]
[[package]]
name = "subtle"
version = "2.6.1"
@@ -14416,7 +14419,7 @@ dependencies = [
"serde_json_lenient",
"serde_repr",
"settings",
"strum 0.27.1",
"strum",
"thiserror 2.0.12",
"util",
"uuid",
@@ -14450,7 +14453,7 @@ dependencies = [
"serde_json",
"serde_json_lenient",
"simplelog",
"strum 0.27.1",
"strum",
"theme",
"vscode_theme",
"workspace-hack",
@@ -15451,7 +15454,7 @@ dependencies = [
"settings",
"smallvec",
"story",
"strum 0.27.1",
"strum",
"theme",
"ui_macros",
"util",
@@ -15485,6 +15488,19 @@ dependencies = [
"workspace-hack",
]
[[package]]
name = "ui_parking_lot"
version = "0.1.0"
dependencies = [
"anyhow",
"component",
"gpui",
"linkme",
"ui",
"util",
"workspace",
]
[[package]]
name = "ui_prompt"
version = "0.1.0"
@@ -16584,36 +16600,6 @@ dependencies = [
"wasm-bindgen",
]
[[package]]
name = "web_search"
version = "0.1.0"
dependencies = [
"anyhow",
"collections",
"gpui",
"serde",
"workspace-hack",
"zed_llm_client",
]
[[package]]
name = "web_search_providers"
version = "0.1.0"
dependencies = [
"anyhow",
"client",
"feature_flags",
"futures 0.3.31",
"gpui",
"http_client",
"language_model",
"serde",
"serde_json",
"web_search",
"workspace-hack",
"zed_llm_client",
]
[[package]]
name = "webpki-root-certs"
version = "0.26.8"
@@ -17652,7 +17638,7 @@ dependencies = [
"settings",
"smallvec",
"sqlez",
"strum 0.27.1",
"strum",
"task",
"telemetry",
"tempfile",
@@ -17797,7 +17783,7 @@ dependencies = [
"sqlx-macros-core",
"sqlx-postgres",
"sqlx-sqlite",
"strum 0.26.3",
"strum",
"subtle",
"syn 1.0.109",
"syn 2.0.100",
@@ -18175,7 +18161,6 @@ dependencies = [
"agent",
"anyhow",
"ashpd",
"askpass",
"assets",
"assistant",
"assistant_context_editor",
@@ -18293,8 +18278,6 @@ dependencies = [
"uuid",
"vim",
"vim_mode_setting",
"web_search",
"web_search_providers",
"welcome",
"windows 0.61.1",
"winresource",
@@ -18359,13 +18342,12 @@ dependencies = [
[[package]]
name = "zed_llm_client"
version = "0.5.1"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9ee4d410dbc030c3e6e3af78fc76296f6bebe20dcb6d7d3fa24bca306fc8c1ce"
checksum = "1bf21350eced858d129840589158a8f6895c4fa4327ae56dd8c7d6a98495bed4"
dependencies = [
"serde",
"serde_json",
"strum 0.27.1",
"uuid",
]

View File

@@ -47,6 +47,7 @@ members = [
"crates/docs_preprocessor",
"crates/editor",
"crates/eval",
"crates/evals",
"crates/extension",
"crates/extension_api",
"crates/extension_cli",
@@ -157,6 +158,7 @@ members = [
"crates/title_bar",
"crates/toolchain_selector",
"crates/ui",
"crates/ui_parking_lot",
"crates/ui_input",
"crates/ui_macros",
"crates/ui_prompt",
@@ -164,8 +166,6 @@ members = [
"crates/util_macros",
"crates/vim",
"crates/vim_mode_setting",
"crates/web_search",
"crates/web_search_providers",
"crates/welcome",
"crates/workspace",
"crates/worktree",
@@ -366,13 +366,12 @@ toolchain_selector = { path = "crates/toolchain_selector" }
ui = { path = "crates/ui" }
ui_input = { path = "crates/ui_input" }
ui_macros = { path = "crates/ui_macros" }
ui_parking_lot = { path = "crates/ui_parking_lot" }
ui_prompt = { path = "crates/ui_prompt" }
util = { path = "crates/util" }
util_macros = { path = "crates/util_macros" }
vim = { path = "crates/vim" }
vim_mode_setting = { path = "crates/vim_mode_setting" }
web_search = { path = "crates/web_search" }
web_search_providers = { path = "crates/web_search_providers" }
welcome = { path = "crates/welcome" }
workspace = { path = "crates/workspace" }
worktree = { path = "crates/worktree" }
@@ -539,7 +538,7 @@ smol = "2.0"
sqlformat = "0.2"
streaming-iterator = "0.1"
strsim = "0.11"
strum = { version = "0.27.0", features = ["derive"] }
strum = { version = "0.26.0", features = ["derive"] }
subtle = "2.5.0"
syn = { version = "1.0.72", features = ["full", "extra-traits"] }
sys-locale = "0.3.1"
@@ -604,7 +603,7 @@ wasmtime-wasi = "29"
which = "6.0.0"
wit-component = "0.221"
workspace-hack = "0.1.0"
zed_llm_client = "0.5.1"
zed_llm_client = "0.4"
zstd = "0.11"
metal = "0.29"
@@ -695,6 +694,7 @@ breadcrumbs = { codegen-units = 1 }
collections = { codegen-units = 1 }
command_palette = { codegen-units = 1 }
command_palette_hooks = { codegen-units = 1 }
evals = { codegen-units = 1 }
extension_cli = { codegen-units = 1 }
feature_flags = { codegen-units = 1 }
file_icons = { codegen-units = 1 }

View File

@@ -134,9 +134,7 @@
"shift-f10": "editor::OpenContextMenu",
"ctrl-shift-e": "editor::ToggleEditPrediction",
"f9": "editor::ToggleBreakpoint",
"shift-f9": "editor::EditLogBreakpoint",
"ctrl-shift-backspace": "editor::GoToPreviousChange",
"ctrl-shift-alt-backspace": "editor::GoToNextChange"
"shift-f9": "editor::EditLogBreakpoint"
}
},
{
@@ -632,7 +630,6 @@
"ctrl-alt-n": "agent::NewTextThread",
"ctrl-shift-h": "agent::OpenHistory",
"ctrl-alt-c": "agent::OpenConfiguration",
"ctrl-alt-p": "assistant::OpenPromptLibrary",
"ctrl-i": "agent::ToggleProfileSelector",
"ctrl-alt-/": "assistant::ToggleModelSelector",
"ctrl-shift-a": "agent::ToggleContextPicker",
@@ -721,7 +718,7 @@
"alt-shift-copy": "workspace::CopyRelativePath",
"alt-ctrl-shift-c": "workspace::CopyRelativePath",
"alt-ctrl-r": "outline_panel::RevealInFileManager",
"space": "outline_panel::OpenSelectedEntry",
"space": "outline_panel::Open",
"shift-down": "menu::SelectNext",
"shift-up": "menu::SelectPrevious",
"alt-enter": "editor::OpenExcerpts",

View File

@@ -286,7 +286,6 @@
"cmd-alt-n": "agent::NewTextThread",
"cmd-shift-h": "agent::OpenHistory",
"cmd-alt-c": "agent::OpenConfiguration",
"cmd-alt-p": "assistant::OpenPromptLibrary",
"cmd-i": "agent::ToggleProfileSelector",
"cmd-alt-/": "assistant::ToggleModelSelector",
"cmd-shift-a": "agent::ToggleContextPicker",
@@ -542,9 +541,7 @@
"cmd-\\": "pane::SplitRight",
"cmd-k v": "markdown::OpenPreviewToTheSide",
"cmd-shift-v": "markdown::OpenPreview",
"ctrl-cmd-c": "editor::DisplayCursorNames",
"cmd-shift-backspace": "editor::GoToPreviousChange",
"cmd-shift-alt-backspace": "editor::GoToNextChange"
"ctrl-cmd-c": "editor::DisplayCursorNames"
}
},
{
@@ -789,7 +786,7 @@
"cmd-alt-c": "workspace::CopyPath",
"alt-cmd-shift-c": "workspace::CopyRelativePath",
"alt-cmd-r": "outline_panel::RevealInFileManager",
"space": "outline_panel::OpenSelectedEntry",
"space": "outline_panel::Open",
"shift-down": "menu::SelectNext",
"shift-up": "menu::SelectPrevious",
"alt-enter": "editor::OpenExcerpts",

View File

@@ -652,8 +652,7 @@
"path_search": true,
"read_file": true,
"regex_search": true,
"thinking": true,
"web_search": true
"thinking": true
}
},
"write": {
@@ -679,8 +678,7 @@
"regex_search": true,
"rename": true,
"symbol_info": true,
"thinking": true,
"web_search": true
"thinking": true
}
}
},

View File

@@ -1,30 +1,27 @@
use crate::context::{AssistantContext, ContextId, format_context_as_string};
use crate::context::{AssistantContext, ContextId};
use crate::context_picker::MentionLink;
use crate::thread::{
LastRestoreCheckpoint, MessageId, MessageSegment, RequestKind, Thread, ThreadError,
ThreadEvent, ThreadFeedback,
};
use crate::thread_store::{RulesLoadingError, ThreadStore};
use crate::tool_use::{PendingToolUseStatus, ToolUse};
use crate::tool_use::{PendingToolUseStatus, ToolUse, ToolUseStatus};
use crate::ui::{AddedContext, AgentNotification, AgentNotificationEvent, ContextPill};
use crate::{AssistantPanel, OpenActiveThreadAsMarkdown};
use anyhow::Context as _;
use assistant_settings::{AssistantSettings, NotifyWhenAgentWaiting};
use assistant_tool::ToolUseStatus;
use collections::{HashMap, HashSet};
use editor::scroll::Autoscroll;
use editor::{Editor, EditorElement, EditorEvent, EditorStyle, MultiBuffer};
use editor::{Editor, EditorElement, EditorStyle, MultiBuffer};
use gpui::{
AbsoluteLength, Animation, AnimationExt, AnyElement, App, ClickEvent, ClipboardItem,
DefiniteLength, EdgesRefinement, Empty, Entity, EventEmitter, Focusable, Hsla, ListAlignment,
ListState, MouseButton, PlatformDisplay, ScrollHandle, Stateful, StyleRefinement, Subscription,
Task, TextStyle, TextStyleRefinement, Transformation, UnderlineStyle, WeakEntity, WindowHandle,
DefiniteLength, EdgesRefinement, Empty, Entity, Focusable, Hsla, ListAlignment, ListState,
MouseButton, PlatformDisplay, ScrollHandle, Stateful, StyleRefinement, Subscription, Task,
TextStyle, TextStyleRefinement, Transformation, UnderlineStyle, WeakEntity, WindowHandle,
linear_color_stop, linear_gradient, list, percentage, pulsating_between,
};
use language::{Buffer, LanguageRegistry};
use language_model::{
LanguageModelRegistry, LanguageModelRequestMessage, LanguageModelToolUseId, Role, StopReason,
};
use language_model::{LanguageModelRegistry, LanguageModelToolUseId, Role, StopReason};
use markdown::parser::{CodeBlockKind, CodeBlockMetadata};
use markdown::{HeadingLevelStyles, Markdown, MarkdownElement, MarkdownStyle, ParsedMarkdown};
use project::ProjectItem as _;
@@ -684,9 +681,6 @@ fn open_markdown_link(
struct EditMessageState {
editor: Entity<Editor>,
last_estimated_token_count: Option<usize>,
_subscription: Subscription,
_update_token_count_task: Option<Task<anyhow::Result<()>>>,
}
impl ActiveThread {
@@ -756,10 +750,6 @@ impl ActiveThread {
this
}
pub fn context_store(&self) -> &Entity<ContextStore> {
&self.context_store
}
pub fn thread(&self) -> &Entity<Thread> {
&self.thread
}
@@ -790,13 +780,6 @@ impl ActiveThread {
self.last_error.take();
}
/// Returns the editing message id and the estimated token count in the content
pub fn editing_message_id(&self) -> Option<(MessageId, usize)> {
self.editing_message
.as_ref()
.map(|(id, state)| (*id, state.last_estimated_token_count.unwrap_or(0)))
}
fn push_message(
&mut self,
id: &MessageId,
@@ -960,8 +943,8 @@ impl ActiveThread {
&tool_use.input,
self.thread
.read(cx)
.output_for_tool(&tool_use.id)
.map(|output| output.clone().into())
.tool_result(&tool_use.id)
.map(|result| result.content.clone().into())
.unwrap_or("".into()),
cx,
);
@@ -1142,91 +1125,15 @@ impl ActiveThread {
editor.move_to_end(&editor::actions::MoveToEnd, window, cx);
editor
});
let subscription = cx.subscribe(&editor, |this, _, event, cx| match event {
EditorEvent::BufferEdited => {
this.update_editing_message_token_count(true, cx);
}
_ => {}
});
self.editing_message = Some((
message_id,
EditMessageState {
editor: editor.clone(),
last_estimated_token_count: None,
_subscription: subscription,
_update_token_count_task: None,
},
));
self.update_editing_message_token_count(false, cx);
cx.notify();
}
fn update_editing_message_token_count(&mut self, debounce: bool, cx: &mut Context<Self>) {
let Some((message_id, state)) = self.editing_message.as_mut() else {
return;
};
cx.emit(ActiveThreadEvent::EditingMessageTokenCountChanged);
state._update_token_count_task.take();
let Some(default_model) = LanguageModelRegistry::read_global(cx).default_model() else {
state.last_estimated_token_count.take();
return;
};
let editor = state.editor.clone();
let thread = self.thread.clone();
let message_id = *message_id;
state._update_token_count_task = Some(cx.spawn(async move |this, cx| {
if debounce {
cx.background_executor()
.timer(Duration::from_millis(200))
.await;
}
let token_count = if let Some(task) = cx.update(|cx| {
let context = thread.read(cx).context_for_message(message_id);
let new_context = thread.read(cx).filter_new_context(context);
let context_text =
format_context_as_string(new_context, cx).unwrap_or(String::new());
let message_text = editor.read(cx).text(cx);
let content = context_text + &message_text;
if content.is_empty() {
return None;
}
let request = language_model::LanguageModelRequest {
messages: vec![LanguageModelRequestMessage {
role: language_model::Role::User,
content: vec![content.into()],
cache: false,
}],
tools: vec![],
stop: vec![],
temperature: None,
};
Some(default_model.model.count_tokens(request, cx))
})? {
task.await?
} else {
0
};
this.update(cx, |this, cx| {
let Some((_message_id, state)) = this.editing_message.as_mut() else {
return;
};
state.last_estimated_token_count = Some(token_count);
cx.emit(ActiveThreadEvent::EditingMessageTokenCountChanged);
})
}));
}
fn cancel_editing_message(&mut self, _: &menu::Cancel, _: &mut Window, cx: &mut Context<Self>) {
self.editing_message.take();
cx.notify();
@@ -1495,12 +1402,12 @@ impl ActiveThread {
let editor_bg_color = colors.editor_background;
let bg_user_message_header = editor_bg_color.blend(active_color.opacity(0.25));
let open_as_markdown = IconButton::new(("open-as-markdown", ix), IconName::FileCode)
let open_as_markdown = IconButton::new("open-as-markdown", IconName::FileCode)
.shape(ui::IconButtonShape::Square)
.icon_size(IconSize::XSmall)
.icon_color(Color::Ignored)
.tooltip(Tooltip::text("Open Thread as Markdown"))
.on_click(|_, window, cx| {
.on_click(|_event, window, cx| {
window.dispatch_action(Box::new(OpenActiveThreadAsMarkdown), cx)
});
@@ -1768,9 +1675,6 @@ impl ActiveThread {
"confirm-edit-message",
"Regenerate",
)
.disabled(
edit_message_editor.read(cx).is_empty(cx),
)
.label_size(LabelSize::Small)
.key_binding(
KeyBinding::for_action_in(
@@ -1833,16 +1737,8 @@ impl ActiveThread {
),
};
let after_editing_message = self
.editing_message
.as_ref()
.map_or(false, |(editing_message_id, _)| {
message_id > *editing_message_id
});
v_flex()
.w_full()
.when(after_editing_message, |parent| parent.opacity(0.2))
.when_some(checkpoint, |parent, checkpoint| {
let mut is_pending = false;
let mut error = None;
@@ -2383,15 +2279,12 @@ impl ActiveThread {
window: &mut Window,
cx: &mut Context<Self>,
) -> impl IntoElement + use<> {
if let Some(card) = self.thread.read(cx).card_for_tool(&tool_use.id) {
return card.render(&tool_use.status, window, cx);
}
let is_open = self
.expanded_tool_uses
.get(&tool_use.id)
.copied()
.unwrap_or_default();
let is_status_finished = matches!(&tool_use.status, ToolUseStatus::Finished(_));
let fs = self
@@ -2450,9 +2343,6 @@ impl ActiveThread {
rendered.input.clone(),
tool_use_markdown_style(window, cx),
)
.code_block_renderer(markdown::CodeBlockRenderer::Default {
copy_button: false,
})
.on_url_click({
let workspace = self.workspace.clone();
move |text, window, cx| {
@@ -2479,16 +2369,12 @@ impl ActiveThread {
rendered.output.clone(),
tool_use_markdown_style(window, cx),
)
.code_block_renderer(markdown::CodeBlockRenderer::Default {
copy_button: false,
})
.on_url_click({
let workspace = self.workspace.clone();
move |text, window, cx| {
open_markdown_link(text, workspace.clone(), window, cx);
}
})
.into_any_element()
}),
)),
),
@@ -2545,7 +2431,6 @@ impl ActiveThread {
open_markdown_link(text, workspace.clone(), window, cx);
}
})
.into_any_element()
})),
),
),
@@ -2659,7 +2544,7 @@ impl ActiveThread {
)
} else {
v_flex()
.my_2()
.my_3()
.rounded_lg()
.border_1()
.border_color(self.tool_card_border_color(cx))
@@ -2876,7 +2761,7 @@ impl ActiveThread {
)
})
}
}).into_any_element()
})
}
fn render_rules_item(&self, cx: &Context<Self>) -> AnyElement {
@@ -3068,12 +2953,6 @@ impl ActiveThread {
}
}
pub enum ActiveThreadEvent {
EditingMessageTokenCountChanged,
}
impl EventEmitter<ActiveThreadEvent> for ActiveThread {}
impl Render for ActiveThread {
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
v_flex()
@@ -3149,21 +3028,28 @@ pub(crate) fn open_context(
.start
.to_point(&snapshot);
open_editor_at_position(project_path, target_position, &workspace, window, cx)
.detach();
}
}
AssistantContext::Excerpt(excerpt_context) => {
if let Some(project_path) = excerpt_context
.context_buffer
.buffer
.read(cx)
.project_path(cx)
{
let snapshot = excerpt_context.context_buffer.buffer.read(cx).snapshot();
let target_position = excerpt_context.range.start.to_point(&snapshot);
open_editor_at_position(project_path, target_position, &workspace, window, cx)
let open_task = workspace.update(cx, |workspace, cx| {
workspace.open_path(project_path, None, true, window, cx)
});
window
.spawn(cx, async move |cx| {
if let Some(active_editor) = open_task
.await
.log_err()
.and_then(|item| item.downcast::<Editor>())
{
active_editor
.downgrade()
.update_in(cx, |editor, window, cx| {
editor.go_to_singleton_buffer_point(
target_position,
window,
cx,
);
})
.log_err();
}
})
.detach();
}
}
@@ -3184,29 +3070,3 @@ pub(crate) fn open_context(
}
}
}
fn open_editor_at_position(
project_path: project::ProjectPath,
target_position: Point,
workspace: &Entity<Workspace>,
window: &mut Window,
cx: &mut App,
) -> Task<()> {
let open_task = workspace.update(cx, |workspace, cx| {
workspace.open_path(project_path, None, true, window, cx)
});
window.spawn(cx, async move |cx| {
if let Some(active_editor) = open_task
.await
.log_err()
.and_then(|item| item.downcast::<Editor>())
{
active_editor
.downgrade()
.update_in(cx, |editor, window, cx| {
editor.go_to_singleton_buffer_point(target_position, window, cx);
})
.log_err();
}
})
}

View File

@@ -1,4 +1,3 @@
use std::ops::Range;
use std::path::PathBuf;
use std::sync::Arc;
use std::time::Duration;
@@ -6,14 +5,14 @@ use std::time::Duration;
use anyhow::{Result, anyhow};
use assistant_context_editor::{
AssistantPanelDelegate, ConfigurationError, ContextEditor, SlashCommandCompletionProvider,
humanize_token_count, make_lsp_adapter_delegate, render_remaining_tokens,
make_lsp_adapter_delegate, render_remaining_tokens,
};
use assistant_settings::{AssistantDockPosition, AssistantSettings};
use assistant_slash_command::SlashCommandWorkingSet;
use assistant_tool::ToolWorkingSet;
use client::zed_urls;
use editor::{Anchor, AnchorRangeExt as _, Editor, EditorEvent, MultiBuffer};
use editor::{Editor, EditorEvent, MultiBuffer};
use fs::Fs;
use gpui::{
Action, Animation, AnimationExt as _, AnyElement, App, AsyncWindowContext, Corner, Entity,
@@ -26,7 +25,6 @@ use language_model_selector::ToggleModelSelector;
use project::Project;
use prompt_library::{PromptLibrary, open_prompt_library};
use prompt_store::PromptBuilder;
use proto::Plan;
use settings::{Settings, update_settings_file};
use time::UtcOffset;
use ui::{
@@ -38,10 +36,10 @@ use workspace::dock::{DockPosition, Panel, PanelEvent};
use zed_actions::agent::OpenConfiguration;
use zed_actions::assistant::{OpenPromptLibrary, ToggleFocus};
use crate::active_thread::{ActiveThread, ActiveThreadEvent};
use crate::active_thread::ActiveThread;
use crate::assistant_configuration::{AssistantConfiguration, AssistantConfigurationEvent};
use crate::history_store::{HistoryEntry, HistoryStore};
use crate::message_editor::{MessageEditor, MessageEditorEvent};
use crate::message_editor::MessageEditor;
use crate::thread::{Thread, ThreadError, ThreadId, TokenUsageRatio};
use crate::thread_history::{PastContext, PastThread, ThreadHistory};
use crate::thread_store::ThreadStore;
@@ -113,9 +111,7 @@ enum ActiveView {
change_title_editor: Entity<Editor>,
_subscriptions: Vec<gpui::Subscription>,
},
PromptEditor {
context_editor: Entity<ContextEditor>,
},
PromptEditor,
History,
Configuration,
}
@@ -184,9 +180,10 @@ pub struct AssistantPanel {
language_registry: Arc<LanguageRegistry>,
thread_store: Entity<ThreadStore>,
thread: Entity<ActiveThread>,
_thread_subscription: Subscription,
message_editor: Entity<MessageEditor>,
_active_thread_subscriptions: Vec<Subscription>,
context_store: Entity<assistant_context_editor::ContextStore>,
context_editor: Option<Entity<ContextEditor>>,
configuration: Option<Entity<AssistantConfiguration>>,
configuration_subscription: Option<Subscription>,
local_timezone: UtcOffset,
@@ -266,13 +263,6 @@ impl AssistantPanel {
)
});
let message_editor_subscription =
cx.subscribe(&message_editor, |_, _, event, cx| match event {
MessageEditorEvent::Changed | MessageEditorEvent::EstimatedTokenCount => {
cx.notify();
}
});
let history_store =
cx.new(|cx| HistoryStore::new(thread_store.clone(), context_store.clone(), cx));
@@ -297,12 +287,6 @@ impl AssistantPanel {
)
});
let active_thread_subscription = cx.subscribe(&thread, |_, _, event, cx| match &event {
ActiveThreadEvent::EditingMessageTokenCountChanged => {
cx.notify();
}
});
Self {
active_view,
workspace,
@@ -311,13 +295,10 @@ impl AssistantPanel {
language_registry,
thread_store: thread_store.clone(),
thread,
_thread_subscription: thread_subscription,
message_editor,
_active_thread_subscriptions: vec![
thread_subscription,
active_thread_subscription,
message_editor_subscription,
],
context_store,
context_editor: None,
configuration: None,
configuration_subscription: None,
local_timezone: UtcOffset::from_whole_seconds(
@@ -400,13 +381,6 @@ impl AssistantPanel {
.detach_and_log_err(cx);
}
let thread_subscription = cx.subscribe(&thread, |_, _, event, cx| {
if let ThreadEvent::MessageAdded(_) = &event {
// needed to leave empty state
cx.notify();
}
});
self.thread = cx.new(|cx| {
ActiveThread::new(
thread.clone(),
@@ -419,12 +393,12 @@ impl AssistantPanel {
)
});
let active_thread_subscription =
cx.subscribe(&self.thread, |_, _, event, cx| match &event {
ActiveThreadEvent::EditingMessageTokenCountChanged => {
cx.notify();
}
});
self._thread_subscription = cx.subscribe(&thread, |_, _, event, cx| {
if let ThreadEvent::MessageAdded(_) = &event {
// needed to leave empty state
cx.notify();
}
});
self.message_editor = cx.new(|cx| {
MessageEditor::new(
@@ -438,22 +412,11 @@ impl AssistantPanel {
)
});
self.message_editor.focus_handle(cx).focus(window);
let message_editor_subscription =
cx.subscribe(&self.message_editor, |_, _, event, cx| match event {
MessageEditorEvent::Changed | MessageEditorEvent::EstimatedTokenCount => {
cx.notify();
}
});
self._active_thread_subscriptions = vec![
thread_subscription,
active_thread_subscription,
message_editor_subscription,
];
}
fn new_prompt_editor(&mut self, window: &mut Window, cx: &mut Context<Self>) {
self.set_active_view(ActiveView::PromptEditor, window, cx);
let context = self
.context_store
.update(cx, |context_store, cx| context_store.create(cx));
@@ -461,7 +424,7 @@ impl AssistantPanel {
.log_err()
.flatten();
let context_editor = cx.new(|cx| {
self.context_editor = Some(cx.new(|cx| {
let mut editor = ContextEditor::for_context(
context,
self.fs.clone(),
@@ -473,16 +436,11 @@ impl AssistantPanel {
);
editor.insert_default_prompt(window, cx);
editor
});
}));
self.set_active_view(
ActiveView::PromptEditor {
context_editor: context_editor.clone(),
},
window,
cx,
);
context_editor.focus_handle(cx).focus(window);
if let Some(context_editor) = self.context_editor.as_ref() {
context_editor.focus_handle(cx).focus(window);
}
}
fn deploy_prompt_library(
@@ -549,13 +507,8 @@ impl AssistantPanel {
cx,
)
});
this.set_active_view(
ActiveView::PromptEditor {
context_editor: editor,
},
window,
cx,
);
this.set_active_view(ActiveView::PromptEditor, window, cx);
this.context_editor = Some(editor);
anyhow::Ok(())
})??;
@@ -584,13 +537,6 @@ impl AssistantPanel {
Some(this.thread_store.downgrade()),
)
});
let thread_subscription = cx.subscribe(&thread, |_, _, event, cx| {
if let ThreadEvent::MessageAdded(_) = &event {
// needed to leave empty state
cx.notify();
}
});
this.thread = cx.new(|cx| {
ActiveThread::new(
thread.clone(),
@@ -602,14 +548,6 @@ impl AssistantPanel {
cx,
)
});
let active_thread_subscription =
cx.subscribe(&this.thread, |_, _, event, cx| match &event {
ActiveThreadEvent::EditingMessageTokenCountChanged => {
cx.notify();
}
});
this.message_editor = cx.new(|cx| {
MessageEditor::new(
this.fs.clone(),
@@ -622,19 +560,6 @@ impl AssistantPanel {
)
});
this.message_editor.focus_handle(cx).focus(window);
let message_editor_subscription =
cx.subscribe(&this.message_editor, |_, _, event, cx| match event {
MessageEditorEvent::Changed | MessageEditorEvent::EstimatedTokenCount => {
cx.notify();
}
});
this._active_thread_subscriptions = vec![
thread_subscription,
active_thread_subscription,
message_editor_subscription,
];
})
})
}
@@ -786,15 +711,8 @@ impl AssistantPanel {
.update(cx, |this, cx| this.delete_thread(thread_id, cx))
}
pub(crate) fn has_active_thread(&self) -> bool {
matches!(self.active_view, ActiveView::Thread { .. })
}
pub(crate) fn active_context_editor(&self) -> Option<Entity<ContextEditor>> {
match &self.active_view {
ActiveView::PromptEditor { context_editor } => Some(context_editor.clone()),
_ => None,
}
self.context_editor.clone()
}
pub(crate) fn delete_context(
@@ -832,10 +750,16 @@ impl AssistantPanel {
impl Focusable for AssistantPanel {
fn focus_handle(&self, cx: &App) -> FocusHandle {
match &self.active_view {
match self.active_view {
ActiveView::Thread { .. } => self.message_editor.focus_handle(cx),
ActiveView::History => self.history.focus_handle(cx),
ActiveView::PromptEditor { context_editor } => context_editor.focus_handle(cx),
ActiveView::PromptEditor => {
if let Some(context_editor) = self.context_editor.as_ref() {
context_editor.focus_handle(cx)
} else {
cx.focus_handle()
}
}
ActiveView::Configuration => {
if let Some(configuration) = self.configuration.as_ref() {
configuration.focus_handle(cx)
@@ -928,7 +852,7 @@ impl Panel for AssistantPanel {
}
impl AssistantPanel {
fn render_title_view(&self, _window: &mut Window, cx: &Context<Self>) -> AnyElement {
fn render_title_view(&self, _window: &mut Window, cx: &mut Context<Self>) -> AnyElement {
const LOADING_SUMMARY_PLACEHOLDER: &str = "Loading Summary…";
let content = match &self.active_view {
@@ -959,8 +883,15 @@ impl AssistantPanel {
.into_any_element()
}
}
ActiveView::PromptEditor { context_editor } => {
let title = SharedString::from(context_editor.read(cx).title(cx).to_string());
ActiveView::PromptEditor => {
let title = self
.context_editor
.as_ref()
.map(|context_editor| {
SharedString::from(context_editor.read(cx).title(cx).to_string())
})
.unwrap_or_else(|| SharedString::from(LOADING_SUMMARY_PLACEHOLDER));
Label::new(title).ml_2().truncate().into_any_element()
}
ActiveView::History => Label::new("History").truncate().into_any_element(),
@@ -981,18 +912,21 @@ impl AssistantPanel {
fn render_toolbar(&self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let active_thread = self.thread.read(cx);
let thread = active_thread.thread().read(cx);
let token_usage = thread.total_token_usage(cx);
let thread_id = thread.id().clone();
let is_generating = thread.is_generating();
let is_empty = active_thread.is_empty();
let focus_handle = self.focus_handle(cx);
let is_history = matches!(self.active_view, ActiveView::History);
let show_token_count = match &self.active_view {
ActiveView::Thread { .. } => !is_empty,
ActiveView::PromptEditor { .. } => true,
ActiveView::PromptEditor => self.context_editor.is_some(),
_ => false,
};
let focus_handle = self.focus_handle(cx);
let go_back_button = match &self.active_view {
ActiveView::History | ActiveView::Configuration => Some(
div().pl_1().child(
@@ -1039,9 +973,69 @@ impl AssistantPanel {
h_flex()
.h_full()
.gap_2()
.when(show_token_count, |parent|
parent.children(self.render_token_count(&thread, cx))
)
.when(show_token_count, |parent| match self.active_view {
ActiveView::Thread { .. } => {
if token_usage.total == 0 {
return parent;
}
let token_color = match token_usage.ratio {
TokenUsageRatio::Normal => Color::Muted,
TokenUsageRatio::Warning => Color::Warning,
TokenUsageRatio::Exceeded => Color::Error,
};
parent.child(
h_flex()
.flex_shrink_0()
.gap_0p5()
.child(
Label::new(assistant_context_editor::humanize_token_count(
token_usage.total,
))
.size(LabelSize::Small)
.color(token_color)
.map(|label| {
if is_generating {
label
.with_animation(
"used-tokens-label",
Animation::new(Duration::from_secs(2))
.repeat()
.with_easing(pulsating_between(
0.6, 1.,
)),
|label, delta| label.alpha(delta),
)
.into_any()
} else {
label.into_any_element()
}
}),
)
.child(
Label::new("/").size(LabelSize::Small).color(Color::Muted),
)
.child(
Label::new(assistant_context_editor::humanize_token_count(
token_usage.max,
))
.size(LabelSize::Small)
.color(Color::Muted),
),
)
}
ActiveView::PromptEditor => {
let Some(editor) = self.context_editor.as_ref() else {
return parent;
};
let Some(element) = render_remaining_tokens(editor, cx) else {
return parent;
};
parent.child(element)
}
_ => parent,
})
.child(
h_flex()
.h_full()
@@ -1118,16 +1112,16 @@ impl AssistantPanel {
"New Text Thread",
NewTextThread.boxed_clone(),
)
.action("Prompt Library", Box::new(OpenPromptLibrary))
.action("Settings", Box::new(OpenConfiguration))
.action("Settings", OpenConfiguration.boxed_clone())
.separator()
.action(
"Install MCPs",
Box::new(zed_actions::Extensions {
zed_actions::Extensions {
category_filter: Some(
zed_actions::ExtensionCategoryFilter::ContextServers,
),
}),
}
.boxed_clone(),
)
},
))
@@ -1137,110 +1131,6 @@ impl AssistantPanel {
)
}
fn render_token_count(&self, thread: &Thread, cx: &App) -> Option<AnyElement> {
let is_generating = thread.is_generating();
let message_editor = self.message_editor.read(cx);
let conversation_token_usage = thread.total_token_usage(cx);
let (total_token_usage, is_estimating) = if let Some((editing_message_id, unsent_tokens)) =
self.thread.read(cx).editing_message_id()
{
let combined = thread
.token_usage_up_to_message(editing_message_id, cx)
.add(unsent_tokens);
(combined, unsent_tokens > 0)
} else {
let unsent_tokens = message_editor.last_estimated_token_count().unwrap_or(0);
let combined = conversation_token_usage.add(unsent_tokens);
(combined, unsent_tokens > 0)
};
let is_waiting_to_update_token_count = message_editor.is_waiting_to_update_token_count();
match &self.active_view {
ActiveView::Thread { .. } => {
if total_token_usage.total == 0 {
return None;
}
let token_color = match total_token_usage.ratio() {
TokenUsageRatio::Normal if is_estimating => Color::Default,
TokenUsageRatio::Normal => Color::Muted,
TokenUsageRatio::Warning => Color::Warning,
TokenUsageRatio::Exceeded => Color::Error,
};
let token_count = h_flex()
.id("token-count")
.flex_shrink_0()
.gap_0p5()
.when(!is_generating && is_estimating, |parent| {
parent
.child(
h_flex()
.mr_0p5()
.size_2()
.justify_center()
.rounded_full()
.bg(cx.theme().colors().text.opacity(0.1))
.child(
div().size_1().rounded_full().bg(cx.theme().colors().text),
),
)
.tooltip(move |window, cx| {
Tooltip::with_meta(
"Estimated New Token Count",
None,
format!(
"Current Conversation Tokens: {}",
humanize_token_count(conversation_token_usage.total)
),
window,
cx,
)
})
})
.child(
Label::new(humanize_token_count(total_token_usage.total))
.size(LabelSize::Small)
.color(token_color)
.map(|label| {
if is_generating || is_waiting_to_update_token_count {
label
.with_animation(
"used-tokens-label",
Animation::new(Duration::from_secs(2))
.repeat()
.with_easing(pulsating_between(0.6, 1.)),
|label, delta| label.alpha(delta),
)
.into_any()
} else {
label.into_any_element()
}
}),
)
.child(Label::new("/").size(LabelSize::Small).color(Color::Muted))
.child(
Label::new(humanize_token_count(total_token_usage.max))
.size(LabelSize::Small)
.color(Color::Muted),
)
.into_any();
Some(token_count)
}
ActiveView::PromptEditor { context_editor } => {
let element = render_remaining_tokens(context_editor, cx)?;
Some(element.into_any_element())
}
_ => None,
}
}
fn render_active_thread_or_empty_state(
&self,
window: &mut Window,
@@ -1559,9 +1449,6 @@ impl AssistantPanel {
ThreadError::MaxMonthlySpendReached => {
self.render_max_monthly_spend_reached_error(cx)
}
ThreadError::ModelRequestLimitReached { plan } => {
self.render_model_request_limit_reached_error(plan, cx)
}
ThreadError::Message { header, message } => {
self.render_error_message(header, message, cx)
}
@@ -1664,71 +1551,6 @@ impl AssistantPanel {
.into_any()
}
fn render_model_request_limit_reached_error(
&self,
plan: Plan,
cx: &mut Context<Self>,
) -> AnyElement {
let error_message = match plan {
Plan::ZedPro => {
"Model request limit reached. Upgrade to usage-based billing for more requests."
}
Plan::ZedProTrial => {
"Model request limit reached. Upgrade to Zed Pro for more requests."
}
Plan::Free => "Model request limit reached. Upgrade to Zed Pro for more requests.",
};
let call_to_action = match plan {
Plan::ZedPro => "Upgrade to usage-based billing",
Plan::ZedProTrial => "Upgrade to Zed Pro",
Plan::Free => "Upgrade to Zed Pro",
};
v_flex()
.gap_0p5()
.child(
h_flex()
.gap_1p5()
.items_center()
.child(Icon::new(IconName::XCircle).color(Color::Error))
.child(Label::new("Model Request Limit Reached").weight(FontWeight::MEDIUM)),
)
.child(
div()
.id("error-message")
.max_h_24()
.overflow_y_scroll()
.child(Label::new(error_message)),
)
.child(
h_flex()
.justify_end()
.mt_1()
.child(
Button::new("subscribe", call_to_action).on_click(cx.listener(
|this, _, _, cx| {
this.thread.update(cx, |this, _cx| {
this.clear_last_error();
});
cx.open_url(&zed_urls::account_url(cx));
cx.notify();
},
)),
)
.child(Button::new("dismiss", "Dismiss").on_click(cx.listener(
|this, _, _, cx| {
this.thread.update(cx, |this, _cx| {
this.clear_last_error();
});
cx.notify();
},
))),
)
.into_any()
}
fn render_error_message(
&self,
header: SharedString,
@@ -1771,7 +1593,7 @@ impl AssistantPanel {
fn key_context(&self) -> KeyContext {
let mut key_context = KeyContext::new_with_defaults();
key_context.add("AgentPanel");
if matches!(self.active_view, ActiveView::PromptEditor { .. }) {
if matches!(self.active_view, ActiveView::PromptEditor) {
key_context.add("prompt_editor");
}
key_context
@@ -1799,13 +1621,13 @@ impl Render for AssistantPanel {
.on_action(cx.listener(Self::open_agent_diff))
.on_action(cx.listener(Self::go_back))
.child(self.render_toolbar(window, cx))
.map(|parent| match &self.active_view {
.map(|parent| match self.active_view {
ActiveView::Thread { .. } => parent
.child(self.render_active_thread_or_empty_state(window, cx))
.child(h_flex().child(self.message_editor.clone()))
.children(self.render_last_error(cx)),
ActiveView::History => parent.child(self.history.clone()),
ActiveView::PromptEditor { context_editor } => parent.child(context_editor.clone()),
ActiveView::PromptEditor => parent.children(self.context_editor.clone()),
ActiveView::Configuration => parent.children(self.configuration.clone()),
})
}
@@ -1870,7 +1692,7 @@ impl AssistantPanelDelegate for ConcreteAssistantPanelDelegate {
cx: &mut Context<Workspace>,
) -> Option<Entity<ContextEditor>> {
let panel = workspace.panel::<AssistantPanel>(cx)?;
panel.read(cx).active_context_editor()
panel.update(cx, |panel, _cx| panel.context_editor.clone())
}
fn open_saved_context(
@@ -1901,59 +1723,10 @@ impl AssistantPanelDelegate for ConcreteAssistantPanelDelegate {
fn quote_selection(
&self,
workspace: &mut Workspace,
selection_ranges: Vec<Range<Anchor>>,
buffer: Entity<MultiBuffer>,
window: &mut Window,
cx: &mut Context<Workspace>,
_workspace: &mut Workspace,
_creases: Vec<(String, String)>,
_window: &mut Window,
_cx: &mut Context<Workspace>,
) {
let Some(panel) = workspace.panel::<AssistantPanel>(cx) else {
return;
};
if !panel.focus_handle(cx).contains_focused(window, cx) {
workspace.toggle_panel_focus::<AssistantPanel>(window, cx);
}
panel.update(cx, |_, cx| {
// Wait to create a new context until the workspace is no longer
// being updated.
cx.defer_in(window, move |panel, window, cx| {
if panel.has_active_thread() {
panel.thread.update(cx, |thread, cx| {
thread.context_store().update(cx, |store, cx| {
let buffer = buffer.read(cx);
let selection_ranges = selection_ranges
.into_iter()
.flat_map(|range| {
let (start_buffer, start) =
buffer.text_anchor_for_position(range.start, cx)?;
let (end_buffer, end) =
buffer.text_anchor_for_position(range.end, cx)?;
if start_buffer != end_buffer {
return None;
}
Some((start_buffer, start..end))
})
.collect::<Vec<_>>();
for (buffer, range) in selection_ranges {
store.add_excerpt(range, buffer, cx).detach_and_log_err(cx);
}
})
})
} else if let Some(context_editor) = panel.active_context_editor() {
let snapshot = buffer.read(cx).snapshot(cx);
let selection_ranges = selection_ranges
.into_iter()
.map(|range| range.to_point(&snapshot))
.collect::<Vec<_>>();
context_editor.update(cx, |context_editor, cx| {
context_editor.quote_ranges(selection_ranges, snapshot, window, cx)
});
}
});
});
}
}

View File

@@ -4,7 +4,6 @@ use gpui::{App, Entity, SharedString};
use language::{Buffer, File};
use language_model::LanguageModelRequestMessage;
use project::{ProjectPath, Worktree};
use rope::Point;
use serde::{Deserialize, Serialize};
use text::{Anchor, BufferId};
use ui::IconName;
@@ -24,7 +23,6 @@ pub enum ContextKind {
File,
Directory,
Symbol,
Excerpt,
FetchedUrl,
Thread,
}
@@ -35,7 +33,6 @@ impl ContextKind {
ContextKind::File => IconName::File,
ContextKind::Directory => IconName::Folder,
ContextKind::Symbol => IconName::Code,
ContextKind::Excerpt => IconName::Code,
ContextKind::FetchedUrl => IconName::Globe,
ContextKind::Thread => IconName::MessageBubbles,
}
@@ -49,7 +46,6 @@ pub enum AssistantContext {
Symbol(SymbolContext),
FetchedUrl(FetchedUrlContext),
Thread(ThreadContext),
Excerpt(ExcerptContext),
}
impl AssistantContext {
@@ -60,7 +56,6 @@ impl AssistantContext {
Self::Symbol(symbol) => symbol.id,
Self::FetchedUrl(url) => url.id,
Self::Thread(thread) => thread.id,
Self::Excerpt(excerpt) => excerpt.id,
}
}
}
@@ -160,14 +155,6 @@ pub struct ContextSymbolId {
pub range: Range<Anchor>,
}
#[derive(Debug, Clone)]
pub struct ExcerptContext {
pub id: ContextId,
pub range: Range<Anchor>,
pub line_range: Range<Point>,
pub context_buffer: ContextBuffer,
}
/// Formats a collection of contexts into a string representation
pub fn format_context_as_string<'a>(
contexts: impl Iterator<Item = &'a AssistantContext>,
@@ -176,7 +163,6 @@ pub fn format_context_as_string<'a>(
let mut file_context = Vec::new();
let mut directory_context = Vec::new();
let mut symbol_context = Vec::new();
let mut excerpt_context = Vec::new();
let mut fetch_context = Vec::new();
let mut thread_context = Vec::new();
@@ -185,7 +171,6 @@ pub fn format_context_as_string<'a>(
AssistantContext::File(context) => file_context.push(context),
AssistantContext::Directory(context) => directory_context.push(context),
AssistantContext::Symbol(context) => symbol_context.push(context),
AssistantContext::Excerpt(context) => excerpt_context.push(context),
AssistantContext::FetchedUrl(context) => fetch_context.push(context),
AssistantContext::Thread(context) => thread_context.push(context),
}
@@ -194,7 +179,6 @@ pub fn format_context_as_string<'a>(
if file_context.is_empty()
&& directory_context.is_empty()
&& symbol_context.is_empty()
&& excerpt_context.is_empty()
&& fetch_context.is_empty()
&& thread_context.is_empty()
{
@@ -232,15 +216,6 @@ pub fn format_context_as_string<'a>(
result.push_str("</symbols>\n");
}
if !excerpt_context.is_empty() {
result.push_str("<excerpts>\n");
for context in excerpt_context {
result.push_str(&context.context_buffer.text);
result.push('\n');
}
result.push_str("</excerpts>\n");
}
if !fetch_context.is_empty() {
result.push_str("<fetched_urls>\n");
for context in &fetch_context {

View File

@@ -8,7 +8,6 @@ use std::sync::atomic::AtomicBool;
use anyhow::Result;
use editor::{CompletionProvider, Editor, ExcerptId};
use file_icons::FileIcons;
use fuzzy::{StringMatch, StringMatchCandidate};
use gpui::{App, Entity, Task, WeakEntity};
use http_client::HttpClientWithUrl;
use language::{Buffer, CodeLabel, HighlightId};
@@ -38,24 +37,7 @@ pub(crate) enum Match {
File(FileMatch),
Thread(ThreadMatch),
Fetch(SharedString),
Mode(ModeMatch),
}
pub struct ModeMatch {
mat: Option<StringMatch>,
mode: ContextPickerMode,
}
impl Match {
pub fn score(&self) -> f64 {
match self {
Match::File(file) => file.mat.score,
Match::Mode(mode) => mode.mat.as_ref().map(|mat| mat.score).unwrap_or(1.),
Match::Thread(_) => 1.,
Match::Symbol(_) => 1.,
Match::Fetch(_) => 1.,
}
}
Mode(ContextPickerMode),
}
fn search(
@@ -144,54 +126,19 @@ fn search(
matches.extend(
supported_context_picker_modes(&thread_store)
.into_iter()
.map(|mode| Match::Mode(ModeMatch { mode, mat: None })),
.map(Match::Mode),
);
Task::ready(matches)
} else {
let executor = cx.background_executor().clone();
let search_files_task =
search_files(query.clone(), cancellation_flag.clone(), &workspace, cx);
let modes = supported_context_picker_modes(&thread_store);
let mode_candidates = modes
.iter()
.enumerate()
.map(|(ix, mode)| StringMatchCandidate::new(ix, mode.mention_prefix()))
.collect::<Vec<_>>();
cx.background_spawn(async move {
let mut matches = search_files_task
search_files_task
.await
.into_iter()
.map(Match::File)
.collect::<Vec<_>>();
let mode_matches = fuzzy::match_strings(
&mode_candidates,
&query,
false,
100,
&Arc::new(AtomicBool::default()),
executor,
)
.await;
matches.extend(mode_matches.into_iter().map(|mat| {
Match::Mode(ModeMatch {
mode: modes[mat.candidate_id],
mat: Some(mat),
})
}));
matches.sort_by(|a, b| {
b.score()
.partial_cmp(&a.score())
.unwrap_or(std::cmp::Ordering::Equal)
});
matches
.collect()
})
}
}
@@ -601,7 +548,7 @@ impl CompletionProvider for ContextPickerCompletionProvider {
context_store.clone(),
http_client.clone(),
)),
Match::Mode(ModeMatch { mode, .. }) => {
Match::Mode(mode) => {
Some(Self::completion_for_mode(source_range.clone(), mode))
}
})

View File

@@ -9,14 +9,14 @@ use futures::{self, Future, FutureExt, future};
use gpui::{App, AppContext as _, Context, Entity, SharedString, Task, WeakEntity};
use language::{Buffer, File};
use project::{Project, ProjectItem, ProjectPath, Worktree};
use rope::{Point, Rope};
use rope::Rope;
use text::{Anchor, BufferId, OffsetRangeExt};
use util::{ResultExt as _, maybe};
use crate::ThreadStore;
use crate::context::{
AssistantContext, ContextBuffer, ContextId, ContextSymbol, ContextSymbolId, DirectoryContext,
ExcerptContext, FetchedUrlContext, FileContext, SymbolContext, ThreadContext,
FetchedUrlContext, FileContext, SymbolContext, ThreadContext,
};
use crate::context_strip::SuggestedContext;
use crate::thread::{Thread, ThreadId};
@@ -110,7 +110,7 @@ impl ContextStore {
}
let (buffer_info, text_task) =
this.update(cx, |_, cx| collect_buffer_info_and_text(buffer, cx))??;
this.update(cx, |_, cx| collect_buffer_info_and_text(buffer, None, cx))??;
let text = text_task.await;
@@ -129,7 +129,7 @@ impl ContextStore {
) -> Task<Result<()>> {
cx.spawn(async move |this, cx| {
let (buffer_info, text_task) =
this.update(cx, |_, cx| collect_buffer_info_and_text(buffer, cx))??;
this.update(cx, |_, cx| collect_buffer_info_and_text(buffer, None, cx))??;
let text = text_task.await;
@@ -206,7 +206,7 @@ impl ContextStore {
// Skip all binary files and other non-UTF8 files
for buffer in buffers.into_iter().flatten() {
if let Some((buffer_info, text_task)) =
collect_buffer_info_and_text(buffer, cx).log_err()
collect_buffer_info_and_text(buffer, None, cx).log_err()
{
buffer_infos.push(buffer_info);
text_tasks.push(text_task);
@@ -290,14 +290,11 @@ impl ContextStore {
}
}
let (buffer_info, collect_content_task) = match collect_buffer_info_and_text_for_range(
buffer,
symbol_enclosing_range.clone(),
cx,
) {
Ok((_, buffer_info, collect_context_task)) => (buffer_info, collect_context_task),
Err(err) => return Task::ready(Err(err)),
};
let (buffer_info, collect_content_task) =
match collect_buffer_info_and_text(buffer, Some(symbol_enclosing_range.clone()), cx) {
Ok((buffer_info, collect_context_task)) => (buffer_info, collect_context_task),
Err(err) => return Task::ready(Err(err)),
};
cx.spawn(async move |this, cx| {
let content = collect_content_task.await;
@@ -419,49 +416,6 @@ impl ContextStore {
cx.notify();
}
pub fn add_excerpt(
&mut self,
range: Range<Anchor>,
buffer: Entity<Buffer>,
cx: &mut Context<ContextStore>,
) -> Task<Result<()>> {
cx.spawn(async move |this, cx| {
let (line_range, buffer_info, text_task) = this.update(cx, |_, cx| {
collect_buffer_info_and_text_for_range(buffer, range.clone(), cx)
})??;
let text = text_task.await;
this.update(cx, |this, cx| {
this.insert_excerpt(
make_context_buffer(buffer_info, text),
range,
line_range,
cx,
)
})?;
anyhow::Ok(())
})
}
fn insert_excerpt(
&mut self,
context_buffer: ContextBuffer,
range: Range<Anchor>,
line_range: Range<Point>,
cx: &mut Context<Self>,
) {
let id = self.next_context_id.post_inc();
self.context.push(AssistantContext::Excerpt(ExcerptContext {
id,
range,
line_range,
context_buffer,
}));
cx.notify();
}
pub fn accept_suggested_context(
&mut self,
suggested: &SuggestedContext,
@@ -511,7 +465,6 @@ impl ContextStore {
self.symbol_buffers.remove(&symbol.context_symbol.id);
self.symbols.retain(|_, context_id| *context_id != id);
}
AssistantContext::Excerpt(_) => {}
AssistantContext::FetchedUrl(_) => {
self.fetched_urls.retain(|_, context_id| *context_id != id);
}
@@ -639,7 +592,6 @@ impl ContextStore {
}
AssistantContext::Directory(_)
| AssistantContext::Symbol(_)
| AssistantContext::Excerpt(_)
| AssistantContext::FetchedUrl(_)
| AssistantContext::Thread(_) => None,
})
@@ -691,78 +643,41 @@ fn make_context_symbol(
}
}
fn collect_buffer_info_and_text_for_range(
buffer: Entity<Buffer>,
range: Range<Anchor>,
cx: &App,
) -> Result<(Range<Point>, BufferInfo, Task<SharedString>)> {
let content = buffer
.read(cx)
.text_for_range(range.clone())
.collect::<Rope>();
let line_range = range.to_point(&buffer.read(cx).snapshot());
let buffer_info = collect_buffer_info(buffer, cx)?;
let full_path = buffer_info.file.full_path(cx);
let text_task = cx.background_spawn({
let line_range = line_range.clone();
async move { to_fenced_codeblock(&full_path, content, Some(line_range)) }
});
Ok((line_range, buffer_info, text_task))
}
fn collect_buffer_info_and_text(
buffer: Entity<Buffer>,
range: Option<Range<Anchor>>,
cx: &App,
) -> Result<(BufferInfo, Task<SharedString>)> {
let content = buffer.read(cx).as_rope().clone();
let buffer_info = collect_buffer_info(buffer, cx)?;
let full_path = buffer_info.file.full_path(cx);
let text_task =
cx.background_spawn(async move { to_fenced_codeblock(&full_path, content, None) });
Ok((buffer_info, text_task))
}
fn collect_buffer_info(buffer: Entity<Buffer>, cx: &App) -> Result<BufferInfo> {
let buffer_ref = buffer.read(cx);
let file = buffer_ref.file().context("file context must have a path")?;
// Important to collect version at the same time as content so that staleness logic is correct.
let version = buffer_ref.version();
let content = if let Some(range) = range {
buffer_ref.text_for_range(range).collect::<Rope>()
} else {
buffer_ref.as_rope().clone()
};
Ok(BufferInfo {
let buffer_info = BufferInfo {
buffer,
id: buffer_ref.remote_id(),
file: file.clone(),
version,
})
};
let full_path = file.full_path(cx);
let text_task = cx.background_spawn(async move { to_fenced_codeblock(&full_path, content) });
Ok((buffer_info, text_task))
}
fn to_fenced_codeblock(
path: &Path,
content: Rope,
line_range: Option<Range<Point>>,
) -> SharedString {
let line_range_text = line_range.map(|range| {
if range.start.row == range.end.row {
format!(":{}", range.start.row + 1)
} else {
format!(":{}-{}", range.start.row + 1, range.end.row + 1)
}
});
fn to_fenced_codeblock(path: &Path, content: Rope) -> SharedString {
let path_extension = path.extension().and_then(|ext| ext.to_str());
let path_string = path.to_string_lossy();
let capacity = 3
+ path_extension.map_or(0, |extension| extension.len() + 1)
+ path_string.len()
+ line_range_text.as_ref().map_or(0, |text| text.len())
+ 1
+ content.len()
+ 5;
@@ -776,10 +691,6 @@ fn to_fenced_codeblock(
}
buffer.push_str(&path_string);
if let Some(line_range_text) = line_range_text {
buffer.push_str(&line_range_text);
}
buffer.push('\n');
for chunk in content.chunks() {
buffer.push_str(&chunk);
@@ -858,14 +769,6 @@ pub fn refresh_context_store_text(
return refresh_symbol_text(context_store, symbol_context, cx);
}
}
AssistantContext::Excerpt(excerpt_context) => {
if changed_buffers.is_empty()
|| changed_buffers.contains(&excerpt_context.context_buffer.buffer)
{
let context_store = context_store.clone();
return refresh_excerpt_text(context_store, excerpt_context, cx);
}
}
AssistantContext::Thread(thread_context) => {
if changed_buffers.is_empty() {
let context_store = context_store.clone();
@@ -977,34 +880,6 @@ fn refresh_symbol_text(
}
}
fn refresh_excerpt_text(
context_store: Entity<ContextStore>,
excerpt_context: &ExcerptContext,
cx: &App,
) -> Option<Task<()>> {
let id = excerpt_context.id;
let range = excerpt_context.range.clone();
let task = refresh_context_excerpt(&excerpt_context.context_buffer, range.clone(), cx);
if let Some(task) = task {
Some(cx.spawn(async move |cx| {
let (line_range, context_buffer) = task.await;
context_store
.update(cx, |context_store, _| {
let new_excerpt_context = ExcerptContext {
id,
range,
line_range,
context_buffer,
};
context_store.replace_context(AssistantContext::Excerpt(new_excerpt_context));
})
.ok();
}))
} else {
None
}
}
fn refresh_thread_text(
context_store: Entity<ContextStore>,
thread_context: &ThreadContext,
@@ -1033,29 +908,13 @@ fn refresh_context_buffer(
let buffer = context_buffer.buffer.read(cx);
if buffer.version.changed_since(&context_buffer.version) {
let (buffer_info, text_task) =
collect_buffer_info_and_text(context_buffer.buffer.clone(), cx).log_err()?;
collect_buffer_info_and_text(context_buffer.buffer.clone(), None, cx).log_err()?;
Some(text_task.map(move |text| make_context_buffer(buffer_info, text)))
} else {
None
}
}
fn refresh_context_excerpt(
context_buffer: &ContextBuffer,
range: Range<Anchor>,
cx: &App,
) -> Option<impl Future<Output = (Range<Point>, ContextBuffer)> + use<>> {
let buffer = context_buffer.buffer.read(cx);
if buffer.version.changed_since(&context_buffer.version) {
let (line_range, buffer_info, text_task) =
collect_buffer_info_and_text_for_range(context_buffer.buffer.clone(), range, cx)
.log_err()?;
Some(text_task.map(move |text| (line_range, make_context_buffer(buffer_info, text))))
} else {
None
}
}
fn refresh_context_symbol(
context_symbol: &ContextSymbol,
cx: &App,
@@ -1063,9 +922,9 @@ fn refresh_context_symbol(
let buffer = context_symbol.buffer.read(cx);
let project_path = buffer.project_path(cx)?;
if buffer.version.changed_since(&context_symbol.buffer_version) {
let (_, buffer_info, text_task) = collect_buffer_info_and_text_for_range(
let (buffer_info, text_task) = collect_buffer_info_and_text(
context_symbol.buffer.clone(),
context_symbol.enclosing_range.clone(),
Some(context_symbol.enclosing_range.clone()),
cx,
)
.log_err()?;

View File

@@ -2,23 +2,22 @@ use std::collections::BTreeMap;
use std::sync::Arc;
use crate::assistant_model_selector::ModelType;
use crate::context::format_context_as_string;
use crate::tool_compatibility::{IncompatibleToolsState, IncompatibleToolsTooltip};
use buffer_diff::BufferDiff;
use collections::HashSet;
use editor::actions::MoveUp;
use editor::{
ContextMenuOptions, ContextMenuPlacement, Editor, EditorElement, EditorEvent, EditorMode,
EditorStyle, MultiBuffer,
ContextMenuOptions, ContextMenuPlacement, Editor, EditorElement, EditorMode, EditorStyle,
MultiBuffer,
};
use file_icons::FileIcons;
use fs::Fs;
use gpui::{
Animation, AnimationExt, App, Entity, EventEmitter, Focusable, Subscription, Task, TextStyle,
WeakEntity, linear_color_stop, linear_gradient, point, pulsating_between,
Animation, AnimationExt, App, Entity, Focusable, Subscription, TextStyle, WeakEntity,
linear_color_stop, linear_gradient, point, pulsating_between,
};
use language::{Buffer, Language};
use language_model::{ConfiguredModel, LanguageModelRegistry, LanguageModelRequestMessage};
use language_model::{ConfiguredModel, LanguageModelRegistry};
use language_model_selector::ToggleModelSelector;
use multi_buffer;
use project::Project;
@@ -56,8 +55,6 @@ pub struct MessageEditor {
edits_expanded: bool,
editor_is_expanded: bool,
waiting_for_summaries_to_send: bool,
last_estimated_token_count: Option<usize>,
update_token_count_task: Option<Task<anyhow::Result<()>>>,
_subscriptions: Vec<Subscription>,
}
@@ -132,18 +129,8 @@ impl MessageEditor {
let incompatible_tools =
cx.new(|cx| IncompatibleToolsState::new(thread.read(cx).tools().clone(), cx));
let subscriptions = vec![
cx.subscribe_in(&context_strip, window, Self::handle_context_strip_event),
cx.subscribe(&editor, |this, _, event, cx| match event {
EditorEvent::BufferEdited => {
this.message_or_context_changed(true, cx);
}
_ => {}
}),
cx.observe(&context_store, |this, _, cx| {
this.message_or_context_changed(false, cx);
}),
];
let subscriptions =
vec![cx.subscribe_in(&context_strip, window, Self::handle_context_strip_event)];
Self {
editor: editor.clone(),
@@ -169,8 +156,6 @@ impl MessageEditor {
waiting_for_summaries_to_send: false,
profile_selector: cx
.new(|cx| ProfileSelector::new(fs, thread_store, editor.focus_handle(cx), cx)),
last_estimated_token_count: None,
update_token_count_task: None,
_subscriptions: subscriptions,
}
}
@@ -271,9 +256,6 @@ impl MessageEditor {
text
});
self.last_estimated_token_count.take();
cx.emit(MessageEditorEvent::EstimatedTokenCount);
let refresh_task =
refresh_context_store_text(self.context_store.clone(), &HashSet::default(), cx);
@@ -955,80 +937,6 @@ impl MessageEditor {
.label_size(LabelSize::Small),
)
}
pub fn last_estimated_token_count(&self) -> Option<usize> {
self.last_estimated_token_count
}
pub fn is_waiting_to_update_token_count(&self) -> bool {
self.update_token_count_task.is_some()
}
fn message_or_context_changed(&mut self, debounce: bool, cx: &mut Context<Self>) {
cx.emit(MessageEditorEvent::Changed);
self.update_token_count_task.take();
let Some(default_model) = LanguageModelRegistry::read_global(cx).default_model() else {
self.last_estimated_token_count.take();
return;
};
let context_store = self.context_store.clone();
let editor = self.editor.clone();
let thread = self.thread.clone();
self.update_token_count_task = Some(cx.spawn(async move |this, cx| {
if debounce {
cx.background_executor()
.timer(Duration::from_millis(200))
.await;
}
let token_count = if let Some(task) = cx.update(|cx| {
let context = context_store.read(cx).context().iter();
let new_context = thread.read(cx).filter_new_context(context);
let context_text =
format_context_as_string(new_context, cx).unwrap_or(String::new());
let message_text = editor.read(cx).text(cx);
let content = context_text + &message_text;
if content.is_empty() {
return None;
}
let request = language_model::LanguageModelRequest {
messages: vec![LanguageModelRequestMessage {
role: language_model::Role::User,
content: vec![content.into()],
cache: false,
}],
tools: vec![],
stop: vec![],
temperature: None,
};
Some(default_model.model.count_tokens(request, cx))
})? {
task.await?
} else {
0
};
this.update(cx, |this, cx| {
this.last_estimated_token_count = Some(token_count);
cx.emit(MessageEditorEvent::EstimatedTokenCount);
this.update_token_count_task.take();
})
}));
}
}
impl EventEmitter<MessageEditorEvent> for MessageEditor {}
pub enum MessageEditorEvent {
EstimatedTokenCount,
Changed,
}
impl Focusable for MessageEditor {
@@ -1041,7 +949,6 @@ impl Render for MessageEditor {
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let thread = self.thread.read(cx);
let total_token_usage = thread.total_token_usage(cx);
let token_usage_ratio = total_token_usage.ratio();
let action_log = self.thread.read(cx).action_log();
let changed_buffers = action_log.read(cx).changed_buffers(cx);
@@ -1090,8 +997,15 @@ impl Render for MessageEditor {
parent.child(self.render_changed_buffers(&changed_buffers, window, cx))
})
.child(self.render_editor(font_size, line_height, window, cx))
.when(token_usage_ratio != TokenUsageRatio::Normal, |parent| {
parent.child(self.render_token_limit_callout(line_height, token_usage_ratio, cx))
})
.when(
total_token_usage.ratio != TokenUsageRatio::Normal,
|parent| {
parent.child(self.render_token_limit_callout(
line_height,
total_token_usage.ratio,
cx,
))
},
)
}
}

View File

@@ -6,7 +6,7 @@ use std::time::Instant;
use anyhow::{Context as _, Result, anyhow};
use assistant_settings::AssistantSettings;
use assistant_tool::{ActionLog, AnyToolCard, Tool, ToolWorkingSet};
use assistant_tool::{ActionLog, Tool, ToolWorkingSet};
use chrono::{DateTime, Utc};
use collections::{BTreeMap, HashMap};
use feature_flags::{self, FeatureFlagAppExt};
@@ -18,13 +18,12 @@ use language_model::{
ConfiguredModel, LanguageModel, LanguageModelCompletionEvent, LanguageModelId,
LanguageModelKnownError, LanguageModelRegistry, LanguageModelRequest,
LanguageModelRequestMessage, LanguageModelRequestTool, LanguageModelToolResult,
LanguageModelToolUseId, MaxMonthlySpendReachedError, MessageContent,
ModelRequestLimitReachedError, PaymentRequiredError, Role, StopReason, TokenUsage,
LanguageModelToolUseId, MaxMonthlySpendReachedError, MessageContent, PaymentRequiredError,
Role, StopReason, TokenUsage,
};
use project::Project;
use project::git_store::{GitStore, GitStoreCheckpoint, RepositoryState};
use prompt_store::PromptBuilder;
use proto::Plan;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use settings::Settings;
@@ -227,33 +226,7 @@ pub enum DetailedSummaryState {
pub struct TotalTokenUsage {
pub total: usize,
pub max: usize,
}
impl TotalTokenUsage {
pub fn ratio(&self) -> TokenUsageRatio {
#[cfg(debug_assertions)]
let warning_threshold: f32 = std::env::var("ZED_THREAD_WARNING_THRESHOLD")
.unwrap_or("0.8".to_string())
.parse()
.unwrap();
#[cfg(not(debug_assertions))]
let warning_threshold: f32 = 0.8;
if self.total >= self.max {
TokenUsageRatio::Exceeded
} else if self.total as f32 / self.max as f32 >= warning_threshold {
TokenUsageRatio::Warning
} else {
TokenUsageRatio::Normal
}
}
pub fn add(&self, tokens: usize) -> TotalTokenUsage {
TotalTokenUsage {
total: self.total + tokens,
max: self.max,
}
}
pub ratio: TokenUsageRatio,
}
#[derive(Debug, Default, PartialEq, Eq)]
@@ -287,7 +260,6 @@ pub struct Thread {
last_restore_checkpoint: Option<LastRestoreCheckpoint>,
pending_checkpoint: Option<ThreadCheckpoint>,
initial_project_snapshot: Shared<Task<Option<Arc<ProjectSnapshot>>>>,
request_token_usage: Vec<TokenUsage>,
cumulative_token_usage: TokenUsage,
exceeded_window_error: Option<ExceededWindowError>,
feedback: Option<ThreadFeedback>,
@@ -338,7 +310,6 @@ impl Thread {
.spawn(async move { Some(project_snapshot.await) })
.shared()
},
request_token_usage: Vec::new(),
cumulative_token_usage: TokenUsage::default(),
exceeded_window_error: None,
feedback: None,
@@ -406,7 +377,6 @@ impl Thread {
tool_use,
action_log: cx.new(|_| ActionLog::new(project)),
initial_project_snapshot: Task::ready(serialized.initial_project_snapshot).shared(),
request_token_usage: serialized.request_token_usage,
cumulative_token_usage: serialized.cumulative_token_usage,
exceeded_window_error: None,
feedback: None,
@@ -660,30 +630,10 @@ impl Thread {
self.tool_use.tool_result(id)
}
pub fn output_for_tool(&self, id: &LanguageModelToolUseId) -> Option<&Arc<str>> {
Some(&self.tool_use.tool_result(id)?.content)
}
pub fn card_for_tool(&self, id: &LanguageModelToolUseId) -> Option<AnyToolCard> {
self.tool_use.tool_result_card(id).cloned()
}
pub fn message_has_tool_results(&self, message_id: MessageId) -> bool {
self.tool_use.message_has_tool_results(message_id)
}
/// Filter out contexts that have already been included in previous messages
pub fn filter_new_context<'a>(
&self,
context: impl Iterator<Item = &'a AssistantContext>,
) -> impl Iterator<Item = &'a AssistantContext> {
context.filter(|ctx| self.is_context_new(ctx))
}
fn is_context_new(&self, context: &AssistantContext) -> bool {
!self.context.contains_key(&context.id())
}
pub fn insert_user_message(
&mut self,
text: impl Into<String>,
@@ -695,9 +645,10 @@ impl Thread {
let message_id = self.insert_message(Role::User, vec![MessageSegment::Text(text)], cx);
// Filter out contexts that have already been included in previous messages
let new_context: Vec<_> = context
.into_iter()
.filter(|ctx| self.is_context_new(ctx))
.filter(|ctx| !self.context.contains_key(&ctx.id()))
.collect();
if !new_context.is_empty() {
@@ -725,12 +676,6 @@ impl Thread {
cx,
);
}
AssistantContext::Excerpt(excerpt_context) => {
log.buffer_added_as_context(
excerpt_context.context_buffer.buffer.clone(),
cx,
);
}
AssistantContext::FetchedUrl(_) | AssistantContext::Thread(_) => {}
}
}
@@ -883,7 +828,6 @@ impl Thread {
.collect(),
initial_project_snapshot,
cumulative_token_usage: this.cumulative_token_usage,
request_token_usage: this.request_token_usage.clone(),
detailed_summary_state: this.detailed_summary_state.clone(),
exceeded_window_error: this.exceeded_window_error.clone(),
})
@@ -1069,6 +1013,7 @@ impl Thread {
cx: &mut Context<Self>,
) {
let pending_completion_id = post_inc(&mut self.completion_count);
let task = cx.spawn(async move |thread, cx| {
let stream = model.stream_completion(request, &cx);
let initial_token_usage =
@@ -1094,7 +1039,6 @@ impl Thread {
stop_reason = reason;
}
LanguageModelCompletionEvent::UsageUpdate(token_usage) => {
thread.update_token_usage_at_last_message(token_usage);
thread.cumulative_token_usage = thread.cumulative_token_usage
+ token_usage
- current_token_usage;
@@ -1206,12 +1150,6 @@ impl Thread {
cx.emit(ThreadEvent::ShowError(
ThreadError::MaxMonthlySpendReached,
));
} else if let Some(error) =
error.downcast_ref::<ModelRequestLimitReachedError>()
{
cx.emit(ThreadEvent::ShowError(
ThreadError::ModelRequestLimitReached { plan: error.plan },
));
} else if let Some(known_error) =
error.downcast_ref::<LanguageModelKnownError>()
{
@@ -1481,12 +1419,6 @@ impl Thread {
)
};
// Store the card separately if it exists
if let Some(card) = tool_result.card.clone() {
self.tool_use
.insert_tool_result_card(tool_use_id.clone(), card);
}
cx.spawn({
async move |thread: WeakEntity<Thread>, cx| {
let output = tool_result.output.await;
@@ -1936,35 +1868,6 @@ impl Thread {
self.cumulative_token_usage
}
pub fn token_usage_up_to_message(&self, message_id: MessageId, cx: &App) -> TotalTokenUsage {
let Some(model) = LanguageModelRegistry::read_global(cx).default_model() else {
return TotalTokenUsage::default();
};
let max = model.model.max_token_count();
let index = self
.messages
.iter()
.position(|msg| msg.id == message_id)
.unwrap_or(0);
if index == 0 {
return TotalTokenUsage { total: 0, max };
}
let token_usage = &self
.request_token_usage
.get(index - 1)
.cloned()
.unwrap_or_default();
TotalTokenUsage {
total: token_usage.total_tokens() as usize,
max,
}
}
pub fn total_token_usage(&self, cx: &App) -> TotalTokenUsage {
let model_registry = LanguageModelRegistry::read_global(cx);
let Some(model) = model_registry.default_model() else {
@@ -1978,33 +1881,30 @@ impl Thread {
return TotalTokenUsage {
total: exceeded_error.token_count,
max,
ratio: TokenUsageRatio::Exceeded,
};
}
}
let total = self
.token_usage_at_last_message()
.unwrap_or_default()
.total_tokens() as usize;
#[cfg(debug_assertions)]
let warning_threshold: f32 = std::env::var("ZED_THREAD_WARNING_THRESHOLD")
.unwrap_or("0.8".to_string())
.parse()
.unwrap();
#[cfg(not(debug_assertions))]
let warning_threshold: f32 = 0.8;
TotalTokenUsage { total, max }
}
let total = self.cumulative_token_usage.total_tokens() as usize;
fn token_usage_at_last_message(&self) -> Option<TokenUsage> {
self.request_token_usage
.get(self.messages.len().saturating_sub(1))
.or_else(|| self.request_token_usage.last())
.cloned()
}
let ratio = if total >= max {
TokenUsageRatio::Exceeded
} else if total as f32 / max as f32 >= warning_threshold {
TokenUsageRatio::Warning
} else {
TokenUsageRatio::Normal
};
fn update_token_usage_at_last_message(&mut self, token_usage: TokenUsage) {
let placeholder = self.token_usage_at_last_message().unwrap_or_default();
self.request_token_usage
.resize(self.messages.len(), placeholder);
if let Some(last) = self.request_token_usage.last_mut() {
*last = token_usage;
}
TotalTokenUsage { total, max, ratio }
}
pub fn deny_tool_use(
@@ -2029,8 +1929,6 @@ pub enum ThreadError {
PaymentRequired,
#[error("Max monthly spend reached")]
MaxMonthlySpendReached,
#[error("Model request limit reached")]
ModelRequestLimitReached { plan: Plan },
#[error("Message {header}: {message}")]
Message {
header: SharedString,

View File

@@ -509,8 +509,6 @@ pub struct SerializedThread {
#[serde(default)]
pub cumulative_token_usage: TokenUsage,
#[serde(default)]
pub request_token_usage: Vec<TokenUsage>,
#[serde(default)]
pub detailed_summary_state: DetailedSummaryState,
#[serde(default)]
pub exceeded_window_error: Option<ExceededWindowError>,
@@ -599,7 +597,6 @@ impl LegacySerializedThread {
messages: self.messages.into_iter().map(|msg| msg.upgrade()).collect(),
initial_project_snapshot: self.initial_project_snapshot,
cumulative_token_usage: TokenUsage::default(),
request_token_usage: Vec::new(),
detailed_summary_state: DetailedSummaryState::default(),
exceeded_window_error: None,
}

View File

@@ -1,6 +1,6 @@
use std::sync::Arc;
use assistant_tool::{Tool, ToolSource, ToolWorkingSet, ToolWorkingSetEvent};
use assistant_tool::{Tool, ToolWorkingSet, ToolWorkingSetEvent};
use collections::HashMap;
use gpui::{App, Context, Entity, IntoElement, Render, Subscription, Window};
use language_model::{LanguageModel, LanguageModelToolSchemaFormat};
@@ -73,12 +73,7 @@ impl Render for IncompatibleToolsTooltip {
.children(
self.incompatible_tools
.iter()
.map(|tool| h_flex().gap_4().child(Label::new(tool.name()).size(LabelSize::Small)).map(|parent|
match tool.source() {
ToolSource::Native => parent,
ToolSource::ContextServer { id } => parent.child(Label::new(id).size(LabelSize::Small).color(Color::Muted)),
}
)),
.map(|tool| Label::new(tool.name()).size(LabelSize::Small).buffer_font(cx)),
),
)
.child(Label::new("What To Do Instead").size(LabelSize::Small))

View File

@@ -1,7 +1,7 @@
use std::sync::Arc;
use anyhow::Result;
use assistant_tool::{AnyToolCard, Tool, ToolUseStatus, ToolWorkingSet};
use assistant_tool::{Tool, ToolWorkingSet};
use collections::HashMap;
use futures::FutureExt as _;
use futures::future::Shared;
@@ -27,7 +27,26 @@ pub struct ToolUse {
pub needs_confirmation: bool,
}
pub const USING_TOOL_MARKER: &str = "<using_tool>";
#[derive(Debug, Clone)]
pub enum ToolUseStatus {
NeedsConfirmation,
Pending,
Running,
Finished(SharedString),
Error(SharedString),
}
impl ToolUseStatus {
pub fn text(&self) -> SharedString {
match self {
ToolUseStatus::NeedsConfirmation => "".into(),
ToolUseStatus::Pending => "".into(),
ToolUseStatus::Running => "".into(),
ToolUseStatus::Finished(out) => out.clone(),
ToolUseStatus::Error(out) => out.clone(),
}
}
}
pub struct ToolUseState {
tools: Entity<ToolWorkingSet>,
@@ -35,9 +54,10 @@ pub struct ToolUseState {
tool_uses_by_user_message: HashMap<MessageId, Vec<LanguageModelToolUseId>>,
tool_results: HashMap<LanguageModelToolUseId, LanguageModelToolResult>,
pending_tool_uses_by_id: HashMap<LanguageModelToolUseId, PendingToolUse>,
tool_result_cards: HashMap<LanguageModelToolUseId, AnyToolCard>,
}
pub const USING_TOOL_MARKER: &str = "<using_tool>";
impl ToolUseState {
pub fn new(tools: Entity<ToolWorkingSet>) -> Self {
Self {
@@ -46,7 +66,6 @@ impl ToolUseState {
tool_uses_by_user_message: HashMap::default(),
tool_results: HashMap::default(),
pending_tool_uses_by_id: HashMap::default(),
tool_result_cards: HashMap::default(),
}
}
@@ -238,18 +257,6 @@ impl ToolUseState {
self.tool_results.get(tool_use_id)
}
pub fn tool_result_card(&self, tool_use_id: &LanguageModelToolUseId) -> Option<&AnyToolCard> {
self.tool_result_cards.get(tool_use_id)
}
pub fn insert_tool_result_card(
&mut self,
tool_use_id: LanguageModelToolUseId,
card: AnyToolCard,
) {
self.tool_result_cards.insert(tool_use_id, card);
}
pub fn request_tool_use(
&mut self,
assistant_message_id: MessageId,

View File

@@ -191,12 +191,15 @@ impl RenderOnce for ContextPill {
ContextPill::Suggested {
name,
icon_path: _,
kind: _,
kind,
focused,
on_click,
} => base_pill
.cursor_pointer()
.pr_1()
.when(*focused, |this| {
this.bg(color.element_background.opacity(0.5))
})
.border_dashed()
.border_color(if *focused {
color.border_focused
@@ -204,17 +207,30 @@ impl RenderOnce for ContextPill {
color.border
})
.hover(|style| style.bg(color.element_hover.opacity(0.5)))
.when(*focused, |this| {
this.bg(color.element_background.opacity(0.5))
})
.child(
div().max_w_64().child(
div().px_0p5().max_w_64().child(
Label::new(name.clone())
.size(LabelSize::Small)
.color(Color::Muted)
.truncate(),
),
)
.child(
Label::new(match kind {
ContextKind::File => "Active Tab",
ContextKind::Thread
| ContextKind::Directory
| ContextKind::FetchedUrl
| ContextKind::Symbol => "Active",
})
.size(LabelSize::XSmall)
.color(Color::Muted),
)
.child(
Icon::new(IconName::Plus)
.size(IconSize::XSmall)
.into_any_element(),
)
.tooltip(|window, cx| {
Tooltip::with_meta("Suggested Context", None, "Click to add it", window, cx)
})
@@ -299,39 +315,6 @@ impl AddedContext {
summarizing: false,
},
AssistantContext::Excerpt(excerpt_context) => {
let full_path = excerpt_context.context_buffer.file.full_path(cx);
let mut full_path_string = full_path.to_string_lossy().into_owned();
let mut name = full_path
.file_name()
.map(|n| n.to_string_lossy().into_owned())
.unwrap_or_else(|| full_path_string.clone());
let line_range_text = format!(
" ({}-{})",
excerpt_context.line_range.start.row + 1,
excerpt_context.line_range.end.row + 1
);
full_path_string.push_str(&line_range_text);
name.push_str(&line_range_text);
let parent = full_path
.parent()
.and_then(|p| p.file_name())
.map(|n| n.to_string_lossy().into_owned().into());
AddedContext {
id: excerpt_context.id,
kind: ContextKind::File, // Use File icon for excerpts
name: name.into(),
parent,
tooltip: Some(full_path_string.into()),
icon_path: FileIcons::get_icon(&full_path, cx),
summarizing: false,
}
}
AssistantContext::FetchedUrl(fetched_url_context) => AddedContext {
id: fetched_url_context.id,
kind: ContextKind::FetchedUrl,

View File

@@ -18,4 +18,5 @@ gpui.workspace = true
smol.workspace = true
tempfile.workspace = true
util.workspace = true
which.workspace = true
workspace-hack.workspace = true

View File

@@ -72,8 +72,6 @@ impl AskPassSession {
let (askpass_opened_tx, askpass_opened_rx) = oneshot::channel::<()>();
let listener =
UnixListener::bind(&askpass_socket).context("failed to create askpass socket")?;
let zed_path = std::env::current_exe()
.context("Failed to figure out current executable path for use in askpass")?;
let (askpass_kill_master_tx, askpass_kill_master_rx) = oneshot::channel::<()>();
let mut kill_tx = Some(askpass_kill_master_tx);
@@ -112,10 +110,21 @@ impl AskPassSession {
drop(temp_dir)
});
anyhow::ensure!(
which::which("nc").is_ok(),
"Cannot find `nc` command (netcat), which is required to connect over SSH."
);
// Create an askpass script that communicates back to this process.
let askpass_script = format!(
"{shebang}\n{print_args} | {zed_exe} --askpass={askpass_socket} 2> /dev/null \n",
zed_exe = zed_path.display(),
"{shebang}\n{print_args} | {nc} -U {askpass_socket} 2> /dev/null \n",
// on macOS `brew install netcat` provides the GNU netcat implementation
// which does not support -U.
nc = if cfg!(target_os = "macos") {
"/usr/bin/nc"
} else {
"nc"
},
askpass_socket = askpass_socket.display(),
print_args = "printf '%s\\0' \"$@\"",
shebang = "#!/bin/sh",
@@ -161,51 +170,6 @@ impl AskPassSession {
}
}
/// The main function for when Zed is running in netcat mode for use in askpass.
/// Called from both the remote server binary and the zed binary in their respective main functions.
#[cfg(unix)]
pub fn main(socket: &str) {
use std::io::{self, Read, Write};
use std::os::unix::net::UnixStream;
use std::process::exit;
let mut stream = match UnixStream::connect(socket) {
Ok(stream) => stream,
Err(err) => {
eprintln!("Error connecting to socket {}: {}", socket, err);
exit(1);
}
};
let mut buffer = Vec::new();
if let Err(err) = io::stdin().read_to_end(&mut buffer) {
eprintln!("Error reading from stdin: {}", err);
exit(1);
}
if buffer.last() != Some(&b'\0') {
buffer.push(b'\0');
}
if let Err(err) = stream.write_all(&buffer) {
eprintln!("Error writing to socket: {}", err);
exit(1);
}
let mut response = Vec::new();
if let Err(err) = stream.read_to_end(&mut response) {
eprintln!("Error reading from socket: {}", err);
exit(1);
}
if let Err(err) = io::stdout().write_all(&response) {
eprintln!("Error writing to stdout: {}", err);
exit(1);
}
}
#[cfg(not(unix))]
pub fn main(_socket: &str) {}
#[cfg(not(unix))]
pub struct AskPassSession {
path: PathBuf,

View File

@@ -13,7 +13,7 @@ use assistant_context_editor::{
use assistant_settings::{AssistantDockPosition, AssistantSettings};
use assistant_slash_command::SlashCommandWorkingSet;
use client::{Client, Status, proto};
use editor::{Anchor, AnchorRangeExt as _, Editor, EditorEvent, MultiBuffer};
use editor::{Editor, EditorEvent};
use fs::Fs;
use gpui::{
Action, App, AsyncWindowContext, Entity, EventEmitter, ExternalPaths, FocusHandle, Focusable,
@@ -28,12 +28,9 @@ use language_model::{
use project::Project;
use prompt_library::{PromptLibrary, open_prompt_library};
use prompt_store::PromptBuilder;
use search::{BufferSearchBar, buffer_search::DivRegistrar};
use settings::{Settings, update_settings_file};
use smol::stream::StreamExt;
use std::ops::Range;
use std::{ops::ControlFlow, path::PathBuf, sync::Arc};
use terminal_view::{TerminalView, terminal_panel::TerminalPanel};
use ui::{ContextMenu, PopoverMenu, Tooltip, prelude::*};
@@ -1416,8 +1413,7 @@ impl AssistantPanelDelegate for ConcreteAssistantPanelDelegate {
fn quote_selection(
&self,
workspace: &mut Workspace,
selection_ranges: Vec<Range<Anchor>>,
buffer: Entity<MultiBuffer>,
creases: Vec<(String, String)>,
window: &mut Window,
cx: &mut Context<Workspace>,
) {
@@ -1429,12 +1425,6 @@ impl AssistantPanelDelegate for ConcreteAssistantPanelDelegate {
workspace.toggle_panel_focus::<AssistantPanel>(window, cx);
}
let snapshot = buffer.read(cx).snapshot(cx);
let selection_ranges = selection_ranges
.into_iter()
.map(|range| range.to_point(&snapshot))
.collect::<Vec<_>>();
panel.update(cx, |_, cx| {
// Wait to create a new context until the workspace is no longer
// being updated.
@@ -1443,9 +1433,7 @@ impl AssistantPanelDelegate for ConcreteAssistantPanelDelegate {
.active_context_editor(cx)
.or_else(|| panel.new_context(window, cx))
{
context.update(cx, |context, cx| {
context.quote_ranges(selection_ranges, snapshot, window, cx)
});
context.update(cx, |context, cx| context.quote_creases(creases, window, cx));
};
});
});

View File

@@ -8,8 +8,8 @@ use assistant_slash_commands::{
use client::{proto, zed_urls};
use collections::{BTreeSet, HashMap, HashSet, hash_map};
use editor::{
Anchor, Editor, EditorEvent, MenuInlineCompletionsPolicy, MultiBuffer, MultiBufferSnapshot,
ProposedChangeLocation, ProposedChangesEditor, RowExt, ToOffset as _, ToPoint,
Anchor, Editor, EditorEvent, MenuInlineCompletionsPolicy, ProposedChangeLocation,
ProposedChangesEditor, RowExt, ToOffset as _, ToPoint,
actions::{MoveToEndOfLine, Newline, ShowCompletions},
display_map::{
BlockContext, BlockId, BlockPlacement, BlockProperties, BlockStyle, Crease, CreaseMetadata,
@@ -155,8 +155,7 @@ pub trait AssistantPanelDelegate {
fn quote_selection(
&self,
workspace: &mut Workspace,
selection_ranges: Vec<Range<Anchor>>,
buffer: Entity<MultiBuffer>,
creases: Vec<(String, String)>,
window: &mut Window,
cx: &mut Context<Workspace>,
);
@@ -1801,42 +1800,23 @@ impl ContextEditor {
return;
};
let Some((selections, buffer)) = maybe!({
let editor = workspace
.active_item(cx)
.and_then(|item| item.act_as::<Editor>(cx))?;
let buffer = editor.read(cx).buffer().clone();
let snapshot = buffer.read(cx).snapshot(cx);
let selections = editor.update(cx, |editor, cx| {
editor
.selections
.all_adjusted(cx)
.into_iter()
.map(|s| snapshot.anchor_after(s.start)..snapshot.anchor_before(s.end))
.collect::<Vec<_>>()
});
Some((selections, buffer))
}) else {
let Some(creases) = selections_creases(workspace, cx) else {
return;
};
if selections.is_empty() {
if creases.is_empty() {
return;
}
assistant_panel_delegate.quote_selection(workspace, selections, buffer, window, cx);
assistant_panel_delegate.quote_selection(workspace, creases, window, cx);
}
pub fn quote_ranges(
pub fn quote_creases(
&mut self,
ranges: Vec<Range<Point>>,
snapshot: MultiBufferSnapshot,
creases: Vec<(String, String)>,
window: &mut Window,
cx: &mut Context<Self>,
) {
let creases = selections_creases(ranges, snapshot, cx);
self.editor.update(cx, |editor, cx| {
editor.insert("\n", window, cx);
for (text, crease_title) in creases {

View File

@@ -3,12 +3,10 @@ use assistant_slash_command::{
ArgumentCompletion, SlashCommand, SlashCommandContent, SlashCommandEvent,
SlashCommandOutputSection, SlashCommandResult,
};
use editor::{Editor, MultiBufferSnapshot};
use editor::Editor;
use futures::StreamExt;
use gpui::{App, SharedString, Task, WeakEntity, Window};
use gpui::{App, Context, SharedString, Task, WeakEntity, Window};
use language::{BufferSnapshot, CodeLabel, LspAdapterDelegate};
use rope::Point;
use std::ops::Range;
use std::sync::Arc;
use std::sync::atomic::AtomicBool;
use ui::IconName;
@@ -71,22 +69,7 @@ impl SlashCommand for SelectionCommand {
let mut events = vec![];
let Some(creases) = workspace
.update(cx, |workspace, cx| {
let editor = workspace
.active_item(cx)
.and_then(|item| item.act_as::<Editor>(cx))?;
editor.update(cx, |editor, cx| {
let selection_ranges = editor
.selections
.all_adjusted(cx)
.iter()
.map(|selection| selection.range())
.collect::<Vec<_>>();
let snapshot = editor.buffer().read(cx).snapshot(cx);
Some(selections_creases(selection_ranges, snapshot, cx))
})
})
.update(cx, selections_creases)
.unwrap_or_else(|e| {
events.push(Err(e));
None
@@ -119,82 +102,94 @@ impl SlashCommand for SelectionCommand {
}
pub fn selections_creases(
selection_ranges: Vec<Range<Point>>,
snapshot: MultiBufferSnapshot,
cx: &App,
) -> Vec<(String, String)> {
let mut creases = Vec::new();
for range in selection_ranges {
let selected_text = snapshot.text_for_range(range.clone()).collect::<String>();
if selected_text.is_empty() {
continue;
}
let start_language = snapshot.language_at(range.start);
let end_language = snapshot.language_at(range.end);
let language_name = if start_language == end_language {
start_language.map(|language| language.code_fence_block_name())
} else {
None
};
let language_name = language_name.as_deref().unwrap_or("");
let filename = snapshot.file_at(range.start).map(|file| file.full_path(cx));
let text = if language_name == "markdown" {
selected_text
.lines()
.map(|line| format!("> {}", line))
.collect::<Vec<_>>()
.join("\n")
} else {
let start_symbols = snapshot
.symbols_containing(range.start, None)
.map(|(_, symbols)| symbols);
let end_symbols = snapshot
.symbols_containing(range.end, None)
.map(|(_, symbols)| symbols);
workspace: &mut workspace::Workspace,
cx: &mut Context<Workspace>,
) -> Option<Vec<(String, String)>> {
let editor = workspace
.active_item(cx)
.and_then(|item| item.act_as::<Editor>(cx))?;
let outline_text =
if let Some((start_symbols, end_symbols)) = start_symbols.zip(end_symbols) {
Some(
start_symbols
.into_iter()
.zip(end_symbols)
.take_while(|(a, b)| a == b)
.map(|(a, _)| a.text)
.collect::<Vec<_>>()
.join(" > "),
)
let mut creases = vec![];
editor.update(cx, |editor, cx| {
let selections = editor.selections.all_adjusted(cx);
let buffer = editor.buffer().read(cx).snapshot(cx);
for selection in selections {
let range = editor::ToOffset::to_offset(&selection.start, &buffer)
..editor::ToOffset::to_offset(&selection.end, &buffer);
let selected_text = buffer.text_for_range(range.clone()).collect::<String>();
if selected_text.is_empty() {
continue;
}
let start_language = buffer.language_at(range.start);
let end_language = buffer.language_at(range.end);
let language_name = if start_language == end_language {
start_language.map(|language| language.code_fence_block_name())
} else {
None
};
let language_name = language_name.as_deref().unwrap_or("");
let filename = buffer
.file_at(selection.start)
.map(|file| file.full_path(cx));
let text = if language_name == "markdown" {
selected_text
.lines()
.map(|line| format!("> {}", line))
.collect::<Vec<_>>()
.join("\n")
} else {
let start_symbols = buffer
.symbols_containing(selection.start, None)
.map(|(_, symbols)| symbols);
let end_symbols = buffer
.symbols_containing(selection.end, None)
.map(|(_, symbols)| symbols);
let outline_text =
if let Some((start_symbols, end_symbols)) = start_symbols.zip(end_symbols) {
Some(
start_symbols
.into_iter()
.zip(end_symbols)
.take_while(|(a, b)| a == b)
.map(|(a, _)| a.text)
.collect::<Vec<_>>()
.join(" > "),
)
} else {
None
};
let line_comment_prefix = start_language
.and_then(|l| l.default_scope().line_comment_prefixes().first().cloned());
let fence = codeblock_fence_for_path(
filename.as_deref(),
Some(selection.start.row..=selection.end.row),
);
if let Some((line_comment_prefix, outline_text)) =
line_comment_prefix.zip(outline_text)
{
let breadcrumb = format!("{line_comment_prefix}Excerpt from: {outline_text}\n");
format!("{fence}{breadcrumb}{selected_text}\n```")
} else {
None
};
let line_comment_prefix = start_language
.and_then(|l| l.default_scope().line_comment_prefixes().first().cloned());
let fence = codeblock_fence_for_path(
filename.as_deref(),
Some(range.start.row..=range.end.row),
);
if let Some((line_comment_prefix, outline_text)) = line_comment_prefix.zip(outline_text)
{
let breadcrumb = format!("{line_comment_prefix}Excerpt from: {outline_text}\n");
format!("{fence}{breadcrumb}{selected_text}\n```")
format!("{fence}{selected_text}\n```")
}
};
let crease_title = if let Some(path) = filename {
let start_line = selection.start.row + 1;
let end_line = selection.end.row + 1;
if start_line == end_line {
format!("{}, Line {}", path.display(), start_line)
} else {
format!("{}, Lines {} to {}", path.display(), start_line, end_line)
}
} else {
format!("{fence}{selected_text}\n```")
}
};
let crease_title = if let Some(path) = filename {
let start_line = range.start.row + 1;
let end_line = range.end.row + 1;
if start_line == end_line {
format!("{}, Line {}", path.display(), start_line)
} else {
format!("{}, Lines {} to {}", path.display(), start_line, end_line)
}
} else {
"Quoted selection".to_string()
};
creases.push((text, crease_title));
}
creases
"Quoted selection".to_string()
};
creases.push((text, crease_title));
}
});
Some(creases)
}

View File

@@ -9,10 +9,6 @@ use std::fmt::Formatter;
use std::sync::Arc;
use anyhow::Result;
use gpui::AnyElement;
use gpui::Context;
use gpui::IntoElement;
use gpui::Window;
use gpui::{App, Entity, SharedString, Task};
use icons::IconName;
use language_model::LanguageModelRequestMessage;
@@ -28,87 +24,16 @@ pub fn init(cx: &mut App) {
ToolRegistry::default_global(cx);
}
#[derive(Debug, Clone)]
pub enum ToolUseStatus {
NeedsConfirmation,
Pending,
Running,
Finished(SharedString),
Error(SharedString),
}
impl ToolUseStatus {
pub fn text(&self) -> SharedString {
match self {
ToolUseStatus::NeedsConfirmation => "".into(),
ToolUseStatus::Pending => "".into(),
ToolUseStatus::Running => "".into(),
ToolUseStatus::Finished(out) => out.clone(),
ToolUseStatus::Error(out) => out.clone(),
}
}
}
/// The result of running a tool, containing both the asynchronous output
/// and an optional card view that can be rendered immediately.
/// The result of running a tool
pub struct ToolResult {
/// The asynchronous task that will eventually resolve to the tool's output
pub output: Task<Result<String>>,
/// An optional view to present the output of the tool.
pub card: Option<AnyToolCard>,
}
pub trait ToolCard: 'static + Sized {
fn render(
&mut self,
status: &ToolUseStatus,
window: &mut Window,
cx: &mut Context<Self>,
) -> impl IntoElement;
}
#[derive(Clone)]
pub struct AnyToolCard {
entity: gpui::AnyEntity,
render: fn(
entity: gpui::AnyEntity,
status: &ToolUseStatus,
window: &mut Window,
cx: &mut App,
) -> AnyElement,
}
impl<T: ToolCard> From<Entity<T>> for AnyToolCard {
fn from(entity: Entity<T>) -> Self {
fn downcast_render<T: ToolCard>(
entity: gpui::AnyEntity,
status: &ToolUseStatus,
window: &mut Window,
cx: &mut App,
) -> AnyElement {
let entity = entity.downcast::<T>().unwrap();
entity.update(cx, |entity, cx| {
entity.render(status, window, cx).into_any_element()
})
}
Self {
entity: entity.into(),
render: downcast_render::<T>,
}
}
}
impl AnyToolCard {
pub fn render(&self, status: &ToolUseStatus, window: &mut Window, cx: &mut App) -> AnyElement {
(self.render)(self.entity.clone(), status, window, cx)
}
}
impl From<Task<Result<String>>> for ToolResult {
/// Convert from a task to a ToolResult with no card
/// Convert from a task to a ToolResult
fn from(output: Task<Result<String>>) -> Self {
Self { output, card: None }
Self { output }
}
}

View File

@@ -16,7 +16,6 @@ anyhow.workspace = true
assistant_tool.workspace = true
chrono.workspace = true
collections.workspace = true
feature_flags.workspace = true
futures.workspace = true
gpui.workspace = true
html_to_markdown.workspace = true
@@ -33,9 +32,7 @@ ui.workspace = true
util.workspace = true
worktree.workspace = true
open = { workspace = true }
web_search.workspace = true
workspace-hack.workspace = true
zed_llm_client.workspace = true
[dev-dependencies]
collections = { workspace = true, features = ["test-support"] }

View File

@@ -22,17 +22,14 @@ mod schema;
mod symbol_info_tool;
mod terminal_tool;
mod thinking_tool;
mod web_search_tool;
use std::sync::Arc;
use assistant_tool::ToolRegistry;
use copy_path_tool::CopyPathTool;
use feature_flags::FeatureFlagAppExt;
use gpui::App;
use http_client::HttpClientWithUrl;
use move_path_tool::MovePathTool;
use web_search_tool::WebSearchTool;
use crate::batch_tool::BatchTool;
use crate::code_action_tool::CodeActionTool;
@@ -59,39 +56,28 @@ pub fn init(http_client: Arc<HttpClientWithUrl>, cx: &mut App) {
assistant_tool::init(cx);
let registry = ToolRegistry::global(cx);
registry.register_tool(TerminalTool);
registry.register_tool(BatchTool);
registry.register_tool(CodeActionTool);
registry.register_tool(CodeSymbolsTool);
registry.register_tool(ContentsTool);
registry.register_tool(CopyPathTool);
registry.register_tool(CreateDirectoryTool);
registry.register_tool(CreateFileTool);
registry.register_tool(CopyPathTool);
registry.register_tool(DeletePathTool);
registry.register_tool(DiagnosticsTool);
registry.register_tool(FetchTool::new(http_client));
registry.register_tool(FindReplaceFileTool);
registry.register_tool(ListDirectoryTool);
registry.register_tool(SymbolInfoTool);
registry.register_tool(CodeActionTool);
registry.register_tool(MovePathTool);
registry.register_tool(DiagnosticsTool);
registry.register_tool(ListDirectoryTool);
registry.register_tool(NowTool);
registry.register_tool(OpenTool);
registry.register_tool(CodeSymbolsTool);
registry.register_tool(ContentsTool);
registry.register_tool(PathSearchTool);
registry.register_tool(ReadFileTool);
registry.register_tool(RegexSearchTool);
registry.register_tool(RenameTool);
registry.register_tool(SymbolInfoTool);
registry.register_tool(TerminalTool);
registry.register_tool(ThinkingTool);
cx.observe_flag::<feature_flags::ZedProWebSearchTool, _>({
move |is_enabled, cx| {
if is_enabled {
ToolRegistry::global(cx).register_tool(WebSearchTool);
} else {
ToolRegistry::global(cx).unregister_tool(WebSearchTool);
}
}
})
.detach();
registry.register_tool(FetchTool::new(http_client));
}
#[cfg(test)]

View File

@@ -1,213 +0,0 @@
use std::{sync::Arc, time::Duration};
use crate::schema::json_schema_for;
use anyhow::{Context as _, Result, anyhow};
use assistant_tool::{ActionLog, Tool, ToolCard, ToolResult, ToolUseStatus};
use futures::{FutureExt, TryFutureExt};
use gpui::{
Animation, AnimationExt, App, AppContext, Context, Entity, IntoElement, Task, Window,
pulsating_between,
};
use language_model::{LanguageModelRequestMessage, LanguageModelToolSchemaFormat};
use project::Project;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use ui::{IconName, Tooltip, prelude::*};
use web_search::WebSearchRegistry;
use zed_llm_client::WebSearchResponse;
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
pub struct WebSearchToolInput {
/// The search term or question to query on the web.
query: String,
}
pub struct WebSearchTool;
impl Tool for WebSearchTool {
fn name(&self) -> String {
"web_search".into()
}
fn needs_confirmation(&self, _: &serde_json::Value, _: &App) -> bool {
false
}
fn description(&self) -> String {
"Search the web for information using your query. Use this when you need real-time information, facts, or data that might not be in your training. Results will include snippets and links from relevant web pages.".into()
}
fn icon(&self) -> IconName {
IconName::Globe
}
fn input_schema(&self, format: LanguageModelToolSchemaFormat) -> Result<serde_json::Value> {
json_schema_for::<WebSearchToolInput>(format)
}
fn ui_text(&self, _input: &serde_json::Value) -> String {
"Web Search".to_string()
}
fn run(
self: Arc<Self>,
input: serde_json::Value,
_messages: &[LanguageModelRequestMessage],
_project: Entity<Project>,
_action_log: Entity<ActionLog>,
cx: &mut App,
) -> ToolResult {
let input = match serde_json::from_value::<WebSearchToolInput>(input) {
Ok(input) => input,
Err(err) => return Task::ready(Err(anyhow!(err))).into(),
};
let Some(provider) = WebSearchRegistry::read_global(cx).active_provider() else {
return Task::ready(Err(anyhow!("Web search is not available."))).into();
};
let search_task = provider.search(input.query, cx).map_err(Arc::new).shared();
let output = cx.background_spawn({
let search_task = search_task.clone();
async move {
let response = search_task.await.map_err(|err| anyhow!(err))?;
serde_json::to_string(&response).context("Failed to serialize search results")
}
});
ToolResult {
output,
card: Some(cx.new(|cx| WebSearchToolCard::new(search_task, cx)).into()),
}
}
}
struct WebSearchToolCard {
response: Option<Result<WebSearchResponse>>,
_task: Task<()>,
}
impl WebSearchToolCard {
fn new(
search_task: impl 'static + Future<Output = Result<WebSearchResponse, Arc<anyhow::Error>>>,
cx: &mut Context<Self>,
) -> Self {
let _task = cx.spawn(async move |this, cx| {
let response = search_task.await.map_err(|err| anyhow!(err));
this.update(cx, |this, cx| {
this.response = Some(response);
cx.notify();
})
.ok();
});
Self {
response: None,
_task,
}
}
}
impl ToolCard for WebSearchToolCard {
fn render(
&mut self,
_status: &ToolUseStatus,
_window: &mut Window,
cx: &mut Context<Self>,
) -> impl IntoElement {
let header = h_flex()
.id("tool-label-container")
.gap_1p5()
.max_w_full()
.overflow_x_scroll()
.child(
Icon::new(IconName::Globe)
.size(IconSize::XSmall)
.color(Color::Muted),
)
.child(match self.response.as_ref() {
Some(Ok(response)) => {
let text: SharedString = if response.citations.len() == 1 {
"1 result".into()
} else {
format!("{} results", response.citations.len()).into()
};
h_flex()
.gap_1p5()
.child(Label::new("Searched the Web").size(LabelSize::Small))
.child(
div()
.size(px(3.))
.rounded_full()
.bg(cx.theme().colors().text),
)
.child(Label::new(text).size(LabelSize::Small))
.into_any_element()
}
Some(Err(error)) => div()
.id("web-search-error")
.child(Label::new("Web Search failed").size(LabelSize::Small))
.tooltip(Tooltip::text(error.to_string()))
.into_any_element(),
None => Label::new("Searching the Web…")
.size(LabelSize::Small)
.with_animation(
"web-search-label",
Animation::new(Duration::from_secs(2))
.repeat()
.with_easing(pulsating_between(0.6, 1.)),
|label, delta| label.alpha(delta),
)
.into_any_element(),
})
.into_any();
let content =
self.response.as_ref().and_then(|response| match response {
Ok(response) => {
Some(
v_flex()
.ml_1p5()
.pl_1p5()
.border_l_1()
.border_color(cx.theme().colors().border_variant)
.gap_1()
.children(response.citations.iter().enumerate().map(
|(index, citation)| {
let title = citation.title.clone();
let url = citation.url.clone();
Button::new(("citation", index), title)
.label_size(LabelSize::Small)
.color(Color::Muted)
.icon(IconName::ArrowUpRight)
.icon_size(IconSize::XSmall)
.icon_position(IconPosition::End)
.truncate(true)
.tooltip({
let url = url.clone();
move |window, cx| {
Tooltip::with_meta(
"Citation Link",
None,
url.clone(),
window,
cx,
)
}
})
.on_click({
let url = url.clone();
move |_, _, cx| cx.open_url(&url)
})
},
))
.into_any(),
)
}
Err(_) => None,
});
v_flex().my_2().gap_1().child(header).children(content)
}
}

View File

@@ -75,7 +75,6 @@ tracing-subscriber = { version = "0.3.18", features = ["env-filter", "json", "re
util.workspace = true
uuid.workspace = true
workspace-hack.workspace = true
zed_llm_client.workspace = true
[dev-dependencies]
assistant = { workspace = true, features = ["test-support"] }

View File

@@ -1,4 +0,0 @@
alter table subscription_usages
add column plan text not null;
create index ix_subscription_usages_on_plan on subscription_usages (plan);

View File

@@ -330,10 +330,8 @@ async fn create_billing_subscription(
.await?
}
None => {
let default_model = llm_db.model(
zed_llm_client::LanguageModelProvider::Anthropic,
"claude-3-7-sonnet",
)?;
let default_model =
llm_db.model(rpc::LanguageModelProvider::Anthropic, "claude-3-7-sonnet")?;
let stripe_model = stripe_billing.register_model(default_model).await?;
stripe_billing
.checkout(customer_id, &user.github_login, &stripe_model, &success_url)
@@ -1020,20 +1018,8 @@ async fn get_current_usage(
return Ok(Json(empty_usage));
};
let plan = match usage.plan {
SubscriptionKind::ZedPro => zed_llm_client::Plan::ZedPro,
SubscriptionKind::ZedProTrial => zed_llm_client::Plan::ZedProTrial,
SubscriptionKind::ZedFree => zed_llm_client::Plan::Free,
};
let model_requests_limit = match plan.model_requests_limit() {
zed_llm_client::UsageLimit::Limited(limit) => Some(limit),
zed_llm_client::UsageLimit::Unlimited => None,
};
let edit_prediction_limit = match plan.edit_predictions_limit() {
zed_llm_client::UsageLimit::Limited(limit) => Some(limit),
zed_llm_client::UsageLimit::Unlimited => None,
};
let model_requests_limit = Some(500);
let edit_prediction_limit = Some(2000);
Ok(Json(GetCurrentUsageResponse {
model_requests: UsageCounts {

View File

@@ -8,9 +8,9 @@ mod tests;
use collections::HashMap;
pub use ids::*;
use rpc::LanguageModelProvider;
pub use seed::*;
pub use tables::*;
use zed_llm_client::LanguageModelProvider;
#[cfg(test)]
pub use tests::TestLlmDb;

View File

@@ -1,5 +1,4 @@
use crate::db::UserId;
use crate::db::billing_subscription::SubscriptionKind;
use sea_orm::entity::prelude::*;
use time::PrimitiveDateTime;
@@ -11,7 +10,6 @@ pub struct Model {
pub user_id: UserId,
pub period_start_at: PrimitiveDateTime,
pub period_end_at: PrimitiveDateTime,
pub plan: SubscriptionKind,
pub model_requests: i32,
pub edit_predictions: i32,
}

View File

@@ -1,5 +1,5 @@
use pretty_assertions::assert_eq;
use zed_llm_client::LanguageModelProvider;
use rpc::LanguageModelProvider;
use crate::llm::db::LlmDatabase;
use crate::test_llm_db;

View File

@@ -10,7 +10,6 @@ use std::time::Duration;
use thiserror::Error;
use util::maybe;
use uuid::Uuid;
use zed_llm_client::Plan;
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
@@ -26,10 +25,11 @@ pub struct LlmTokenClaims {
pub is_staff: bool,
pub has_llm_closed_beta_feature_flag: bool,
pub bypass_account_age_check: bool,
pub has_predict_edits_feature_flag: bool,
pub has_llm_subscription: bool,
pub max_monthly_spend_in_cents: u32,
pub custom_llm_monthly_allowance_in_cents: Option<u32>,
pub plan: Plan,
pub plan: rpc::proto::Plan,
#[serde(default)]
pub subscription_period: Option<(NaiveDateTime, NaiveDateTime)>,
}
@@ -70,6 +70,9 @@ impl LlmTokenClaims {
bypass_account_age_check: feature_flags
.iter()
.any(|flag| flag == "bypass-account-age-check"),
has_predict_edits_feature_flag: feature_flags
.iter()
.any(|flag| flag == "predict-edits"),
has_llm_subscription: has_legacy_llm_subscription,
max_monthly_spend_in_cents: billing_preferences
.map_or(DEFAULT_MAX_MONTHLY_SPEND.0, |preferences| {
@@ -78,11 +81,7 @@ impl LlmTokenClaims {
custom_llm_monthly_allowance_in_cents: user
.custom_llm_monthly_allowance_in_cents
.map(|allowance| allowance as u32),
plan: match plan {
rpc::proto::Plan::Free => Plan::Free,
rpc::proto::Plan::ZedPro => Plan::ZedPro,
rpc::proto::Plan::ZedProTrial => Plan::ZedProTrial,
},
plan,
subscription_period: maybe!({
let subscription = subscription?;
let period_start_at = subscription.current_period_start_at()?;

View File

@@ -3707,9 +3707,7 @@ async fn count_language_model_tokens(
let rate_limit: Box<dyn RateLimit> = match session.current_plan(&session.db().await).await? {
proto::Plan::ZedPro => Box::new(ZedProCountLanguageModelTokensRateLimit),
proto::Plan::Free | proto::Plan::ZedProTrial => {
Box::new(FreeCountLanguageModelTokensRateLimit)
}
proto::Plan::Free => Box::new(FreeCountLanguageModelTokensRateLimit),
};
session
@@ -3829,7 +3827,7 @@ async fn compute_embeddings(
let rate_limit: Box<dyn RateLimit> = match session.current_plan(&session.db().await).await? {
proto::Plan::ZedPro => Box::new(ZedProComputeEmbeddingsRateLimit),
proto::Plan::Free | proto::Plan::ZedProTrial => Box::new(FreeComputeEmbeddingsRateLimit),
proto::Plan::Free => Box::new(FreeComputeEmbeddingsRateLimit),
};
session

View File

@@ -1,3 +1,4 @@
use std::any::Any;
use std::fmt::Display;
use std::ops::{Deref, DerefMut};
use std::sync::LazyLock;
@@ -15,9 +16,20 @@ pub trait Component {
fn scope() -> ComponentScope {
ComponentScope::None
}
fn init_component(_weak_workspace: Box<dyn Any>) {}
// In theory we could downcast to a WeakEntity<Workspace> and use that to build
// whatever you need to initialize the component, but I haven't tested it yet.
//
// fn init_component(weak_workspace: Box<dyn Any>) {
// let weak_workspace = weak_workspace.downcast::<WeakEntity<Workspace>>().unwrap();
// }
fn name() -> &'static str {
std::any::type_name::<Self>()
}
/// Returns a name that the component should be sorted by.
///
/// Implement this if the component should be sorted in an alternate order than its name.

View File

@@ -23,6 +23,7 @@ languages.workspace = true
notifications.workspace = true
project.workspace = true
ui.workspace = true
ui_parking_lot.workspace = true
ui_input.workspace = true
workspace-hack.workspace = true
workspace.workspace = true

View File

@@ -26,6 +26,9 @@ use ui_input::SingleLineInput;
use workspace::{AppState, ItemId, SerializableItem};
use workspace::{Item, Workspace, WorkspaceId, item::ItemEvent};
#[allow(unused)]
use ui_parking_lot::*; // Import for component registry, gets culled otherwise
pub fn init(app_state: Arc<AppState>, cx: &mut App) {
workspace::register_serializable_item::<ComponentPreview>(cx);

View File

@@ -1,5 +1,5 @@
use ::fs::Fs;
use anyhow::{Context as _, Result, anyhow};
use anyhow::{Context as _, Ok, Result, anyhow};
use async_compression::futures::bufread::GzipDecoder;
use async_tar::Archive;
use async_trait::async_trait;
@@ -256,21 +256,7 @@ pub trait DebugAdapter: 'static + Send + Sync {
self.name()
);
delegate.update_status(self.name(), DapStatus::Downloading);
match self.install_binary(version, delegate).await {
Ok(_) => {
delegate.update_status(self.name(), DapStatus::None);
}
Err(error) => {
delegate.update_status(
self.name(),
DapStatus::Failed {
error: error.to_string(),
},
);
return Err(error);
}
}
self.install_binary(version, delegate).await?;
delegate
.updated_adapters()

View File

@@ -7,7 +7,7 @@ pub mod transport;
pub use dap_types::*;
pub use registry::DapRegistry;
pub use task::DebugRequestType;
pub use task::{DebugAdapterConfig, DebugRequestType};
pub type ScopeId = u64;
pub type VariableReference = u64;

View File

@@ -33,7 +33,6 @@ use std::sync::Arc;
use task::DebugTaskDefinition;
use terminal_view::terminal_panel::TerminalPanel;
use ui::{ContextMenu, Divider, DropdownMenu, Tooltip, prelude::*};
use util::debug_panic;
use workspace::{
Workspace,
dock::{DockPosition, Panel, PanelEvent},
@@ -317,20 +316,8 @@ impl DebugPanel {
.any(|item| item.read(cx).session_id(cx) == session_id)
{
// We already have an item for this session.
debug_panic!("We should never reuse session ids");
return;
}
this.sessions.retain(|session| {
session
.read(cx)
.mode()
.as_running()
.map_or(false, |running_state| {
!running_state.read(cx).session().read(cx).is_terminated()
})
});
let session_item = DebugSession::running(
project,
this.workspace.clone(),
@@ -782,6 +769,9 @@ impl DebugPanel {
this.restart_session(cx);
},
))
.disabled(
!capabilities.supports_restart_request.unwrap_or_default(),
)
.tooltip(move |window, cx| {
Tooltip::text("Restart")(window, cx)
}),

View File

@@ -15,7 +15,7 @@ use text::{AnchorRangeExt, Point};
use theme::ThemeSettings;
use ui::{
ActiveTheme, AnyElement, App, Context, IntoElement, ParentElement, SharedString, Styled,
Window, div,
Window, div, px,
};
use util::maybe;
@@ -166,8 +166,7 @@ impl DiagnosticBlock {
pub fn render_block(&self, editor: WeakEntity<Editor>, bcx: &BlockContext) -> AnyElement {
let cx = &bcx.app;
let status_colors = bcx.app.theme().status();
let max_width = bcx.em_width * 100.;
let max_width = px(600.);
let (background_color, border_color) = match self.severity {
DiagnosticSeverity::ERROR => (status_colors.error_background, status_colors.error),

View File

@@ -46,8 +46,7 @@ use workspace::{
actions!(diagnostics, [Deploy, ToggleWarnings]);
#[derive(Default)]
pub(crate) struct IncludeWarnings(bool);
struct IncludeWarnings(bool);
impl Global for IncludeWarnings {}
pub fn init(cx: &mut App) {
@@ -380,6 +379,7 @@ impl ProjectDiagnosticsEditor {
Point::zero()..buffer_snapshot.max_point(),
false,
)
.filter(|d| !(d.diagnostic.is_primary && d.diagnostic.is_unnecessary))
.collect::<Vec<_>>();
let unchanged = this.update(cx, |this, _| {
if this.diagnostics.get(&buffer_id).is_some_and(|existing| {

View File

@@ -1,9 +1,9 @@
use super::*;
use collections::{HashMap, HashSet};
use editor::{
DisplayPoint, InlayId,
DisplayPoint,
actions::{GoToDiagnostic, GoToPreviousDiagnostic, MoveToBeginning},
display_map::{DisplayRow, Inlay},
display_map::DisplayRow,
test::{editor_content_with_blocks, editor_test_context::EditorTestContext},
};
use gpui::{TestAppContext, VisualTestContext};
@@ -620,7 +620,7 @@ async fn test_diagnostics_multiple_servers(cx: &mut TestAppContext) {
}
#[gpui::test(iterations = 20)]
async fn test_random_diagnostics_blocks(cx: &mut TestAppContext, mut rng: StdRng) {
async fn test_random_diagnostics(cx: &mut TestAppContext, mut rng: StdRng) {
init_test(cx);
let operations = env::var("OPERATIONS")
@@ -779,162 +779,6 @@ async fn test_random_diagnostics_blocks(cx: &mut TestAppContext, mut rng: StdRng
}
}
// similar to above, but with inlays. Used to find panics when mixing diagnostics and inlays.
#[gpui::test]
async fn test_random_diagnostics_with_inlays(cx: &mut TestAppContext, mut rng: StdRng) {
init_test(cx);
let operations = env::var("OPERATIONS")
.map(|i| i.parse().expect("invalid `OPERATIONS` variable"))
.unwrap_or(10);
let fs = FakeFs::new(cx.executor());
fs.insert_tree(path!("/test"), json!({})).await;
let project = Project::test(fs.clone(), [path!("/test").as_ref()], cx).await;
let lsp_store = project.read_with(cx, |project, _| project.lsp_store());
let window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
let cx = &mut VisualTestContext::from_window(*window, cx);
let workspace = window.root(cx).unwrap();
let mutated_diagnostics = window.build_entity(cx, |window, cx| {
ProjectDiagnosticsEditor::new(true, project.clone(), workspace.downgrade(), window, cx)
});
workspace.update_in(cx, |workspace, window, cx| {
workspace.add_item_to_center(Box::new(mutated_diagnostics.clone()), window, cx);
});
mutated_diagnostics.update_in(cx, |diagnostics, window, _cx| {
assert!(diagnostics.focus_handle.is_focused(window));
});
let mut next_id = 0;
let mut next_filename = 0;
let mut language_server_ids = vec![LanguageServerId(0)];
let mut updated_language_servers = HashSet::default();
let mut current_diagnostics: HashMap<(PathBuf, LanguageServerId), Vec<lsp::Diagnostic>> =
Default::default();
let mut next_inlay_id = 0;
for _ in 0..operations {
match rng.gen_range(0..100) {
// language server completes its diagnostic check
0..=20 if !updated_language_servers.is_empty() => {
let server_id = *updated_language_servers.iter().choose(&mut rng).unwrap();
log::info!("finishing diagnostic check for language server {server_id}");
lsp_store.update(cx, |lsp_store, cx| {
lsp_store.disk_based_diagnostics_finished(server_id, cx)
});
if rng.gen_bool(0.5) {
cx.run_until_parked();
}
}
21..=50 => mutated_diagnostics.update_in(cx, |diagnostics, window, cx| {
diagnostics.editor.update(cx, |editor, cx| {
let snapshot = editor.snapshot(window, cx);
if snapshot.buffer_snapshot.len() > 0 {
let position = rng.gen_range(0..snapshot.buffer_snapshot.len());
let position = snapshot.buffer_snapshot.clip_offset(position, Bias::Left);
log::info!(
"adding inlay at {position}/{}: {:?}",
snapshot.buffer_snapshot.len(),
snapshot.buffer_snapshot.text(),
);
editor.splice_inlays(
&[],
vec![Inlay {
id: InlayId::InlineCompletion(post_inc(&mut next_inlay_id)),
position: snapshot.buffer_snapshot.anchor_before(position),
text: Rope::from(format!("Test inlay {next_inlay_id}")),
}],
cx,
);
}
});
}),
// language server updates diagnostics
_ => {
let (path, server_id, diagnostics) =
match current_diagnostics.iter_mut().choose(&mut rng) {
// update existing set of diagnostics
Some(((path, server_id), diagnostics)) if rng.gen_bool(0.5) => {
(path.clone(), *server_id, diagnostics)
}
// insert a set of diagnostics for a new path
_ => {
let path: PathBuf =
format!(path!("/test/{}.rs"), post_inc(&mut next_filename)).into();
let len = rng.gen_range(128..256);
let content =
RandomCharIter::new(&mut rng).take(len).collect::<String>();
fs.insert_file(&path, content.into_bytes()).await;
let server_id = match language_server_ids.iter().choose(&mut rng) {
Some(server_id) if rng.gen_bool(0.5) => *server_id,
_ => {
let id = LanguageServerId(language_server_ids.len());
language_server_ids.push(id);
id
}
};
(
path.clone(),
server_id,
current_diagnostics.entry((path, server_id)).or_default(),
)
}
};
updated_language_servers.insert(server_id);
lsp_store.update(cx, |lsp_store, cx| {
log::info!("updating diagnostics. language server {server_id} path {path:?}");
randomly_update_diagnostics_for_path(
&fs,
&path,
diagnostics,
&mut next_id,
&mut rng,
);
lsp_store
.update_diagnostics(
server_id,
lsp::PublishDiagnosticsParams {
uri: lsp::Url::from_file_path(&path).unwrap_or_else(|_| {
lsp::Url::parse("file:///test/fallback.rs").unwrap()
}),
diagnostics: diagnostics.clone(),
version: None,
},
&[],
cx,
)
.unwrap()
});
cx.executor()
.advance_clock(DIAGNOSTICS_UPDATE_DELAY + Duration::from_millis(10));
cx.run_until_parked();
}
}
}
log::info!("updating mutated diagnostics view");
mutated_diagnostics.update_in(cx, |diagnostics, window, cx| {
diagnostics.update_stale_excerpts(window, cx)
});
cx.executor()
.advance_clock(DIAGNOSTICS_UPDATE_DELAY + Duration::from_millis(10));
cx.run_until_parked();
}
#[gpui::test]
async fn active_diagnostics_dismiss_after_invalidation(cx: &mut TestAppContext) {
init_test(cx);

View File

@@ -9,7 +9,7 @@ use language::Diagnostic;
use ui::{Button, ButtonLike, Color, Icon, IconName, Label, Tooltip, h_flex, prelude::*};
use workspace::{StatusItemView, ToolbarItemEvent, Workspace, item::ItemHandle};
use crate::{Deploy, IncludeWarnings, ProjectDiagnosticsEditor};
use crate::{Deploy, ProjectDiagnosticsEditor};
pub struct DiagnosticIndicator {
summary: project::DiagnosticSummary,
@@ -94,11 +94,6 @@ impl Render for DiagnosticIndicator {
})
.on_click(cx.listener(|this, _, window, cx| {
if let Some(workspace) = this.workspace.upgrade() {
if this.summary.error_count == 0 && this.summary.warning_count > 0 {
cx.update_default_global(
|show_warnings: &mut IncludeWarnings, _| show_warnings.0 = true,
);
}
workspace.update(cx, |workspace, cx| {
ProjectDiagnosticsEditor::deploy(
workspace,

View File

@@ -306,8 +306,6 @@ actions!(
GoToPreviousHunk,
GoToImplementation,
GoToImplementationSplit,
GoToNextChange,
GoToPreviousChange,
GoToPreviousDiagnostic,
GoToTypeDefinition,
GoToTypeDefinitionSplit,

View File

@@ -49,8 +49,8 @@ use language::{
};
use lsp::DiagnosticSeverity;
use multi_buffer::{
Anchor, AnchorRangeExt, ExcerptId, MultiBuffer, MultiBufferPoint, MultiBufferRow,
MultiBufferSnapshot, RowInfo, ToOffset, ToPoint,
Anchor, AnchorRangeExt, MultiBuffer, MultiBufferPoint, MultiBufferRow, MultiBufferSnapshot,
RowInfo, ToOffset, ToPoint,
};
use serde::Deserialize;
use std::{
@@ -574,21 +574,6 @@ impl DisplayMap {
self.block_map.read(snapshot, edits);
}
pub fn remove_inlays_for_excerpts(&mut self, excerpts_removed: &[ExcerptId]) {
let to_remove = self
.inlay_map
.current_inlays()
.filter_map(|inlay| {
if excerpts_removed.contains(&inlay.position.excerpt_id) {
Some(inlay.id)
} else {
None
}
})
.collect::<Vec<_>>();
self.inlay_map.splice(&to_remove, Vec::new());
}
fn tab_size(buffer: &Entity<MultiBuffer>, cx: &App) -> NonZeroU32 {
let buffer = buffer.read(cx).as_singleton().map(|buffer| buffer.read(cx));
let language = buffer

View File

@@ -36,7 +36,7 @@ enum Transform {
#[derive(Debug, Clone)]
pub struct Inlay {
pub id: InlayId,
pub(crate) id: InlayId,
pub position: Anchor,
pub text: text::Rope,
}
@@ -482,9 +482,6 @@ impl InlayMap {
};
for inlay in &self.inlays[start_ix..] {
if !inlay.position.is_valid(&buffer_snapshot) {
continue;
}
let buffer_offset = inlay.position.to_offset(&buffer_snapshot);
if buffer_offset > buffer_edit.new.end {
break;
@@ -497,7 +494,9 @@ impl InlayMap {
buffer_snapshot.text_summary_for_range(prefix_start..prefix_end),
);
new_transforms.push(Transform::Inlay(inlay.clone()), &());
if inlay.position.is_valid(&buffer_snapshot) {
new_transforms.push(Transform::Inlay(inlay.clone()), &());
}
}
// Apply the rest of the edit.

View File

@@ -693,52 +693,6 @@ pub trait Addon: 'static {
fn to_any(&self) -> &dyn std::any::Any;
}
/// A set of caret positions, registered when the editor was edited.
pub struct ChangeList {
changes: Vec<Vec<Anchor>>,
/// Currently "selected" change.
position: Option<usize>,
}
impl ChangeList {
pub fn new() -> Self {
Self {
changes: Vec::new(),
position: None,
}
}
/// Moves to the next change in the list (based on the direction given) and returns the caret positions for the next change.
/// If reaches the end of the list in the direction, returns the corresponding change until called for a different direction.
pub fn next_change(&mut self, count: usize, direction: Direction) -> Option<&[Anchor]> {
if self.changes.is_empty() {
return None;
}
let prev = self.position.unwrap_or(self.changes.len());
let next = if direction == Direction::Prev {
prev.saturating_sub(count)
} else {
(prev + count).min(self.changes.len() - 1)
};
self.position = Some(next);
self.changes.get(next).map(|anchors| anchors.as_slice())
}
/// Adds a new change to the list, resetting the change list position.
pub fn push_to_change_list(&mut self, pop_state: bool, new_positions: Vec<Anchor>) {
self.position.take();
if pop_state {
self.changes.pop();
}
self.changes.push(new_positions.clone());
}
pub fn last(&self) -> Option<&[Anchor]> {
self.changes.last().map(|anchors| anchors.as_slice())
}
}
/// Zed's primary implementation of text input, allowing users to edit a [`MultiBuffer`].
///
/// See the [module level documentation](self) for more information.
@@ -903,7 +857,6 @@ pub struct Editor {
serialize_folds: Task<()>,
mouse_cursor_hidden: bool,
hide_mouse_mode: HideMouseMode,
pub change_list: ChangeList,
}
#[derive(Copy, Clone, Debug, PartialEq, Eq, Default)]
@@ -1695,7 +1648,6 @@ impl Editor {
hide_mouse_mode: EditorSettings::get_global(cx)
.hide_mouse
.unwrap_or_default(),
change_list: ChangeList::new(),
};
if let Some(breakpoints) = this.breakpoint_store.as_ref() {
this._subscriptions
@@ -1709,8 +1661,8 @@ impl Editor {
this._subscriptions.push(cx.subscribe_in(
&cx.entity(),
window,
|editor, _, e: &EditorEvent, window, cx| match e {
EditorEvent::ScrollPositionChanged { local, .. } => {
|editor, _, e: &EditorEvent, window, cx| {
if let EditorEvent::SelectionsChanged { local } = e {
if *local {
let new_anchor = editor.scroll_manager.anchor();
let snapshot = editor.snapshot(window, cx);
@@ -1722,30 +1674,6 @@ impl Editor {
});
}
}
EditorEvent::Edited { .. } => {
if !vim_enabled(cx) {
let (map, selections) = editor.selections.all_adjusted_display(cx);
let pop_state = editor
.change_list
.last()
.map(|previous| {
previous.len() == selections.len()
&& previous.iter().enumerate().all(|(ix, p)| {
p.to_display_point(&map).row()
== selections[ix].head().row()
})
})
.unwrap_or(false);
let new_positions = selections
.into_iter()
.map(|s| map.display_point_to_anchor(s.head(), Bias::Left))
.collect();
editor
.change_list
.push_to_change_list(pop_state, new_positions);
}
}
_ => (),
},
));
@@ -4242,13 +4170,10 @@ impl Editor {
if let Some(InlaySplice {
to_remove,
to_insert,
}) = self.inlay_hint_cache.remove_excerpts(&excerpts_removed)
}) = self.inlay_hint_cache.remove_excerpts(excerpts_removed)
{
self.splice_inlays(&to_remove, to_insert, cx);
}
self.display_map.update(cx, |display_map, _| {
display_map.remove_inlays_for_excerpts(&excerpts_removed)
});
return;
}
InlayHintRefreshReason::NewLinesShown => (InvalidationStrategy::None, None),
@@ -12594,45 +12519,6 @@ impl Editor {
.iter()
.map(|selection| {
let old_range = selection.start..selection.end;
if let Some((node, _)) = buffer.syntax_ancestor(old_range.clone()) {
// manually select word at selection
if ["string_content", "inline"].contains(&node.kind()) {
let word_range = {
let display_point = buffer
.offset_to_point(old_range.start)
.to_display_point(&display_map);
let Range { start, end } =
movement::surrounding_word(&display_map, display_point);
start.to_point(&display_map).to_offset(&buffer)
..end.to_point(&display_map).to_offset(&buffer)
};
// ignore if word is already selected
if !word_range.is_empty() && old_range != word_range {
let last_word_range = {
let display_point = buffer
.offset_to_point(old_range.end)
.to_display_point(&display_map);
let Range { start, end } =
movement::surrounding_word(&display_map, display_point);
start.to_point(&display_map).to_offset(&buffer)
..end.to_point(&display_map).to_offset(&buffer)
};
// only select word if start and end point belongs to same word
if word_range == last_word_range {
selected_larger_node = true;
return Selection {
id: selection.id,
start: word_range.start,
end: word_range.end,
goal: SelectionGoal::None,
reversed: selection.reversed,
};
}
}
}
}
let mut new_range = old_range.clone();
let mut new_node = None;
while let Some((node, containing_range)) = buffer.syntax_ancestor(new_range.clone())
@@ -13375,48 +13261,6 @@ impl Editor {
.or_else(|| snapshot.buffer_snapshot.diff_hunk_before(Point::MAX))
}
fn go_to_next_change(
&mut self,
_: &GoToNextChange,
window: &mut Window,
cx: &mut Context<Self>,
) {
if let Some(selections) = self
.change_list
.next_change(1, Direction::Next)
.map(|s| s.to_vec())
{
self.change_selections(Some(Autoscroll::fit()), window, cx, |s| {
let map = s.display_map();
s.select_display_ranges(selections.iter().map(|a| {
let point = a.to_display_point(&map);
point..point
}))
})
}
}
fn go_to_previous_change(
&mut self,
_: &GoToPreviousChange,
window: &mut Window,
cx: &mut Context<Self>,
) {
if let Some(selections) = self
.change_list
.next_change(1, Direction::Prev)
.map(|s| s.to_vec())
{
self.change_selections(Some(Autoscroll::fit()), window, cx, |s| {
let map = s.display_map();
s.select_display_ranges(selections.iter().map(|a| {
let point = a.to_display_point(&map);
point..point
}))
})
}
}
fn go_to_line<T: 'static>(
&mut self,
position: Anchor,
@@ -17825,7 +17669,11 @@ impl Editor {
.and_then(|e| e.to_str())
.map(|a| a.to_string()));
let vim_mode = vim_enabled(cx);
let vim_mode = cx
.global::<SettingsStore>()
.raw_user_settings()
.get("vim_mode")
== Some(&serde_json::Value::Bool(true));
let edit_predictions_provider = all_language_settings(file, cx).edit_predictions.provider;
let copilot_enabled = edit_predictions_provider
@@ -18271,13 +18119,6 @@ impl Editor {
}
}
fn vim_enabled(cx: &App) -> bool {
cx.global::<SettingsStore>()
.raw_user_settings()
.get("vim_mode")
== Some(&serde_json::Value::Bool(true))
}
// Consider user intent and default settings
fn choose_completion_range(
completion: &Completion,

View File

@@ -6309,187 +6309,7 @@ async fn test_select_larger_smaller_syntax_node(cx: &mut TestAppContext) {
use mod1::mod2::«{mod3, mod4}ˇ»;
fn fn_1«ˇ(param1: bool, param2: &str)» {
let var1 = "«ˇtext»";
}
"#},
cx,
);
});
}
#[gpui::test]
async fn test_select_larger_smaller_syntax_node_for_string(cx: &mut TestAppContext) {
init_test(cx, |_| {});
let language = Arc::new(Language::new(
LanguageConfig::default(),
Some(tree_sitter_rust::LANGUAGE.into()),
));
let text = r#"
use mod1::mod2::{mod3, mod4};
fn fn_1(param1: bool, param2: &str) {
let var1 = "hello world";
}
"#
.unindent();
let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
editor
.condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
.await;
// Test 1: Cursor on a letter of a string word
editor.update_in(cx, |editor, window, cx| {
editor.change_selections(None, window, cx, |s| {
s.select_display_ranges([
DisplayPoint::new(DisplayRow(3), 17)..DisplayPoint::new(DisplayRow(3), 17)
]);
});
});
editor.update_in(cx, |editor, window, cx| {
assert_text_with_selections(
editor,
indoc! {r#"
use mod1::mod2::{mod3, mod4};
fn fn_1(param1: bool, param2: &str) {
let var1 = "hˇello world";
}
"#},
cx,
);
editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
assert_text_with_selections(
editor,
indoc! {r#"
use mod1::mod2::{mod3, mod4};
fn fn_1(param1: bool, param2: &str) {
let var1 = "«ˇhello» world";
}
"#},
cx,
);
});
// Test 2: Partial selection within a word
editor.update_in(cx, |editor, window, cx| {
editor.change_selections(None, window, cx, |s| {
s.select_display_ranges([
DisplayPoint::new(DisplayRow(3), 17)..DisplayPoint::new(DisplayRow(3), 19)
]);
});
});
editor.update_in(cx, |editor, window, cx| {
assert_text_with_selections(
editor,
indoc! {r#"
use mod1::mod2::{mod3, mod4};
fn fn_1(param1: bool, param2: &str) {
let var1 = "h«elˇ»lo world";
}
"#},
cx,
);
editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
assert_text_with_selections(
editor,
indoc! {r#"
use mod1::mod2::{mod3, mod4};
fn fn_1(param1: bool, param2: &str) {
let var1 = "«ˇhello» world";
}
"#},
cx,
);
});
// Test 3: Complete word already selected
editor.update_in(cx, |editor, window, cx| {
editor.change_selections(None, window, cx, |s| {
s.select_display_ranges([
DisplayPoint::new(DisplayRow(3), 16)..DisplayPoint::new(DisplayRow(3), 21)
]);
});
});
editor.update_in(cx, |editor, window, cx| {
assert_text_with_selections(
editor,
indoc! {r#"
use mod1::mod2::{mod3, mod4};
fn fn_1(param1: bool, param2: &str) {
let var1 = "«helloˇ» world";
}
"#},
cx,
);
editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
assert_text_with_selections(
editor,
indoc! {r#"
use mod1::mod2::{mod3, mod4};
fn fn_1(param1: bool, param2: &str) {
let var1 = "«hello worldˇ»";
}
"#},
cx,
);
});
// Test 4: Selection spanning across words
editor.update_in(cx, |editor, window, cx| {
editor.change_selections(None, window, cx, |s| {
s.select_display_ranges([
DisplayPoint::new(DisplayRow(3), 19)..DisplayPoint::new(DisplayRow(3), 24)
]);
});
});
editor.update_in(cx, |editor, window, cx| {
assert_text_with_selections(
editor,
indoc! {r#"
use mod1::mod2::{mod3, mod4};
fn fn_1(param1: bool, param2: &str) {
let var1 = "hel«lo woˇ»rld";
}
"#},
cx,
);
editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
assert_text_with_selections(
editor,
indoc! {r#"
use mod1::mod2::{mod3, mod4};
fn fn_1(param1: bool, param2: &str) {
let var1 = "«ˇhello world»";
}
"#},
cx,
);
});
// Test 5: Expansion beyond string
editor.update_in(cx, |editor, window, cx| {
editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
assert_text_with_selections(
editor,
indoc! {r#"
use mod1::mod2::{mod3, mod4};
fn fn_1(param1: bool, param2: &str) {
«ˇlet var1 = "hello world";»
«ˇlet var1 = "text";»
}
"#},
cx,

View File

@@ -435,8 +435,6 @@ impl EditorElement {
register_action(editor, window, Editor::stage_and_next);
register_action(editor, window, Editor::unstage_and_next);
register_action(editor, window, Editor::expand_all_diff_hunks);
register_action(editor, window, Editor::go_to_previous_change);
register_action(editor, window, Editor::go_to_next_change);
register_action(editor, window, |editor, action, window, cx| {
if let Some(task) = editor.format(action, window, cx) {
@@ -1007,6 +1005,9 @@ impl EditorElement {
} else {
editor.hide_hovered_link(cx);
hover_at(editor, None, window, cx);
if gutter_hovered {
cx.stop_propagation();
}
}
}

View File

@@ -555,12 +555,12 @@ impl InlayHintCache {
/// Completely forget of certain excerpts that were removed from the multibuffer.
pub(super) fn remove_excerpts(
&mut self,
excerpts_removed: &[ExcerptId],
excerpts_removed: Vec<ExcerptId>,
) -> Option<InlaySplice> {
let mut to_remove = Vec::new();
for excerpt_to_remove in excerpts_removed {
self.update_tasks.remove(excerpt_to_remove);
if let Some(cached_hints) = self.hints.remove(excerpt_to_remove) {
self.update_tasks.remove(&excerpt_to_remove);
if let Some(cached_hints) = self.hints.remove(&excerpt_to_remove) {
let cached_hints = cached_hints.read();
to_remove.extend(cached_hints.ordered_hints.iter().copied());
}

View File

@@ -271,12 +271,12 @@ fn main() {
match judge_result {
Ok(judge_output) => {
const SCORES: [&str; 6] = ["💀", "😭", "😔", "😐", "🙂", "🤩"];
let score: u32 = judge_output.score;
let score_index = (score.min(5)) as usize;
println!(
"{} {}{}",
SCORES[score_index], example.log_prefix, judge_output.score,
SCORES[judge_output.score.min(5) as usize],
example.log_prefix,
judge_output.score,
);
judge_scores.push(judge_output.score);
}
@@ -304,6 +304,7 @@ fn main() {
std::thread::sleep(std::time::Duration::from_secs(2));
// Flush telemetry events before exiting
app_state.client.telemetry().flush_events();
cx.update(|cx| cx.quit())
@@ -329,6 +330,7 @@ async fn run_example(
for round in 0..judge_repetitions {
let judge_result = example.judge(model.clone(), diff.clone(), round, cx).await;
// Log telemetry for this judge result
if let Ok(judge_output) = &judge_result {
let cohort_id = example
.output_file_path
@@ -337,9 +339,6 @@ async fn run_example(
.map(|name| name.to_string_lossy().to_string())
.unwrap_or(chrono::Local::now().format("%Y-%m-%d_%H-%M-%S").to_string());
let path = std::path::Path::new(".");
let commit_id = get_current_commit_id(path).await.unwrap_or_default();
telemetry::event!(
"Agent Eval Completed",
cohort_id = cohort_id,
@@ -354,8 +353,7 @@ async fn run_example(
model_provider = model.provider_id().to_string(),
repository_url = example.base.url.clone(),
repository_revision = example.base.revision.clone(),
diagnostics_summary = run_output.diagnostics,
commit_id = commit_id
diagnostics_summary = run_output.diagnostics
);
}
@@ -526,13 +524,3 @@ pub fn authenticate_model_provider(
let model_provider = model_registry.provider(&provider_id).unwrap();
model_provider.authenticate(cx)
}
pub async fn get_current_commit_id(repo_path: &Path) -> Option<String> {
(run_git(repo_path, &["rev-parse", "HEAD"]).await).ok()
}
pub fn get_current_commit_id_sync(repo_path: &Path) -> String {
futures::executor::block_on(async {
get_current_commit_id(repo_path).await.unwrap_or_default()
})
}

40
crates/evals/Cargo.toml Normal file
View File

@@ -0,0 +1,40 @@
[package]
name = "evals"
description = "Evaluations for Zed's AI features"
version = "0.1.0"
edition.workspace = true
publish.workspace = true
license = "GPL-3.0-or-later"
[lints]
workspace = true
[[bin]]
name = "eval"
path = "src/eval.rs"
[dependencies]
anyhow.workspace = true
clap.workspace = true
client.workspace = true
clock.workspace = true
collections.workspace = true
dap.workspace = true
env_logger.workspace = true
feature_flags.workspace = true
fs.workspace = true
gpui.workspace = true
http_client.workspace = true
language.workspace = true
languages.workspace = true
node_runtime.workspace = true
open_ai.workspace = true
project.workspace = true
reqwest_client.workspace = true
semantic_index.workspace = true
serde.workspace = true
serde_json.workspace = true
settings.workspace = true
smol.workspace = true
util.workspace = true
workspace-hack.workspace = true

5
crates/evals/build.rs Normal file
View File

@@ -0,0 +1,5 @@
fn main() {
if cfg!(target_os = "macos") {
println!("cargo:rustc-env=MACOSX_DEPLOYMENT_TARGET=10.15.7");
}
}

718
crates/evals/src/eval.rs Normal file
View File

@@ -0,0 +1,718 @@
use ::fs::{Fs, RealFs};
use anyhow::Result;
use clap::Parser;
use client::{Client, UserStore};
use clock::RealSystemClock;
use collections::BTreeMap;
use dap::DapRegistry;
use feature_flags::FeatureFlagAppExt as _;
use gpui::{AppContext as _, AsyncApp, BackgroundExecutor, Entity};
use http_client::{HttpClient, Method};
use language::LanguageRegistry;
use node_runtime::NodeRuntime;
use open_ai::OpenAiEmbeddingModel;
use project::Project;
use reqwest_client::ReqwestClient;
use semantic_index::{
EmbeddingProvider, OpenAiEmbeddingProvider, ProjectIndex, SemanticDb, Status,
};
use serde::{Deserialize, Serialize};
use settings::SettingsStore;
use smol::Timer;
use smol::channel::bounded;
use smol::io::AsyncReadExt;
use std::ops::RangeInclusive;
use std::path::PathBuf;
use std::time::Duration;
use std::{
fs,
path::Path,
process::{Stdio, exit},
sync::{
Arc,
atomic::{AtomicUsize, Ordering::SeqCst},
},
};
const CODESEARCH_NET_DIR: &'static str = "target/datasets/code-search-net";
const EVAL_REPOS_DIR: &'static str = "target/datasets/eval-repos";
const EVAL_DB_PATH: &'static str = "target/eval_db";
const SEARCH_RESULT_LIMIT: usize = 8;
const SKIP_EVAL_PATH: &'static str = ".skip_eval";
#[derive(clap::Parser)]
#[command(author, version, about, long_about = None)]
struct Cli {
#[command(subcommand)]
command: Commands,
}
#[derive(clap::Subcommand)]
enum Commands {
Fetch {},
Run {
#[arg(long)]
repo: Option<String>,
},
}
#[derive(Clone, Deserialize, Serialize)]
struct EvaluationProject {
repo: String,
sha: String,
queries: Vec<EvaluationQuery>,
}
#[derive(Clone, Debug, Deserialize, Serialize)]
struct EvaluationQuery {
query: String,
expected_results: Vec<EvaluationSearchResult>,
}
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
struct EvaluationSearchResult {
file: String,
lines: RangeInclusive<u32>,
}
#[derive(Clone, Deserialize, Serialize)]
struct EvaluationProjectOutcome {
repo: String,
sha: String,
queries: Vec<EvaluationQueryOutcome>,
}
#[derive(Clone, Debug, Deserialize, Serialize)]
struct EvaluationQueryOutcome {
repo: String,
query: String,
expected_results: Vec<EvaluationSearchResult>,
actual_results: Vec<EvaluationSearchResult>,
covered_file_count: usize,
overlapped_result_count: usize,
covered_result_count: usize,
total_result_count: usize,
covered_result_indices: Vec<usize>,
}
fn main() -> Result<()> {
let cli = Cli::parse();
env_logger::init();
gpui::Application::headless().run(move |cx| {
let executor = cx.background_executor().clone();
let client = Arc::new(ReqwestClient::user_agent("Zed LLM evals").unwrap());
cx.set_http_client(client.clone());
match cli.command {
Commands::Fetch {} => {
executor
.clone()
.spawn(async move {
if let Err(err) = fetch_evaluation_resources(client, &executor).await {
eprintln!("Error: {}", err);
exit(1);
}
exit(0);
})
.detach();
}
Commands::Run { repo } => {
cx.spawn(async move |cx| {
if let Err(err) = run_evaluation(repo, &executor, cx).await {
eprintln!("Error: {}", err);
exit(1);
}
exit(0);
})
.detach();
}
}
});
Ok(())
}
async fn fetch_evaluation_resources(
http_client: Arc<dyn HttpClient>,
executor: &BackgroundExecutor,
) -> Result<()> {
fetch_code_search_net_resources(&*http_client).await?;
fetch_eval_repos(executor, &*http_client).await?;
Ok(())
}
async fn fetch_code_search_net_resources(http_client: &dyn HttpClient) -> Result<()> {
eprintln!("Fetching CodeSearchNet evaluations...");
let annotations_url = "https://raw.githubusercontent.com/github/CodeSearchNet/master/resources/annotationStore.csv";
let dataset_dir = Path::new(CODESEARCH_NET_DIR);
fs::create_dir_all(&dataset_dir).expect("failed to create CodeSearchNet directory");
// Fetch the annotations CSV, which contains the human-annotated search relevances
let annotations_path = dataset_dir.join("annotations.csv");
let annotations_csv_content = if annotations_path.exists() {
fs::read_to_string(&annotations_path).expect("failed to read annotations")
} else {
let response = http_client
.get(annotations_url, Default::default(), true)
.await
.expect("failed to fetch annotations csv");
let mut body = String::new();
response
.into_body()
.read_to_string(&mut body)
.await
.expect("failed to read annotations.csv response");
fs::write(annotations_path, &body).expect("failed to write annotations.csv");
body
};
// Parse the annotations CSV. Skip over queries with zero relevance.
let rows = annotations_csv_content.lines().filter_map(|line| {
let mut values = line.split(',');
let _language = values.next()?;
let query = values.next()?;
let github_url = values.next()?;
let score = values.next()?;
if score == "0" {
return None;
}
let url_path = github_url.strip_prefix("https://github.com/")?;
let (url_path, hash) = url_path.split_once('#')?;
let (repo_name, url_path) = url_path.split_once("/blob/")?;
let (sha, file_path) = url_path.split_once('/')?;
let line_range = if let Some((start, end)) = hash.split_once('-') {
start.strip_prefix("L")?.parse::<u32>().ok()?..=end.strip_prefix("L")?.parse().ok()?
} else {
let row = hash.strip_prefix("L")?.parse().ok()?;
row..=row
};
Some((repo_name, sha, query, file_path, line_range))
});
// Group the annotations by repo and sha.
let mut evaluations_by_repo = BTreeMap::new();
for (repo_name, sha, query, file_path, lines) in rows {
let evaluation_project = evaluations_by_repo
.entry((repo_name, sha))
.or_insert_with(|| EvaluationProject {
repo: repo_name.to_string(),
sha: sha.to_string(),
queries: Vec::new(),
});
let ix = evaluation_project
.queries
.iter()
.position(|entry| entry.query == query)
.unwrap_or_else(|| {
evaluation_project.queries.push(EvaluationQuery {
query: query.to_string(),
expected_results: Vec::new(),
});
evaluation_project.queries.len() - 1
});
let results = &mut evaluation_project.queries[ix].expected_results;
let result = EvaluationSearchResult {
file: file_path.to_string(),
lines,
};
if !results.contains(&result) {
results.push(result);
}
}
let evaluations = evaluations_by_repo.into_values().collect::<Vec<_>>();
let evaluations_path = dataset_dir.join("evaluations.json");
fs::write(
&evaluations_path,
serde_json::to_vec_pretty(&evaluations).unwrap(),
)
.unwrap();
eprintln!(
"Fetched CodeSearchNet evaluations into {}",
evaluations_path.display()
);
Ok(())
}
#[derive(Default, Debug)]
struct Counts {
covered_results: usize,
overlapped_results: usize,
covered_files: usize,
total_results: usize,
}
async fn run_evaluation(
only_repo: Option<String>,
executor: &BackgroundExecutor,
cx: &mut AsyncApp,
) -> Result<()> {
let mut http_client = None;
cx.update(|cx| {
let mut store = SettingsStore::new(cx);
store
.set_default_settings(settings::default_settings().as_ref(), cx)
.unwrap();
cx.set_global(store);
client::init_settings(cx);
language::init(cx);
Project::init_settings(cx);
http_client = Some(cx.http_client());
cx.update_flags(false, vec![]);
})
.unwrap();
let http_client = http_client.unwrap();
let dataset_dir = Path::new(CODESEARCH_NET_DIR);
let evaluations_path = dataset_dir.join("evaluations.json");
let repos_dir = Path::new(EVAL_REPOS_DIR);
let db_path = Path::new(EVAL_DB_PATH);
let api_key = std::env::var("OPENAI_API_KEY").unwrap();
let fs = Arc::new(RealFs::new(None, cx.background_executor().clone())) as Arc<dyn Fs>;
let clock = Arc::new(RealSystemClock);
let client = cx
.update(|cx| {
Client::new(
clock,
Arc::new(http_client::HttpClientWithUrl::new(
http_client.clone(),
"https://zed.dev",
None,
)),
cx,
)
})
.unwrap();
let user_store = cx.new(|cx| UserStore::new(client.clone(), cx)).unwrap();
let node_runtime = NodeRuntime::unavailable();
let evaluations = fs::read(&evaluations_path).expect("failed to read evaluations.json");
let evaluations: Vec<EvaluationProject> = serde_json::from_slice(&evaluations).unwrap();
let embedding_provider = Arc::new(OpenAiEmbeddingProvider::new(
http_client.clone(),
OpenAiEmbeddingModel::TextEmbedding3Small,
open_ai::OPEN_AI_API_URL.to_string(),
api_key,
));
let language_registry = Arc::new(LanguageRegistry::new(executor.clone()));
let debug_adapters = Arc::new(DapRegistry::default());
cx.update(|cx| languages::init(language_registry.clone(), node_runtime.clone(), cx))
.unwrap();
let mut counts = Counts::default();
eprint!("Running evals.");
let mut failures = Vec::new();
for evaluation_project in evaluations {
if only_repo
.as_ref()
.map_or(false, |only_repo| only_repo != &evaluation_project.repo)
{
continue;
}
eprint!("\r\x1B[2K");
eprint!(
"Running evals. {}/{} covered. {}/{} overlapped. {}/{} files captured. Project: {}...",
counts.covered_results,
counts.total_results,
counts.overlapped_results,
counts.total_results,
counts.covered_files,
counts.total_results,
evaluation_project.repo
);
let repo_dir = repos_dir.join(&evaluation_project.repo);
if !repo_dir.exists() || repo_dir.join(SKIP_EVAL_PATH).exists() {
eprintln!("Skipping {}: directory not found", evaluation_project.repo);
continue;
}
let repo_db_path =
db_path.join(format!("{}.db", evaluation_project.repo.replace('/', "_")));
let project = cx
.update(|cx| {
Project::local(
client.clone(),
node_runtime.clone(),
user_store.clone(),
language_registry.clone(),
debug_adapters.clone(),
fs.clone(),
None,
cx,
)
})
.unwrap();
let repo = evaluation_project.repo.clone();
if let Err(err) = run_eval_project(
evaluation_project,
&user_store,
repo_db_path,
&repo_dir,
&mut counts,
project,
embedding_provider.clone(),
fs.clone(),
cx,
)
.await
{
eprintln!("{repo} eval failed with error: {:?}", err);
failures.push((repo, err));
}
}
eprintln!(
"Running evals. {}/{} covered. {}/{} overlapped. {}/{} files captured. {} failed.",
counts.covered_results,
counts.total_results,
counts.overlapped_results,
counts.total_results,
counts.covered_files,
counts.total_results,
failures.len(),
);
if failures.is_empty() {
Ok(())
} else {
eprintln!("Failures:\n");
for (index, (repo, failure)) in failures.iter().enumerate() {
eprintln!("Failure #{} - {repo}\n{:?}", index + 1, failure);
}
Err(anyhow::anyhow!("Some evals failed."))
}
}
async fn run_eval_project(
evaluation_project: EvaluationProject,
user_store: &Entity<UserStore>,
repo_db_path: PathBuf,
repo_dir: &Path,
counts: &mut Counts,
project: Entity<Project>,
embedding_provider: Arc<dyn EmbeddingProvider>,
fs: Arc<dyn Fs>,
cx: &mut AsyncApp,
) -> Result<(), anyhow::Error> {
let mut semantic_index = SemanticDb::new(repo_db_path, embedding_provider, cx).await?;
let (worktree, _) = project
.update(cx, |project, cx| {
project.find_or_create_worktree(repo_dir, true, cx)
})?
.await?;
worktree
.update(cx, |worktree, _| {
worktree.as_local().unwrap().scan_complete()
})?
.await;
let project_index = cx.update(|cx| semantic_index.create_project_index(project.clone(), cx))?;
wait_for_indexing_complete(&project_index, cx, Some(Duration::from_secs(120))).await;
for query in evaluation_project.queries {
let results = {
// Retry search up to 3 times in case of timeout, network failure, etc.
let mut retries_remaining = 3;
let mut result;
loop {
match cx.update(|cx| {
let project_index = project_index.read(cx);
project_index.search(vec![query.query.clone()], SEARCH_RESULT_LIMIT, cx)
}) {
Ok(task) => match task.await {
Ok(answer) => {
result = Ok(answer);
break;
}
Err(err) => {
result = Err(err);
}
},
Err(err) => {
result = Err(err);
}
}
if retries_remaining > 0 {
eprintln!(
"Retrying search after it failed on query {:?} with {:?}",
query, result
);
retries_remaining -= 1;
} else {
eprintln!(
"Ran out of retries; giving up on search which failed on query {:?} with {:?}",
query, result
);
break;
}
}
SemanticDb::load_results(result?, &fs.clone(), &cx).await?
};
let mut project_covered_result_count = 0;
let mut project_overlapped_result_count = 0;
let mut project_covered_file_count = 0;
let mut covered_result_indices = Vec::new();
for expected_result in &query.expected_results {
let mut file_matched = false;
let mut range_overlapped = false;
let mut range_covered = false;
for (ix, result) in results.iter().enumerate() {
if result.path.as_ref() == Path::new(&expected_result.file) {
file_matched = true;
let start_matched = result.row_range.contains(expected_result.lines.start());
let end_matched = result.row_range.contains(expected_result.lines.end());
if start_matched || end_matched {
range_overlapped = true;
}
if start_matched && end_matched {
range_covered = true;
covered_result_indices.push(ix);
break;
}
}
}
if range_covered {
project_covered_result_count += 1
};
if range_overlapped {
project_overlapped_result_count += 1
};
if file_matched {
project_covered_file_count += 1
};
}
let outcome_repo = evaluation_project.repo.clone();
let query_results = EvaluationQueryOutcome {
repo: outcome_repo,
query: query.query,
total_result_count: query.expected_results.len(),
covered_result_count: project_covered_result_count,
overlapped_result_count: project_overlapped_result_count,
covered_file_count: project_covered_file_count,
expected_results: query.expected_results,
actual_results: results
.iter()
.map(|result| EvaluationSearchResult {
file: result.path.to_string_lossy().to_string(),
lines: result.row_range.clone(),
})
.collect(),
covered_result_indices,
};
counts.overlapped_results += query_results.overlapped_result_count;
counts.covered_results += query_results.covered_result_count;
counts.covered_files += query_results.covered_file_count;
counts.total_results += query_results.total_result_count;
println!("{}", serde_json::to_string(&query_results)?);
}
user_store.update(cx, |_, _| {
drop(semantic_index);
drop(project);
drop(worktree);
drop(project_index);
})
}
async fn wait_for_indexing_complete(
project_index: &Entity<ProjectIndex>,
cx: &mut AsyncApp,
timeout: Option<Duration>,
) {
let (tx, rx) = bounded(1);
let subscription = cx.update(|cx| {
cx.subscribe(project_index, move |_, event, _| {
if let Status::Idle = event {
let _ = tx.try_send(*event);
}
})
});
let result = match timeout {
Some(timeout_duration) => {
smol::future::or(
async {
rx.recv().await.map_err(|_| ())?;
Ok(())
},
async {
Timer::after(timeout_duration).await;
Err(())
},
)
.await
}
None => rx.recv().await.map(|_| ()).map_err(|_| ()),
};
match result {
Ok(_) => (),
Err(_) => {
if let Some(timeout) = timeout {
eprintln!("Timeout: Indexing did not complete within {:?}", timeout);
}
}
}
drop(subscription);
}
async fn fetch_eval_repos(
executor: &BackgroundExecutor,
http_client: &dyn HttpClient,
) -> Result<()> {
let dataset_dir = Path::new(CODESEARCH_NET_DIR);
let evaluations_path = dataset_dir.join("evaluations.json");
let repos_dir = Path::new(EVAL_REPOS_DIR);
let evaluations = fs::read(&evaluations_path).expect("failed to read evaluations.json");
let evaluations: Vec<EvaluationProject> = serde_json::from_slice(&evaluations).unwrap();
eprintln!("Fetching evaluation repositories...");
executor
.scoped(move |scope| {
let done_count = Arc::new(AtomicUsize::new(0));
let len = evaluations.len();
for chunk in evaluations.chunks(evaluations.len() / 8) {
let chunk = chunk.to_vec();
let done_count = done_count.clone();
scope.spawn(async move {
for EvaluationProject { repo, sha, .. } in chunk {
eprint!(
"\rFetching evaluation repositories ({}/{})...",
done_count.load(SeqCst),
len,
);
fetch_eval_repo(repo, sha, repos_dir, http_client).await;
done_count.fetch_add(1, SeqCst);
}
});
}
})
.await;
Ok(())
}
async fn fetch_eval_repo(
repo: String,
sha: String,
repos_dir: &Path,
http_client: &dyn HttpClient,
) {
let Some((owner, repo_name)) = repo.split_once('/') else {
return;
};
let repo_dir = repos_dir.join(owner).join(repo_name);
fs::create_dir_all(&repo_dir).unwrap();
let skip_eval_path = repo_dir.join(SKIP_EVAL_PATH);
if skip_eval_path.exists() {
return;
}
if let Ok(head_content) = fs::read_to_string(&repo_dir.join(".git").join("HEAD")) {
if head_content.trim() == sha {
return;
}
}
let repo_response = http_client
.send(
http_client::Request::builder()
.method(Method::HEAD)
.uri(format!("https://github.com/{}", repo))
.body(Default::default())
.expect(""),
)
.await
.expect("failed to check github repo");
if !repo_response.status().is_success() && !repo_response.status().is_redirection() {
fs::write(&skip_eval_path, "").unwrap();
eprintln!(
"Repo {repo} is no longer public ({:?}). Skipping",
repo_response.status()
);
return;
}
if !repo_dir.join(".git").exists() {
let init_output = util::command::new_std_command("git")
.current_dir(&repo_dir)
.args(&["init"])
.output()
.unwrap();
if !init_output.status.success() {
eprintln!(
"Failed to initialize git repository for {}: {}",
repo,
String::from_utf8_lossy(&init_output.stderr)
);
return;
}
}
let url = format!("https://github.com/{}.git", repo);
util::command::new_std_command("git")
.current_dir(&repo_dir)
.args(&["remote", "add", "-f", "origin", &url])
.stdin(Stdio::null())
.output()
.unwrap();
let fetch_output = util::command::new_std_command("git")
.current_dir(&repo_dir)
.args(&["fetch", "--depth", "1", "origin", &sha])
.stdin(Stdio::null())
.output()
.unwrap();
if !fetch_output.status.success() {
eprintln!(
"Failed to fetch {} for {}: {}",
sha,
repo,
String::from_utf8_lossy(&fetch_output.stderr)
);
return;
}
let checkout_output = util::command::new_std_command("git")
.current_dir(&repo_dir)
.args(&["checkout", &sha])
.output()
.unwrap();
if !checkout_output.status.success() {
eprintln!(
"Failed to checkout {} for {}: {}",
sha,
repo,
String::from_utf8_lossy(&checkout_output.stderr)
);
}
}

View File

@@ -84,11 +84,6 @@ impl FeatureFlag for ZedPro {
const NAME: &'static str = "zed-pro";
}
pub struct ZedProWebSearchTool {}
impl FeatureFlag for ZedProWebSearchTool {
const NAME: &'static str = "zed-pro-web-search-tool";
}
pub struct NotebookFeatureFlag;
impl FeatureFlag for NotebookFeatureFlag {

View File

@@ -599,33 +599,11 @@ impl GitPanel {
}
pub fn entry_by_path(&self, path: &RepoPath) -> Option<usize> {
fn binary_search<F>(mut low: usize, mut high: usize, is_target: F) -> Option<usize>
where
F: Fn(usize) -> std::cmp::Ordering,
{
while low < high {
let mid = low + (high - low) / 2;
match is_target(mid) {
std::cmp::Ordering::Equal => return Some(mid),
std::cmp::Ordering::Less => low = mid + 1,
std::cmp::Ordering::Greater => high = mid,
}
}
None
}
if self.conflicted_count > 0 {
let conflicted_start = 1;
if let Some(ix) = binary_search(
conflicted_start,
conflicted_start + self.conflicted_count,
|ix| {
self.entries[ix]
.status_entry()
.unwrap()
.repo_path
.cmp(&path)
},
) {
if let Ok(ix) = self.entries[conflicted_start..conflicted_start + self.conflicted_count]
.binary_search_by(|entry| entry.status_entry().unwrap().repo_path.cmp(&path))
{
return Some(ix);
}
}
@@ -635,14 +613,8 @@ impl GitPanel {
} else {
0
} + 1;
if let Some(ix) =
binary_search(tracked_start, tracked_start + self.tracked_count, |ix| {
self.entries[ix]
.status_entry()
.unwrap()
.repo_path
.cmp(&path)
})
if let Ok(ix) = self.entries[tracked_start..tracked_start + self.tracked_count]
.binary_search_by(|entry| entry.status_entry().unwrap().repo_path.cmp(&path))
{
return Some(ix);
}
@@ -657,14 +629,8 @@ impl GitPanel {
} else {
0
} + 1;
if let Some(ix) =
binary_search(untracked_start, untracked_start + self.new_count, |ix| {
self.entries[ix]
.status_entry()
.unwrap()
.repo_path
.cmp(&path)
})
if let Ok(ix) = self.entries[untracked_start..untracked_start + self.new_count]
.binary_search_by(|entry| entry.status_entry().unwrap().repo_path.cmp(&path))
{
return Some(ix);
}

View File

@@ -125,7 +125,7 @@ pub struct GenerateContentRequest {
#[serde(default, skip_serializing_if = "String::is_empty")]
pub model: String,
pub contents: Vec<Content>,
pub system_instruction: Option<SystemInstruction>,
pub system_instructions: Option<SystemInstructions>,
pub generation_config: Option<GenerationConfig>,
pub safety_settings: Option<Vec<SafetySetting>>,
#[serde(skip_serializing_if = "Option::is_none")]
@@ -162,7 +162,7 @@ pub struct Content {
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct SystemInstruction {
pub struct SystemInstructions {
pub parts: Vec<Part>,
}

View File

@@ -97,10 +97,7 @@ pub struct TokenUsage {
impl TokenUsage {
pub fn total_tokens(&self) -> u32 {
self.input_tokens
+ self.output_tokens
+ self.cache_read_input_tokens
+ self.cache_creation_input_tokens
self.input_tokens + self.output_tokens
}
}

View File

@@ -142,27 +142,6 @@ impl fmt::Display for MaxMonthlySpendReachedError {
}
}
#[derive(Error, Debug)]
pub struct ModelRequestLimitReachedError {
pub plan: Plan,
}
impl fmt::Display for ModelRequestLimitReachedError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
let message = match self.plan {
Plan::Free => "Model request limit reached. Upgrade to Zed Pro for more requests.",
Plan::ZedPro => {
"Model request limit reached. Upgrade to usage-based billing for more requests."
}
Plan::ZedProTrial => {
"Model request limit reached. Upgrade to Zed Pro for more requests."
}
};
write!(f, "{message}")
}
}
#[derive(Clone, Default)]
pub struct LlmApiToken(Arc<RwLock<Option<String>>>);

View File

@@ -546,6 +546,7 @@ impl PickerDelegate for LanguageModelPickerDelegate {
use feature_flags::FeatureFlagAppExt;
let plan = proto::Plan::ZedPro;
let is_trial = false;
Some(
h_flex()
@@ -557,6 +558,7 @@ impl PickerDelegate for LanguageModelPickerDelegate {
.justify_between()
.when(cx.has_flag::<ZedPro>(), |this| {
this.child(match plan {
// Already a Zed Pro subscriber
Plan::ZedPro => Button::new("zed-pro", "Zed Pro")
.icon(IconName::ZedAssistant)
.icon_size(IconSize::Small)
@@ -566,9 +568,10 @@ impl PickerDelegate for LanguageModelPickerDelegate {
window
.dispatch_action(Box::new(zed_actions::OpenAccountSettings), cx)
}),
Plan::Free | Plan::ZedProTrial => Button::new(
// Free user
Plan::Free => Button::new(
"try-pro",
if plan == Plan::ZedProTrial {
if is_trial {
"Upgrade to Pro"
} else {
"Try Pro"

View File

@@ -53,7 +53,6 @@ tokio = { workspace = true, features = ["rt", "rt-multi-thread"] }
ui.workspace = true
util.workspace = true
workspace-hack.workspace = true
zed_llm_client.workspace = true
[dev-dependencies]
editor = { workspace = true, features = ["test-support"] }

View File

@@ -705,12 +705,12 @@ pub fn map_to_language_model_completion_events(
update_usage(&mut state.usage, &message.usage);
return Some((
vec![
Ok(LanguageModelCompletionEvent::UsageUpdate(convert_usage(
&state.usage,
))),
Ok(LanguageModelCompletionEvent::StartMessage {
message_id: message.id,
}),
Ok(LanguageModelCompletionEvent::UsageUpdate(convert_usage(
&state.usage,
))),
],
state,
));

View File

@@ -1,6 +1,9 @@
use anthropic::{AnthropicError, AnthropicModelMode, parse_prompt_too_long};
use anyhow::{Result, anyhow};
use client::{Client, UserStore, zed_urls};
use client::{
Client, EXPIRED_LLM_TOKEN_HEADER_NAME, MAX_LLM_MONTHLY_SPEND_REACHED_HEADER_NAME,
PerformCompletionParams, UserStore, zed_urls,
};
use collections::BTreeMap;
use feature_flags::{FeatureFlagAppExt, LlmClosedBeta, ZedPro};
use futures::{
@@ -13,20 +16,18 @@ use language_model::{
AuthenticateError, CloudModel, LanguageModel, LanguageModelCacheConfiguration, LanguageModelId,
LanguageModelKnownError, LanguageModelName, LanguageModelProviderId, LanguageModelProviderName,
LanguageModelProviderState, LanguageModelProviderTosView, LanguageModelRequest,
LanguageModelToolSchemaFormat, ModelRequestLimitReachedError, RateLimiter,
ZED_CLOUD_PROVIDER_ID,
LanguageModelToolSchemaFormat, RateLimiter, ZED_CLOUD_PROVIDER_ID,
};
use language_model::{
LanguageModelAvailability, LanguageModelCompletionEvent, LanguageModelProvider, LlmApiToken,
MaxMonthlySpendReachedError, PaymentRequiredError, RefreshLlmTokenListener,
};
use proto::Plan;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize, de::DeserializeOwned};
use serde_json::value::RawValue;
use settings::{Settings, SettingsStore};
use smol::Timer;
use smol::io::{AsyncReadExt, BufReader};
use std::str::FromStr as _;
use std::{
sync::{Arc, LazyLock},
time::Duration,
@@ -34,11 +35,6 @@ use std::{
use strum::IntoEnumIterator;
use thiserror::Error;
use ui::{TintColor, prelude::*};
use zed_llm_client::{
CURRENT_PLAN_HEADER_NAME, CompletionBody, EXPIRED_LLM_TOKEN_HEADER_NAME,
MAX_LLM_MONTHLY_SPEND_REACHED_HEADER_NAME, MODEL_REQUESTS_RESOURCE_HEADER_VALUE,
SUBSCRIPTION_LIMIT_RESOURCE_HEADER_NAME,
};
use crate::AllLanguageModelSettings;
use crate::provider::anthropic::{count_anthropic_tokens, into_anthropic};
@@ -517,7 +513,7 @@ impl CloudLanguageModel {
async fn perform_llm_completion(
client: Arc<Client>,
llm_api_token: LlmApiToken,
body: CompletionBody,
body: PerformCompletionParams,
) -> Result<Response<AsyncBody>> {
let http_client = &client.http_client();
@@ -555,33 +551,6 @@ impl CloudLanguageModel {
.is_some()
{
return Err(anyhow!(MaxMonthlySpendReachedError));
} else if status == StatusCode::FORBIDDEN
&& response
.headers()
.get(SUBSCRIPTION_LIMIT_RESOURCE_HEADER_NAME)
.is_some()
{
if let Some(MODEL_REQUESTS_RESOURCE_HEADER_VALUE) = response
.headers()
.get(SUBSCRIPTION_LIMIT_RESOURCE_HEADER_NAME)
.and_then(|resource| resource.to_str().ok())
{
if let Some(plan) = response
.headers()
.get(CURRENT_PLAN_HEADER_NAME)
.and_then(|plan| plan.to_str().ok())
.and_then(|plan| zed_llm_client::Plan::from_str(plan).ok())
{
let plan = match plan {
zed_llm_client::Plan::Free => Plan::Free,
zed_llm_client::Plan::ZedPro => Plan::ZedPro,
zed_llm_client::Plan::ZedProTrial => Plan::ZedProTrial,
};
return Err(anyhow!(ModelRequestLimitReachedError { plan }));
}
}
return Err(anyhow!("Forbidden"));
} else if status.as_u16() >= 500 && status.as_u16() < 600 {
// If we encounter an error in the 500 range, retry after a delay.
// We've seen at least these in the wild from API providers:
@@ -725,10 +694,12 @@ impl LanguageModel for CloudLanguageModel {
let response = Self::perform_llm_completion(
client.clone(),
llm_api_token,
CompletionBody {
provider: zed_llm_client::LanguageModelProvider::Anthropic,
PerformCompletionParams {
provider: client::LanguageModelProvider::Anthropic,
model: request.model.clone(),
provider_request: serde_json::to_value(&request)?,
provider_request: RawValue::from_string(serde_json::to_string(
&request,
)?)?,
},
)
.await
@@ -764,10 +735,12 @@ impl LanguageModel for CloudLanguageModel {
let response = Self::perform_llm_completion(
client.clone(),
llm_api_token,
CompletionBody {
provider: zed_llm_client::LanguageModelProvider::OpenAi,
PerformCompletionParams {
provider: client::LanguageModelProvider::OpenAi,
model: request.model.clone(),
provider_request: serde_json::to_value(&request)?,
provider_request: RawValue::from_string(serde_json::to_string(
&request,
)?)?,
},
)
.await?;
@@ -787,10 +760,12 @@ impl LanguageModel for CloudLanguageModel {
let response = Self::perform_llm_completion(
client.clone(),
llm_api_token,
CompletionBody {
provider: zed_llm_client::LanguageModelProvider::Google,
PerformCompletionParams {
provider: client::LanguageModelProvider::Google,
model: request.model.clone(),
provider_request: serde_json::to_value(&request)?,
provider_request: RawValue::from_string(serde_json::to_string(
&request,
)?)?,
},
)
.await?;

View File

@@ -4,7 +4,7 @@ use credentials_provider::CredentialsProvider;
use editor::{Editor, EditorElement, EditorStyle};
use futures::{FutureExt, Stream, StreamExt, future::BoxFuture};
use google_ai::{
FunctionDeclaration, GenerateContentResponse, Part, SystemInstruction, UsageMetadata,
FunctionDeclaration, GenerateContentResponse, Part, SystemInstructions, UsageMetadata,
};
use gpui::{
AnyView, App, AsyncApp, Context, Entity, FontStyle, Subscription, Task, TextStyle, WhiteSpace,
@@ -405,7 +405,7 @@ pub fn into_google(
.map_or(false, |msg| matches!(msg.role, Role::System))
{
let message = request.messages.remove(0);
Some(SystemInstruction {
Some(SystemInstructions {
parts: map_content(message.content),
})
} else {
@@ -414,7 +414,7 @@ pub fn into_google(
google_ai::GenerateContentRequest {
model,
system_instruction: system_instructions,
system_instructions,
contents: request
.messages
.into_iter()

View File

@@ -206,12 +206,12 @@ impl Render for KeyContextView {
.mt_4()
.gap_4()
.child(
Button::new("open_documentation", "Open Documentation")
Button::new("default", "Open Documentation")
.style(ButtonStyle::Filled)
.on_click(|_, _, cx| cx.open_url("https://zed.dev/docs/key-bindings")),
)
.child(
Button::new("view_default_keymap", "View default keymap")
Button::new("default", "View default keymap")
.style(ButtonStyle::Filled)
.key_binding(ui::KeyBinding::for_action(
&zed_actions::OpenDefaultKeymap,
@@ -219,14 +219,16 @@ impl Render for KeyContextView {
cx
))
.on_click(|_, window, cx| {
window.dispatch_action(workspace::SplitRight.boxed_clone(), cx);
window.dispatch_action(zed_actions::OpenDefaultKeymap.boxed_clone(), cx);
}),
)
.child(
Button::new("edit_your_keymap", "Edit your keymap")
Button::new("default", "Edit your keymap")
.style(ButtonStyle::Filled)
.key_binding(ui::KeyBinding::for_action(&zed_actions::OpenKeymap, window, cx))
.on_click(|_, window, cx| {
window.dispatch_action(workspace::SplitRight.boxed_clone(), cx);
window.dispatch_action(zed_actions::OpenKeymap.boxed_clone(), cx);
}),
),

View File

@@ -39,9 +39,7 @@ pub(crate) mod m_2025_03_29 {
}
pub(crate) mod m_2025_04_15 {
mod keymap;
mod settings;
pub(crate) use keymap::KEYMAP_PATTERNS;
pub(crate) use settings::SETTINGS_PATTERNS;
}

View File

@@ -1,31 +0,0 @@
use collections::HashMap;
use std::{ops::Range, sync::LazyLock};
use tree_sitter::{Query, QueryMatch};
use crate::MigrationPatterns;
use crate::patterns::KEYMAP_ACTION_STRING_PATTERN;
pub const KEYMAP_PATTERNS: MigrationPatterns =
&[(KEYMAP_ACTION_STRING_PATTERN, replace_string_action)];
fn replace_string_action(
contents: &str,
mat: &QueryMatch,
query: &Query,
) -> Option<(Range<usize>, String)> {
let action_name_ix = query.capture_index_for_name("action_name")?;
let action_name_node = mat.nodes_for_capture_index(action_name_ix).next()?;
let action_name_range = action_name_node.byte_range();
let action_name = contents.get(action_name_range.clone())?;
if let Some(new_action_name) = STRING_REPLACE.get(&action_name) {
return Some((action_name_range, new_action_name.to_string()));
}
None
}
/// "ctrl-k ctrl-1": "inline_completion::ToggleMenu" -> "edit_prediction::ToggleMenu"
static STRING_REPLACE: LazyLock<HashMap<&str, &str>> = LazyLock::new(|| {
HashMap::from_iter([("outline_panel::Open", "outline_panel::OpenSelectedEntry")])
});

View File

@@ -98,10 +98,6 @@ pub fn migrate_keymap(text: &str) -> Result<Option<String>> {
migrations::m_2025_03_06::KEYMAP_PATTERNS,
&KEYMAP_QUERY_2025_03_06,
),
(
migrations::m_2025_04_15::KEYMAP_PATTERNS,
&KEYMAP_QUERY_2025_04_15,
),
];
run_migrations(text, migrations)
}
@@ -180,10 +176,6 @@ define_query!(
KEYMAP_QUERY_2025_03_06,
migrations::m_2025_03_06::KEYMAP_PATTERNS
);
define_query!(
KEYMAP_QUERY_2025_04_15,
migrations::m_2025_04_15::KEYMAP_PATTERNS
);
// settings
define_query!(

View File

@@ -71,7 +71,7 @@ impl Anchor {
if self_excerpt_id == ExcerptId::min() || self_excerpt_id == ExcerptId::max() {
return Ordering::Equal;
}
if let Some(excerpt) = snapshot.excerpt(self_excerpt_id) {
if let Some(excerpt) = snapshot.excerpt(self.excerpt_id) {
let text_cmp = self.text_anchor.cmp(&other.text_anchor, &excerpt.buffer);
if text_cmp.is_ne() {
return text_cmp;

View File

@@ -5170,7 +5170,6 @@ impl MultiBufferSnapshot {
excerpt_id: ExcerptId,
text_anchor: text::Anchor,
) -> Option<Anchor> {
let excerpt_id = self.latest_excerpt_id(excerpt_id);
let locator = self.excerpt_locator_for_id(excerpt_id);
let mut cursor = self.excerpts.cursor::<Option<&Locator>>(&());
cursor.seek(locator, Bias::Left, &());
@@ -6042,7 +6041,7 @@ impl MultiBufferSnapshot {
return &entry.locator;
}
}
panic!("invalid excerpt id {id:?}")
panic!("invalid excerpt id {:?}", id)
}
}

View File

@@ -70,7 +70,7 @@ actions!(
ExpandAllEntries,
ExpandSelectedEntry,
FoldDirectory,
OpenSelectedEntry,
Open,
RevealInFileManager,
SelectParent,
ToggleActiveEditorPin,
@@ -922,12 +922,7 @@ impl OutlinePanel {
self.update_cached_entries(None, window, cx);
}
fn open_selected_entry(
&mut self,
_: &OpenSelectedEntry,
window: &mut Window,
cx: &mut Context<Self>,
) {
fn open(&mut self, _: &Open, window: &mut Window, cx: &mut Context<Self>) {
if self.filter_editor.focus_handle(cx).is_focused(window) {
cx.propagate()
} else if let Some(selected_entry) = self.selected_entry().cloned() {
@@ -4911,7 +4906,7 @@ impl Render for OutlinePanel {
}
}))
.key_context(self.dispatch_context(window, cx))
.on_action(cx.listener(Self::open_selected_entry))
.on_action(cx.listener(Self::open))
.on_action(cx.listener(Self::cancel))
.on_action(cx.listener(Self::select_next))
.on_action(cx.listener(Self::select_previous))
@@ -5682,7 +5677,7 @@ mod tests {
});
outline_panel.update_in(cx, |outline_panel, window, cx| {
outline_panel.open_selected_entry(&OpenSelectedEntry, window, cx);
outline_panel.open(&Open, window, cx);
});
outline_panel.update(cx, |_outline_panel, cx| {
assert_eq!(
@@ -5857,7 +5852,7 @@ mod tests {
outline_panel.update_in(cx, |outline_panel, window, cx| {
outline_panel.select_previous(&SelectPrevious, window, cx);
outline_panel.open_selected_entry(&OpenSelectedEntry, window, cx);
outline_panel.open(&Open, window, cx);
});
cx.executor()
.advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
@@ -5881,7 +5876,7 @@ mod tests {
outline_panel.update_in(cx, |outline_panel, window, cx| {
outline_panel.select_next(&SelectNext, window, cx);
outline_panel.open_selected_entry(&OpenSelectedEntry, window, cx);
outline_panel.open(&Open, window, cx);
});
cx.executor()
.advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
@@ -5902,7 +5897,7 @@ mod tests {
});
outline_panel.update_in(cx, |outline_panel, window, cx| {
outline_panel.open_selected_entry(&OpenSelectedEntry, window, cx);
outline_panel.open(&Open, window, cx);
});
cx.executor()
.advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));

View File

@@ -18,7 +18,7 @@ use text::{Point, PointUtf16};
use crate::{Project, ProjectPath, buffer_store::BufferStore, worktree_store::WorktreeStore};
mod breakpoints_in_file {
use language::{BufferEvent, DiskState};
use language::BufferEvent;
use super::*;
@@ -32,9 +32,8 @@ mod breakpoints_in_file {
impl BreakpointsInFile {
pub(super) fn new(buffer: Entity<Buffer>, cx: &mut Context<BreakpointStore>) -> Self {
let subscription = Arc::from(cx.subscribe(
&buffer,
|breakpoint_store, buffer, event, cx| match event {
let subscription =
Arc::from(cx.subscribe(&buffer, |_, buffer, event, cx| match event {
BufferEvent::Saved => {
if let Some(abs_path) = BreakpointStore::abs_path_from_buffer(&buffer, cx) {
cx.emit(BreakpointStoreEvent::BreakpointsUpdated(
@@ -43,44 +42,8 @@ mod breakpoints_in_file {
));
}
}
BufferEvent::FileHandleChanged => {
let entity_id = buffer.entity_id();
if buffer.read(cx).file().is_none_or(|f| f.disk_state() == DiskState::Deleted) {
breakpoint_store.breakpoints.retain(|_, breakpoints_in_file| {
breakpoints_in_file.buffer.entity_id() != entity_id
});
cx.notify();
return;
}
if let Some(abs_path) = BreakpointStore::abs_path_from_buffer(&buffer, cx) {
if breakpoint_store.breakpoints.contains_key(&abs_path) {
return;
}
if let Some(old_path) = breakpoint_store
.breakpoints
.iter()
.find(|(_, in_file)| in_file.buffer.entity_id() == entity_id)
.map(|values| values.0)
.cloned()
{
let Some(breakpoints_in_file) =
breakpoint_store.breakpoints.remove(&old_path) else {
log::error!("Couldn't get breakpoints in file from old path during buffer rename handling");
return;
};
breakpoint_store.breakpoints.insert(abs_path, breakpoints_in_file);
cx.notify();
}
}
}
_ => {}
},
));
}));
BreakpointsInFile {
buffer,

View File

@@ -792,48 +792,10 @@ fn create_new_session(
this.update(cx, |_, cx| {
cx.subscribe(
&session,
move |this: &mut DapStore, session, event: &SessionStateEvent, cx| match event {
move |this: &mut DapStore, _, event: &SessionStateEvent, cx| match event {
SessionStateEvent::Shutdown => {
this.shutdown_session(session_id, cx).detach_and_log_err(cx);
}
SessionStateEvent::Restart => {
let Some((config, binary)) = session.read_with(cx, |session, _| {
session
.configuration()
.map(|config| (config, session.binary().clone()))
}) else {
log::error!("Failed to get debug config from session");
return;
};
let mut curr_session = session;
while let Some(parent_id) = curr_session.read(cx).parent_id() {
if let Some(parent_session) = this.sessions.get(&parent_id).cloned() {
curr_session = parent_session;
} else {
log::error!("Failed to get parent session from parent session id");
break;
}
}
let session_id = curr_session.read(cx).session_id();
let task = curr_session.update(cx, |session, cx| session.shutdown(cx));
cx.spawn(async move |this, cx| {
task.await;
this.update(cx, |this, cx| {
this.sessions.remove(&session_id);
this.new_session(binary, config, None, cx)
})?
.1
.await?;
anyhow::Ok(())
})
.detach_and_log_err(cx);
}
},
)
.detach();

View File

@@ -397,7 +397,6 @@ impl LocalMode {
self.definition.initialize_args.clone().unwrap_or(json!({})),
&mut raw.configuration,
);
// Of relevance: https://github.com/microsoft/vscode/issues/4902#issuecomment-368583522
let launch = match raw.request {
dap::StartDebuggingRequestArgumentsRequest::Launch => self.request(
@@ -685,9 +684,8 @@ pub enum SessionEvent {
Threads,
}
pub(super) enum SessionStateEvent {
pub(crate) enum SessionStateEvent {
Shutdown,
Restart,
}
impl EventEmitter<SessionEvent> for Session {}
@@ -1364,18 +1362,6 @@ impl Session {
&self.loaded_sources
}
fn fallback_to_manual_restart(
&mut self,
res: Result<()>,
cx: &mut Context<Self>,
) -> Option<()> {
if res.log_err().is_none() {
cx.emit(SessionStateEvent::Restart);
return None;
}
Some(())
}
fn empty_response(&mut self, res: Result<()>, _cx: &mut Context<Self>) -> Option<()> {
res.log_err()?;
Some(())
@@ -1435,17 +1421,26 @@ impl Session {
}
pub fn restart(&mut self, args: Option<Value>, cx: &mut Context<Self>) {
if self.capabilities.supports_restart_request.unwrap_or(false) && !self.is_terminated() {
if self.capabilities.supports_restart_request.unwrap_or(false) {
self.request(
RestartCommand {
raw: args.unwrap_or(Value::Null),
},
Self::fallback_to_manual_restart,
Self::empty_response,
cx,
)
.detach();
} else {
cx.emit(SessionStateEvent::Restart);
self.request(
DisconnectCommand {
restart: Some(false),
terminate_debuggee: Some(true),
suspend_debuggee: Some(false),
},
Self::empty_response,
cx,
)
.detach();
}
}
@@ -1480,14 +1475,8 @@ impl Session {
cx.emit(SessionStateEvent::Shutdown);
let debug_client = self.adapter_client();
cx.background_spawn(async move {
let _ = task.await;
if let Some(client) = debug_client {
client.shutdown().await.log_err();
}
})
}

View File

@@ -3094,9 +3094,6 @@ impl Project {
.map(|lister| lister.term())
}
pub fn toolchain_store(&self) -> Option<Entity<ToolchainStore>> {
self.toolchain_store.clone()
}
pub fn activate_toolchain(
&self,
path: ProjectPath,

View File

@@ -55,7 +55,6 @@ impl ToolchainStore {
});
Self(ToolchainStoreInner::Local(entity, subscription))
}
pub(super) fn remote(project_id: u64, client: AnyProtoClient, cx: &mut App) -> Self {
Self(ToolchainStoreInner::Remote(
cx.new(|_| RemoteToolchainStore { client, project_id }),
@@ -286,7 +285,7 @@ struct LocalStore(WeakEntity<LocalToolchainStore>);
struct RemoteStore(WeakEntity<RemoteToolchainStore>);
#[derive(Clone)]
pub enum ToolchainStoreEvent {
pub(crate) enum ToolchainStoreEvent {
ToolchainActivated,
}

View File

@@ -18,7 +18,6 @@ message GetPrivateUserInfoResponse {
enum Plan {
Free = 0;
ZedPro = 1;
ZedProTrial = 2;
}
message UpdateUserPlan {

View File

@@ -23,7 +23,6 @@ test-support = ["fs/test-support"]
[dependencies]
anyhow.workspace = true
askpass.workspace = true
async-watch.workspace = true
backtrace = "0.3"
chrono.workspace = true

View File

@@ -8,10 +8,6 @@ use std::path::PathBuf;
struct Cli {
#[command(subcommand)]
command: Option<Commands>,
/// Used for SSH/Git password authentication, to remove the need for netcat as a dependency,
/// by having Zed act like netcat communicating over a Unix socket.
#[arg(long, hide = true)]
askpass: Option<String>,
}
#[derive(Subcommand)]
@@ -50,11 +46,6 @@ fn main() {
let cli = Cli::parse();
if let Some(socket_path) = &cli.askpass {
askpass::main(socket_path);
return;
}
let result = match cli.command {
Some(Commands::Run {
log_file,

35
crates/rpc/src/llm.rs Normal file
View File

@@ -0,0 +1,35 @@
use serde::{Deserialize, Serialize};
use strum::{Display, EnumIter, EnumString};
pub const EXPIRED_LLM_TOKEN_HEADER_NAME: &str = "x-zed-expired-token";
pub const MAX_LLM_MONTHLY_SPEND_REACHED_HEADER_NAME: &str = "x-zed-llm-max-monthly-spend-reached";
#[derive(
Debug, PartialEq, Eq, Hash, Clone, Copy, Serialize, Deserialize, EnumString, EnumIter, Display,
)]
#[serde(rename_all = "snake_case")]
#[strum(serialize_all = "snake_case")]
pub enum LanguageModelProvider {
Anthropic,
OpenAi,
Google,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct LanguageModel {
pub provider: LanguageModelProvider,
pub name: String,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct ListModelsResponse {
pub models: Vec<LanguageModel>,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct PerformCompletionParams {
pub provider: LanguageModelProvider,
pub model: String,
pub provider_request: Box<serde_json::value::RawValue>,
}

View File

@@ -1,12 +1,14 @@
pub mod auth;
mod conn;
mod extension;
mod llm;
mod message_stream;
mod notification;
mod peer;
pub use conn::Connection;
pub use extension::*;
pub use llm::*;
pub use notification::*;
pub use peer::*;
pub use proto;

View File

@@ -23,8 +23,3 @@ snippet.workspace = true
util.workspace = true
schemars.workspace = true
workspace-hack.workspace = true
[dev-dependencies]
fs = { workspace = true, features = ["test-support"] }
gpui = { workspace = true, features = ["test-support"] }
indoc.workspace = true

View File

@@ -222,15 +222,15 @@ impl SnippetProvider {
.lookup_snippets::<false>(language, cx),
);
}
let Some(registry) = SnippetRegistry::try_global(cx) else {
return user_snippets;
};
let registry_snippets = registry.get_snippets(language);
user_snippets.extend(registry_snippets);
}
let Some(registry) = SnippetRegistry::try_global(cx) else {
return user_snippets;
};
let registry_snippets = registry.get_snippets(language);
user_snippets.extend(registry_snippets);
user_snippets
}
@@ -244,38 +244,3 @@ impl SnippetProvider {
requested_snippets
}
}
#[cfg(test)]
mod tests {
use super::*;
use fs::FakeFs;
use gpui;
use gpui::TestAppContext;
use indoc::indoc;
#[gpui::test]
fn test_lookup_snippets_dup_registry_snippets(cx: &mut TestAppContext) {
let fs = FakeFs::new(cx.background_executor.clone());
cx.update(|cx| {
SnippetRegistry::init_global(cx);
SnippetRegistry::global(cx)
.register_snippets(
"ruby".as_ref(),
indoc! {r#"
{
"Log to console": {
"prefix": "log",
"body": ["console.info(\"Hello, ${1:World}!\")", "$0"],
"description": "Logs to console"
}
}
"#},
)
.unwrap();
let provider = SnippetProvider::new(fs.clone(), Default::default(), cx);
cx.update_entity(&provider, |provider, cx| {
assert_eq!(1, provider.snippets_for(Some("ruby".to_owned()), cx).len());
});
});
}
}

View File

@@ -98,6 +98,62 @@ impl DebugRequestDisposition {
}
}
}
/// Represents the configuration for the debug adapter
#[derive(PartialEq, Eq, Clone, Debug)]
pub struct DebugAdapterConfig {
/// Name of the debug task
pub label: String,
/// The type of adapter you want to use
pub adapter: String,
/// The type of request that should be called on the debug adapter
pub request: DebugRequestDisposition,
/// Additional initialization arguments to be sent on DAP initialization
pub initialize_args: Option<serde_json::Value>,
/// Optional TCP connection information
///
/// If provided, this will be used to connect to the debug adapter instead of
/// spawning a new process. This is useful for connecting to a debug adapter
/// that is already running or is started by another process.
pub tcp_connection: Option<TCPHost>,
/// What Locator to use to configure the debug task
pub locator: Option<String>,
/// Whether to tell the debug adapter to stop on entry
pub stop_on_entry: Option<bool>,
}
impl From<DebugTaskDefinition> for DebugAdapterConfig {
fn from(def: DebugTaskDefinition) -> Self {
Self {
label: def.label,
adapter: def.adapter,
request: DebugRequestDisposition::UserConfigured(def.request),
initialize_args: def.initialize_args,
tcp_connection: def.tcp_connection,
locator: def.locator,
stop_on_entry: def.stop_on_entry,
}
}
}
impl TryFrom<DebugAdapterConfig> for DebugTaskDefinition {
type Error = ();
fn try_from(def: DebugAdapterConfig) -> Result<Self, Self::Error> {
let request = match def.request {
DebugRequestDisposition::UserConfigured(debug_request_type) => debug_request_type,
DebugRequestDisposition::ReverseRequest(_) => return Err(()),
};
Ok(Self {
label: def.label,
adapter: def.adapter,
request,
initialize_args: def.initialize_args,
tcp_connection: def.tcp_connection,
locator: def.locator,
stop_on_entry: def.stop_on_entry,
})
}
}
impl TryFrom<TaskTemplate> for DebugTaskDefinition {
type Error = ();

View File

@@ -16,8 +16,8 @@ use std::path::PathBuf;
use std::str::FromStr;
pub use debug_format::{
AttachConfig, DebugConnectionType, DebugRequestDisposition, DebugRequestType,
DebugTaskDefinition, DebugTaskFile, LaunchConfig, TCPHost,
AttachConfig, DebugAdapterConfig, DebugConnectionType, DebugRequestDisposition,
DebugRequestType, DebugTaskDefinition, DebugTaskFile, LaunchConfig, TCPHost,
};
pub use task_template::{
DebugArgs, DebugArgsRequest, HideStrategy, RevealStrategy, TaskModal, TaskTemplate,

View File

@@ -36,7 +36,7 @@ use ui::{
IconWithIndicator, Indicator, PopoverMenu, Tooltip, h_flex, prelude::*,
};
use util::ResultExt;
use workspace::{Workspace, notifications::NotifyResultExt};
use workspace::{BottomDockLayout, Workspace, notifications::NotifyResultExt};
use zed_actions::{OpenBrowser, OpenRecent, OpenRemote};
pub use onboarding_banner::restore_banner;
@@ -210,6 +210,7 @@ impl Render for TitleBar {
.pr_1()
.on_mouse_down(MouseButton::Left, |_, _, cx| cx.stop_propagation())
.children(self.render_call_controls(window, cx))
.child(self.render_bottom_dock_layout_menu(cx))
.map(|el| {
let status = self.client.status();
let status = &*status.borrow();
@@ -301,7 +302,7 @@ impl TitleBar {
cx.notify()
}),
);
subscriptions.push(cx.subscribe(&project, |_, _, _: &project::Event, cx| cx.notify()));
subscriptions.push(cx.subscribe(&project, |_, _, _, cx| cx.notify()));
subscriptions.push(cx.observe(&active_call, |this, _, cx| this.active_call_changed(cx)));
subscriptions.push(cx.observe_window_activation(window, Self::window_activation_changed));
subscriptions.push(cx.observe(&user_store, |_, _, cx| cx.notify()));
@@ -622,6 +623,101 @@ impl TitleBar {
}
}
pub fn render_bottom_dock_layout_menu(&self, cx: &mut Context<Self>) -> impl IntoElement {
let workspace = self.workspace.upgrade().unwrap();
let current_layout = workspace.update(cx, |workspace, _cx| workspace.bottom_dock_layout());
PopoverMenu::new("layout-menu")
.trigger(
IconButton::new("toggle_layout", IconName::Layout)
.icon_size(IconSize::Small)
.tooltip(Tooltip::text("Toggle Layout Menu")),
)
.anchor(gpui::Corner::TopRight)
.menu(move |window, cx| {
ContextMenu::build(window, cx, {
let workspace = workspace.clone();
move |menu, _, _| {
menu.label("Bottom Dock")
.separator()
.toggleable_entry(
"Contained",
current_layout == BottomDockLayout::Contained,
ui::IconPosition::End,
None,
{
let workspace = workspace.clone();
move |window, cx| {
workspace.update(cx, |workspace, cx| {
workspace.set_bottom_dock_layout(
BottomDockLayout::Contained,
window,
cx,
);
});
}
},
)
.toggleable_entry(
"Full",
current_layout == BottomDockLayout::Full,
ui::IconPosition::End,
None,
{
let workspace = workspace.clone();
move |window, cx| {
workspace.update(cx, |workspace, cx| {
workspace.set_bottom_dock_layout(
BottomDockLayout::Full,
window,
cx,
);
});
}
},
)
.toggleable_entry(
"Left Aligned",
current_layout == BottomDockLayout::LeftAligned,
ui::IconPosition::End,
None,
{
let workspace = workspace.clone();
move |window, cx| {
workspace.update(cx, |workspace, cx| {
workspace.set_bottom_dock_layout(
BottomDockLayout::LeftAligned,
window,
cx,
);
});
}
},
)
.toggleable_entry(
"Right Aligned",
current_layout == BottomDockLayout::RightAligned,
ui::IconPosition::End,
None,
{
let workspace = workspace.clone();
move |window, cx| {
workspace.update(cx, |workspace, cx| {
workspace.set_bottom_dock_layout(
BottomDockLayout::RightAligned,
window,
cx,
);
});
}
},
)
}
})
.into()
})
}
pub fn render_sign_in_button(&mut self, _: &mut Context<Self>) -> Button {
let client = self.client.clone();
Button::new("sign_in", "Sign in")
@@ -655,7 +751,6 @@ impl TitleBar {
None => "",
Some(proto::Plan::Free) => "Free",
Some(proto::Plan::ZedPro) => "Pro",
Some(proto::Plan::ZedProTrial) => "Pro (Trial)",
}
),
zed_actions::OpenAccountSettings.boxed_clone(),

View File

@@ -1,4 +1,4 @@
use std::{path::Path, sync::Arc};
use std::sync::Arc;
use editor::Editor;
use gpui::{
@@ -6,7 +6,7 @@ use gpui::{
WeakEntity, Window, div,
};
use language::{Buffer, BufferEvent, LanguageName, Toolchain};
use project::{Project, ProjectPath, WorktreeId, toolchain_store::ToolchainStoreEvent};
use project::{Project, ProjectPath, WorktreeId};
use ui::{Button, ButtonCommon, Clickable, FluentBuilder, LabelSize, SharedString, Tooltip};
use workspace::{StatusItemView, Workspace, item::ItemHandle};
@@ -22,28 +22,6 @@ pub struct ActiveToolchain {
impl ActiveToolchain {
pub fn new(workspace: &Workspace, window: &mut Window, cx: &mut Context<Self>) -> Self {
if let Some(store) = workspace.project().read(cx).toolchain_store() {
cx.subscribe_in(
&store,
window,
|this, _, _: &ToolchainStoreEvent, window, cx| {
let editor = this
.workspace
.update(cx, |workspace, cx| {
workspace
.active_item(cx)
.and_then(|item| item.downcast::<Editor>())
})
.ok()
.flatten();
if let Some(editor) = editor {
this.active_toolchain.take();
this.update_lister(editor, window, cx);
}
},
)
.detach();
}
Self {
active_toolchain: None,
active_buffer: None,
@@ -79,19 +57,12 @@ impl ActiveToolchain {
this.term = term;
cx.notify();
});
let (worktree_id, path) = active_file
.update(cx, |this, cx| {
this.file().and_then(|file| {
Some((
file.worktree_id(cx),
Arc::<Path>::from(file.path().parent()?),
))
})
})
let worktree_id = active_file
.update(cx, |this, cx| Some(this.file()?.worktree_id(cx)))
.ok()
.flatten()?;
let toolchain =
Self::active_toolchain(workspace, worktree_id, path, language_name, cx).await?;
Self::active_toolchain(workspace, worktree_id, language_name, cx).await?;
let _ = this.update(cx, |this, cx| {
this.active_toolchain = Some(toolchain);
@@ -130,7 +101,6 @@ impl ActiveToolchain {
fn active_toolchain(
workspace: WeakEntity<Workspace>,
worktree_id: WorktreeId,
relative_path: Arc<Path>,
language_name: LanguageName,
cx: &mut AsyncWindowContext,
) -> Task<Option<Toolchain>> {
@@ -144,7 +114,7 @@ impl ActiveToolchain {
this.project().read(cx).active_toolchain(
ProjectPath {
worktree_id,
path: relative_path.clone(),
path: Arc::from("".as_ref()),
},
language_name.clone(),
cx,
@@ -163,7 +133,7 @@ impl ActiveToolchain {
project.read(cx).available_toolchains(
ProjectPath {
worktree_id,
path: relative_path.clone(),
path: Arc::from("".as_ref()),
},
language_name,
cx,
@@ -174,12 +144,7 @@ impl ActiveToolchain {
if let Some(toolchain) = toolchains.toolchains.first() {
// Since we don't have a selected toolchain, pick one for user here.
workspace::WORKSPACE_DB
.set_toolchain(
workspace_id,
worktree_id,
relative_path.to_string_lossy().into_owned(),
toolchain.clone(),
)
.set_toolchain(workspace_id, worktree_id, "".to_owned(), toolchain.clone())
.await
.ok()?;
project
@@ -187,7 +152,7 @@ impl ActiveToolchain {
this.activate_toolchain(
ProjectPath {
worktree_id,
path: relative_path,
path: Arc::from("".as_ref()),
},
toolchain.clone(),
cx,

View File

@@ -50,7 +50,6 @@ impl ToolchainSelector {
let language_name = buffer.read(cx).language()?.name();
let worktree_id = buffer.read(cx).file()?.worktree_id(cx);
let relative_path: Arc<Path> = Arc::from(buffer.read(cx).file()?.path().parent()?);
let worktree_root_path = project
.read(cx)
.worktree_for_id(worktree_id, cx)?
@@ -59,9 +58,8 @@ impl ToolchainSelector {
let workspace_id = workspace.database_id()?;
let weak = workspace.weak_handle();
cx.spawn_in(window, async move |workspace, cx| {
let as_str = relative_path.to_string_lossy().into_owned();
let active_toolchain = workspace::WORKSPACE_DB
.toolchain(workspace_id, worktree_id, as_str, language_name.clone())
.toolchain(workspace_id, worktree_id, language_name.clone())
.await
.ok()
.flatten();
@@ -74,7 +72,6 @@ impl ToolchainSelector {
active_toolchain,
worktree_id,
worktree_root_path,
relative_path,
language_name,
window,
cx,
@@ -94,7 +91,6 @@ impl ToolchainSelector {
active_toolchain: Option<Toolchain>,
worktree_id: WorktreeId,
worktree_root: Arc<Path>,
relative_path: Arc<Path>,
language_name: LanguageName,
window: &mut Window,
cx: &mut Context<Self>,
@@ -108,7 +104,6 @@ impl ToolchainSelector {
worktree_id,
worktree_root,
project,
relative_path,
language_name,
window,
cx,
@@ -142,7 +137,6 @@ pub struct ToolchainSelectorDelegate {
workspace: WeakEntity<Workspace>,
worktree_id: WorktreeId,
worktree_abs_path_root: Arc<Path>,
relative_path: Arc<Path>,
placeholder_text: Arc<str>,
_fetch_candidates_task: Task<Option<()>>,
}
@@ -155,7 +149,6 @@ impl ToolchainSelectorDelegate {
worktree_id: WorktreeId,
worktree_abs_path_root: Arc<Path>,
project: Entity<Project>,
relative_path: Arc<Path>,
language_name: LanguageName,
window: &mut Window,
cx: &mut Context<Picker<Self>>,
@@ -169,26 +162,17 @@ impl ToolchainSelectorDelegate {
})
.ok()?
.await?;
let relative_path = this
.update(cx, |this, _| this.delegate.relative_path.clone())
.ok()?;
let placeholder_text = format!(
"Select a {} for `{}`…",
term.to_lowercase(),
relative_path.to_string_lossy()
)
.into();
let placeholder_text = format!("Select a {}", term.to_lowercase()).into();
let _ = this.update_in(cx, move |this, window, cx| {
this.delegate.placeholder_text = placeholder_text;
this.refresh_placeholder(window, cx);
});
let available_toolchains = project
.update(cx, |this, cx| {
this.available_toolchains(
ProjectPath {
worktree_id,
path: relative_path.clone(),
path: Arc::from("".as_ref()),
},
language_name,
cx,
@@ -227,7 +211,6 @@ impl ToolchainSelectorDelegate {
worktree_id,
worktree_abs_path_root,
placeholder_text,
relative_path,
_fetch_candidates_task,
}
}
@@ -263,18 +246,19 @@ impl PickerDelegate for ToolchainSelectorDelegate {
{
let workspace = self.workspace.clone();
let worktree_id = self.worktree_id;
let path = self.relative_path.clone();
let relative_path = self.relative_path.to_string_lossy().into_owned();
cx.spawn_in(window, async move |_, cx| {
workspace::WORKSPACE_DB
.set_toolchain(workspace_id, worktree_id, relative_path, toolchain.clone())
.set_toolchain(workspace_id, worktree_id, "".to_owned(), toolchain.clone())
.await
.log_err();
workspace
.update(cx, |this, cx| {
this.project().update(cx, |this, cx| {
this.activate_toolchain(
ProjectPath { worktree_id, path },
ProjectPath {
worktree_id,
path: Arc::from("".as_ref()),
},
toolchain,
cx,
)

View File

@@ -110,7 +110,11 @@ impl RenderOnce for Banner {
let mut container = base.bg(bg_color).border_color(border_color);
let mut content_area = h_flex().id("content_area").gap_1p5().overflow_x_scroll();
let mut content_area = h_flex()
.id("content_area")
.flex_1()
.gap_1p5()
.overflow_x_scroll();
if self.icon.is_none() {
content_area =
@@ -126,10 +130,13 @@ impl RenderOnce for Banner {
.pl_2()
.pr_0p5()
.gap_2()
.flex_1()
.child(content_area)
.child(action_slot);
} else {
container = container.px_2().child(div().w_full().child(content_area));
container = container
.px_2()
.child(div().w_full().flex_1().child(content_area));
}
container

View File

@@ -73,11 +73,11 @@ impl Tab {
self
}
pub fn content_height(cx: &App) -> Pixels {
pub fn content_height(cx: &mut App) -> Pixels {
DynamicSpacing::Base32.px(cx) - px(1.)
}
pub fn container_height(cx: &App) -> Pixels {
pub fn container_height(cx: &mut App) -> Pixels {
DynamicSpacing::Base32.px(cx)
}
}

View File

@@ -160,11 +160,7 @@ impl Render for Tooltip {
}),
)
.when_some(self.meta.clone(), |this, meta| {
this.child(
div()
.max_w_72()
.child(Label::new(meta).size(LabelSize::Small).color(Color::Muted)),
)
this.child(Label::new(meta).size(LabelSize::Small).color(Color::Muted))
})
})
}

View File

@@ -1,5 +1,5 @@
[package]
name = "web_search"
name = "ui_parking_lot"
version = "0.1.0"
edition.workspace = true
publish.workspace = true
@@ -9,12 +9,16 @@ license = "GPL-3.0-or-later"
workspace = true
[lib]
path = "src/web_search.rs"
path = "src/ui_parking_lot.rs"
[features]
default = []
[dependencies]
anyhow.workspace = true
collections.workspace = true
component.workspace = true
gpui.workspace = true
serde.workspace = true
workspace-hack.workspace = true
zed_llm_client.workspace = true
linkme.workspace = true
ui.workspace = true
util.workspace = true
workspace.workspace = true

View File

@@ -0,0 +1 @@
pub mod usage_banner;

View File

@@ -0,0 +1,475 @@
use gpui::Entity;
use ui::{Banner, Severity};
use ui::{ProgressBar, prelude::*};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum CurrentPlan {
Trial,
Free,
Paid,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum CapReason {
RequestLimit,
SpendLimit,
}
#[derive(RegisterComponent)]
pub struct UsageBanner {
current_plan: CurrentPlan,
current_requests: u32,
current_spend: u32,
monthly_cap: u32,
usage_based_enabled: bool,
usage_progress: Entity<ProgressBar>,
}
impl UsageBanner {
/// Creates a new UsageBanner with the provided values
pub fn new(
current_plan: CurrentPlan,
current_requests: u32,
current_spend: u32,
monthly_cap: u32,
usage_based_enabled: bool,
cx: &mut App,
) -> Self {
let usage_progress = cx.new(|cx| {
ProgressBar::new(
"usage_progress",
current_requests as f32,
request_cap_for_plan(&current_plan) as f32,
cx,
)
});
let banner = Self {
current_plan,
current_requests,
current_spend,
monthly_cap,
usage_based_enabled,
usage_progress,
};
// No need to update styling here as it will be done when rendering
banner
}
/// Returns the request cap based on the current plan
pub fn request_cap(&self) -> u32 {
request_cap_for_plan(&self.current_plan)
}
/// Check if the user is capped due to hitting request limits
pub fn is_capped_by_requests(&self) -> bool {
self.current_requests >= self.request_cap()
}
/// Check if the user is capped due to hitting spend limits
pub fn is_capped_by_spend(&self) -> bool {
// Only check spend limit if spending is enabled and cap is set
self.usage_based_enabled && self.monthly_cap > 0 && self.current_spend >= self.monthly_cap
}
/// Check if the user is approaching request limit (>=90%)
pub fn is_approaching_request_limit(&self) -> bool {
let threshold = (self.request_cap() as f32 * 0.9) as u32;
self.current_requests >= threshold && self.current_requests < self.request_cap()
}
/// Check if the user is approaching spend limit (>=90%)
pub fn is_approaching_spend_limit(&self) -> bool {
// Only check if spending is enabled and cap is set
self.usage_based_enabled
&& self.monthly_cap > 0
&& self.current_spend >= (self.monthly_cap as f32 * 0.9) as u32
&& self.current_spend < self.monthly_cap
}
/// Check if the user is capped and returns the reason
pub fn cap_status(&self) -> Option<CapReason> {
if self.is_capped_by_requests() {
Some(CapReason::RequestLimit)
} else if self.is_capped_by_spend() {
Some(CapReason::SpendLimit)
} else {
None
}
}
/// Check if the user is capped for any reason
pub fn is_capped(&self) -> bool {
matches!(
self.cap_status(),
Some(CapReason::RequestLimit | CapReason::SpendLimit)
)
}
/// Update the current request count and progress bar
pub fn update_requests(&mut self, requests: u32, cx: &mut Context<Self>) {
self.current_requests = requests;
self.update_progress_bar(cx);
self.update_progress_styling(cx);
}
/// Update the current spend amount
pub fn update_spend(&mut self, spend: u32, cx: &mut Context<Self>) {
self.current_spend = spend;
self.update_progress_bar(cx);
self.update_progress_styling(cx);
}
/// Update the progress bar styling based on current usage levels
fn update_progress_styling(&self, cx: &mut Context<Self>) {
let is_near_cap = self.current_requests as f32 >= self.request_cap() as f32 * 0.9;
let is_capped = self.is_capped();
self.usage_progress.update(cx, |progress_bar, cx| {
if is_capped {
progress_bar.fg_color(cx.theme().status().error);
} else if is_near_cap {
progress_bar.fg_color(cx.theme().status().warning);
} else {
progress_bar.fg_color(cx.theme().status().info);
}
});
}
fn should_show_request_progress(&self) -> bool {
// Show request progress for all plans as long as not capped
// Only show if we have a non-zero request cap
self.request_cap() > 0 && !self.is_capped_by_requests() && !self.is_capped_by_spend()
}
/// Show the spend progress bar once requests are capped
/// if the user has usage based enabled
fn should_show_spend_progress(&self) -> bool {
// Only show spend progress for paid plans with usage-based pricing enabled
// and when a monthly cap is set
self.current_plan == CurrentPlan::Paid
&& self.usage_based_enabled
&& self.monthly_cap > 0
&& self.is_capped_by_requests()
&& !self.is_capped_by_spend()
}
/// Update the progress bar with current values
fn update_progress_bar(&mut self, cx: &mut Context<Self>) {
// Update the progress bar with new values
// We need to recreate it to update both value and max
self.usage_progress.update(cx, |progress_bar, cx| {
// Update progress bar value
*progress_bar = ProgressBar::new(
"usage_progress",
self.current_requests as f32,
self.request_cap() as f32,
cx,
);
});
}
fn severity(&self) -> Severity {
if self.is_capped_by_spend() || self.is_capped_by_requests() {
return Severity::Error;
}
if self.is_approaching_request_limit() || self.is_approaching_spend_limit() {
return Severity::Warning;
}
Severity::Info
}
}
/// Helper function to get the request cap based on plan type
fn request_cap_for_plan(plan: &CurrentPlan) -> u32 {
match plan {
CurrentPlan::Trial => 150,
CurrentPlan::Free => 50,
CurrentPlan::Paid => 500,
}
}
impl Render for UsageBanner {
fn render(&mut self, _: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
let formatted_requests = format!("{} / {}", self.current_requests, self.request_cap());
let formatted_spend = format!(
"${:.2} / ${:.2}",
self.current_spend as f32 / 100.0,
if self.monthly_cap > 0 {
self.monthly_cap as f32 / 100.0
} else {
0.0
}
);
let (message, action_button) = if self.is_capped_by_spend() {
(
"Monthly spending limit reached",
Some(Button::new("manage", "Manage Spending").into_any_element()),
)
} else if self.is_capped_by_requests() {
let msg = match self.current_plan {
CurrentPlan::Trial => "Trial request limit reached",
CurrentPlan::Free => "Free tier request limit reached",
CurrentPlan::Paid => "Monthly request limit reached",
};
let action = match self.current_plan {
CurrentPlan::Trial | CurrentPlan::Free => {
Some(Button::new("upgrade", "Upgrade").into_any_element())
}
CurrentPlan::Paid => {
if self.usage_based_enabled {
Some(Button::new("manage", "Manage").into_any_element())
} else {
Some(Button::new("enable-usage", "Try Usaged-Based").into_any_element())
}
}
};
(msg, action)
} else if self.is_approaching_request_limit() {
let msg = "Approaching request limit";
let action = match self.current_plan {
CurrentPlan::Trial | CurrentPlan::Free => {
Some(Button::new("upgrade", "Upgrade").into_any_element())
}
CurrentPlan::Paid => {
if !self.usage_based_enabled {
Some(Button::new("enable-usage", "Manage").into_any_element())
} else {
None
}
}
};
(msg, action)
} else if self.is_approaching_spend_limit() {
(
"Approaching monthly spend limit",
Some(Button::new("manage", "Manage Spending").into_any_element()),
)
} else {
let msg = match self.current_plan {
CurrentPlan::Trial => "Zed AI Trial",
CurrentPlan::Free => "Zed AI Free",
CurrentPlan::Paid => "Zed AI Paid",
};
(msg, None)
};
// Build the content section with usage information
let mut content = h_flex().flex_1().gap_1().child(Label::new(message));
// Add usage progress section if we should show it
if self.should_show_request_progress() {
content = content.child(
h_flex()
.flex_1()
.justify_end()
.gap_1p5()
.child(
h_flex()
.items_center()
.w_full()
.max_w(px(180.))
.child(self.usage_progress.clone()),
)
.child(
Label::new(formatted_requests)
.size(LabelSize::Small)
.color(Color::Muted),
),
);
}
// Add spending information for Paid users with usage-based pricing
if self.should_show_spend_progress() {
content = content.child(
h_flex().flex_1().justify_end().gap_1p5().child(
Label::new(formatted_spend)
.size(LabelSize::Small)
.color(Color::Muted),
),
);
}
// Create the banner with appropriate severity and content
let mut banner = Banner::new().severity(self.severity()).children(content);
// Add action button if available
if let Some(action) = action_button {
banner = banner.action_slot(action);
}
banner
}
}
impl Component for UsageBanner {
fn scope() -> ComponentScope {
ComponentScope::Notification
}
fn preview(_window: &mut Window, cx: &mut App) -> Option<AnyElement> {
// Create instances of UsageBanner for different scenarios
// Trial plan examples (cap = 150)
let new_trial_user = cx.new(|cx| UsageBanner::new(CurrentPlan::Trial, 10, 0, 0, false, cx));
let trial_user_warning =
cx.new(|cx| UsageBanner::new(CurrentPlan::Trial, 135, 0, 0, false, cx));
let trial_user_capped =
cx.new(|cx| UsageBanner::new(CurrentPlan::Trial, 150, 0, 0, false, cx));
// Free plan examples (cap = 50)
let free_user = cx.new(|cx| UsageBanner::new(CurrentPlan::Free, 25, 0, 0, false, cx));
let free_user_warning =
cx.new(|cx| UsageBanner::new(CurrentPlan::Free, 45, 0, 0, false, cx));
let free_user_capped =
cx.new(|cx| UsageBanner::new(CurrentPlan::Free, 50, 0, 0, false, cx));
// Pro plan examples without usage-based pricing (cap = 500)
let paid_user = cx.new(|cx| UsageBanner::new(CurrentPlan::Paid, 250, 0, 0, false, cx));
let paid_user_warning =
cx.new(|cx| UsageBanner::new(CurrentPlan::Paid, 450, 0, 0, false, cx));
let paid_user_capped =
cx.new(|cx| UsageBanner::new(CurrentPlan::Paid, 500, 0, 0, false, cx));
// Pro plan examples with usage-based pricing and monthly spend cap (cap = 500)
let paid_user_usage_based =
cx.new(|cx| UsageBanner::new(CurrentPlan::Paid, 500, 5000, 20000, true, cx));
let paid_user_usage_based_warning =
cx.new(|cx| UsageBanner::new(CurrentPlan::Paid, 500, 18000, 20000, true, cx));
let paid_user_usage_based_capped =
cx.new(|cx| UsageBanner::new(CurrentPlan::Paid, 500, 20000, 20000, true, cx));
// Group examples by plan type
let trial_examples = vec![
single_example(
"Trial - New User",
div()
.size_full()
.child(new_trial_user.clone())
.into_any_element(),
),
single_example(
"Trial - Approaching Limit",
div()
.size_full()
.child(trial_user_warning.clone())
.into_any_element(),
),
single_example(
"Trial - Request Limit Reached",
div()
.size_full()
.child(trial_user_capped.clone())
.into_any_element(),
),
];
let free_examples = vec![
single_example(
"Free - Normal Usage",
div()
.size_full()
.child(free_user.clone())
.into_any_element(),
),
single_example(
"Free - Approaching Limit",
div()
.size_full()
.child(free_user_warning.clone())
.into_any_element(),
),
single_example(
"Free - Request Limit Reached",
div()
.size_full()
.child(free_user_capped.clone())
.into_any_element(),
),
];
let paid_examples = vec![
single_example(
"Pro - Normal Usage",
div()
.size_full()
.child(paid_user.clone())
.into_any_element(),
),
single_example(
"Pro - Approaching Limit",
div()
.size_full()
.child(paid_user_warning.clone())
.into_any_element(),
),
single_example(
"Pro - Request Limit Reached",
div()
.size_full()
.child(paid_user_capped.clone())
.into_any_element(),
),
];
let paid_usage_based_examples = vec![
single_example(
"Pro with UBP - After Request Cap",
div()
.size_full()
.child(paid_user_usage_based.clone())
.into_any_element(),
),
single_example(
"Pro with UBP - Approaching Spend Cap",
div()
.size_full()
.child(paid_user_usage_based_warning.clone())
.into_any_element(),
),
single_example(
"Pro with UBP - Spend Cap Reached",
div()
.size_full()
.child(paid_user_usage_based_capped.clone())
.into_any_element(),
),
];
// Combine all examples
Some(
v_flex()
.gap_6()
.p_4()
.children(vec![
Label::new("Trial Plan")
.size(LabelSize::Large)
.into_any_element(),
example_group(trial_examples).vertical().into_any_element(),
Label::new("Free Plan")
.size(LabelSize::Large)
.into_any_element(),
example_group(free_examples).vertical().into_any_element(),
Label::new("Pro Plan")
.size(LabelSize::Large)
.into_any_element(),
example_group(paid_examples).vertical().into_any_element(),
Label::new("Pro Plan with Usage-Based Pricing")
.size(LabelSize::Large)
.into_any_element(),
example_group(paid_usage_based_examples)
.vertical()
.into_any_element(),
])
.into_any_element(),
)
}
}

View File

@@ -0,0 +1,8 @@
//! # UI Parking Lot
//!
//! A place for engineering-ready components to be parked
//! until someone has the time to pick them up and implement them further.
mod agent;
pub use agent::usage_banner::UsageBanner;

View File

@@ -1,4 +1,6 @@
use editor::{Bias, Direction, Editor, display_map::ToDisplayPoint, movement, scroll::Autoscroll};
use editor::{
Anchor, Bias, Direction, Editor, display_map::ToDisplayPoint, movement, scroll::Autoscroll,
};
use gpui::{Context, Window, actions};
use crate::{Vim, state::Mode};
@@ -23,60 +25,68 @@ impl Vim {
) {
let count = Vim::take_count(cx).unwrap_or(1);
Vim::take_forced_motion(cx);
if self.change_list.is_empty() {
return;
}
let prev = self.change_list_position.unwrap_or(self.change_list.len());
let next = if direction == Direction::Prev {
prev.saturating_sub(count)
} else {
(prev + count).min(self.change_list.len() - 1)
};
self.change_list_position = Some(next);
let Some(selections) = self.change_list.get(next).cloned() else {
return;
};
self.update_editor(window, cx, |_, editor, window, cx| {
if let Some(selections) = editor
.change_list
.next_change(count, direction)
.map(|s| s.to_vec())
{
editor.change_selections(Some(Autoscroll::fit()), window, cx, |s| {
let map = s.display_map();
s.select_display_ranges(selections.iter().map(|a| {
let point = a.to_display_point(&map);
point..point
}))
})
};
editor.change_selections(Some(Autoscroll::fit()), window, cx, |s| {
let map = s.display_map();
s.select_display_ranges(selections.into_iter().map(|a| {
let point = a.to_display_point(&map);
point..point
}))
})
});
}
pub(crate) fn push_to_change_list(&mut self, window: &mut Window, cx: &mut Context<Self>) {
let Some((new_positions, buffer)) = self.update_editor(window, cx, |vim, editor, _, cx| {
let Some((map, selections, buffer)) = self.update_editor(window, cx, |_, editor, _, cx| {
let (map, selections) = editor.selections.all_adjusted_display(cx);
let buffer = editor.buffer().clone();
let pop_state = editor
.change_list
.last()
.map(|previous| {
previous.len() == selections.len()
&& previous.iter().enumerate().all(|(ix, p)| {
p.to_display_point(&map).row() == selections[ix].head().row()
})
})
.unwrap_or(false);
let new_positions = selections
.into_iter()
.map(|s| {
let point = if vim.mode == Mode::Insert {
movement::saturating_left(&map, s.head())
} else {
s.head()
};
map.display_point_to_anchor(point, Bias::Left)
})
.collect::<Vec<_>>();
editor
.change_list
.push_to_change_list(pop_state, new_positions.clone());
(new_positions, buffer)
(map, selections, buffer)
}) else {
return;
};
let pop_state = self
.change_list
.last()
.map(|previous| {
previous.len() == selections.len()
&& previous.iter().enumerate().all(|(ix, p)| {
p.to_display_point(&map).row() == selections[ix].head().row()
})
})
.unwrap_or(false);
let new_positions: Vec<Anchor> = selections
.into_iter()
.map(|s| {
let point = if self.mode == Mode::Insert {
movement::saturating_left(&map, s.head())
} else {
s.head()
};
map.display_point_to_anchor(point, Bias::Left)
})
.collect();
self.change_list_position.take();
if pop_state {
self.change_list.pop();
}
self.change_list.push(new_positions.clone());
self.set_mark(".".to_string(), new_positions, &buffer, window, cx)
}
}

View File

@@ -132,15 +132,7 @@ fn scroll_editor(
let max_visible_row = top.row().0.saturating_add(
(visible_line_count as u32).saturating_sub(1 + vertical_scroll_margin),
);
// scroll off the end.
let max_row = if top.row().0 + visible_line_count as u32 >= map.max_point().row().0 {
map.max_point().row()
} else {
DisplayRow(
(top.row().0 + visible_line_count as u32)
.saturating_sub(1 + vertical_scroll_margin),
)
};
let max_row = DisplayRow(map.max_point().row().0.max(max_visible_row));
let new_row = if full_page_up {
// Special-casing ctrl-b/page-up, which is special-cased by Vim, it seems
@@ -380,14 +372,14 @@ mod test {
let mut cx = NeovimBackedTestContext::new(cx).await;
cx.set_scroll_height(10).await;
cx.neovim.set_option(&format!("scrolloff={}", 0)).await;
let content = "ˇ".to_owned() + &sample_text(26, 2, 'a');
cx.set_shared_state(&content).await;
cx.update_global(|store: &mut SettingsStore, cx| {
store.update_user_settings::<EditorSettings>(cx, |s| {
s.scroll_beyond_last_line = Some(ScrollBeyondLastLine::Off);
// s.vertical_scroll_margin = Some(0.);
s.scroll_beyond_last_line = Some(ScrollBeyondLastLine::Off)
});
});
@@ -403,24 +395,4 @@ mod test {
cx.simulate_shared_keystrokes("ctrl-u").await;
cx.shared_state().await.assert_matches();
}
#[gpui::test]
async fn test_ctrl_y_e(cx: &mut gpui::TestAppContext) {
let mut cx = NeovimBackedTestContext::new(cx).await;
cx.set_scroll_height(10).await;
let content = "ˇ".to_owned() + &sample_text(26, 2, 'a');
cx.set_shared_state(&content).await;
for _ in 0..8 {
cx.simulate_shared_keystrokes("ctrl-e").await;
cx.shared_state().await.assert_matches();
}
for _ in 0..8 {
cx.simulate_shared_keystrokes("ctrl-y").await;
cx.shared_state().await.assert_matches();
}
}
}

Some files were not shown because too many files have changed in this diff Show More