Compare commits

..

13 Commits

Author SHA1 Message Date
Bennet Bo Fenner
9147f89257 zed_extension_api: Release v0.5.0 (#29802)
This PR releases v0.5.0 of the Zed extension API.

Support for this version of the extension API will land in Zed v0.186.x.

Release Notes:

- N/A
2025-05-02 15:58:54 +00:00
Richard Feldman
9efc09c5a6 Add eval for open_tool (#29801)
Also have its description say it should only be used on request

Release Notes:

- N/A
2025-05-02 15:56:07 +00:00
Bennet Bo Fenner
e6f6b351b7 extension_api: Add documentation to context server configuration (#29800)
Release Notes:

- N/A
2025-05-02 15:37:05 +00:00
Bennet Bo Fenner
fde621f0e3 agent: Ensure that web search tool is always available (#29799)
Some changes in the LanguageModelRegistry caused the web search tool not
to show up, because the `DefaultModelChanged` event is not emitted at
startup anymore.

Release Notes:

- agent: Fixed an issue where the web search tool would not be available
after starting Zed (only when using zed.dev as a provider).
2025-05-02 15:34:08 +00:00
Marshall Bowers
c4556e9909 collab: Fix adding users to feature flags when migrating to new billing (#29795)
This PR fixes an issue where users were not being added to the feature
flags when being migrated to the new billing.

Release Notes:

- N/A
2025-05-02 15:07:49 +00:00
Kirill Bulatov
7e2de84155 Properly score fuzzy match queries with multiple chars in lower case (#29794)
Closes https://github.com/zed-industries/zed/issues/29526

Release Notes:

- Fixed file finder crashing for certain file names with multiple chars
in lowercase form
2025-05-02 15:02:53 +00:00
Kirill Bulatov
d1b35be353 Use proper settings in the diagnostics section (#29791)
Follow-up of https://github.com/zed-industries/zed/pull/29706

Release Notes:

- N/A

Co-authored-by: Cole Miller <cole@zed.dev>
2025-05-02 16:48:52 +03:00
Marshall Bowers
49a71ec3b8 collab: Update billing migration endpoint to work for users without active subscriptions (#29792)
This PR updates the billing migration endpoint to work for users who do
not have an active subscription.

This will allow us to use the endpoint to migrate all users.

Release Notes:

- N/A
2025-05-02 13:48:14 +00:00
Nate Butler
3bd7ae6e5b Standardize agent previews (#29790)
This PR makes agent previews render like any other preview in the
component preview list & pages.

Page:

![CleanShot 2025-05-02 at 09 17
12@2x](https://github.com/user-attachments/assets/8b611380-b686-4fd6-9c76-de27e35b0b38)

List:

![CleanShot 2025-05-02 at 09 17
33@2x](https://github.com/user-attachments/assets/ab063649-dc3c-4c95-969b-c3795b2197f2)


Release Notes:

- N/A
2025-05-02 13:32:59 +00:00
Max Brunsfeld
225deb6785 agent: Add animation in the edit file tool card until a diff is assigned (#29773)
This PR prevents this edit card from being shown expanded but empty,
like this:

<img width="590" alt="Screenshot 2025-05-01 at 7 38 47 PM"
src="https://github.com/user-attachments/assets/147d3d73-05b9-4493-8145-0ee915f12cd9"
/>

Now, we will show an animation until it has a diff computed.


https://github.com/user-attachments/assets/52900cdf-ee3d-4c3b-88c7-c53377543bcf

Release Notes:

- N/A

---------

Co-authored-by: Danilo Leal <daniloleal09@gmail.com>
2025-05-02 09:48:40 -03:00
Kirill Bulatov
33011f2eaf Open diagnostics editor faster when fetching cargo diagnostics (#29787)
Follow-up of https://github.com/zed-industries/zed/pull/29706

Release Notes:

- N/A
2025-05-02 12:10:01 +00:00
Kirill Bulatov
e14d078f8a Fix tasks not being stopped on reruns (#29786)
Follow-up of https://github.com/zed-industries/zed/pull/28993

* Tone down tasks' cancellation logging
* Fix task terminals' leak, disallowing to fully cancel the task by
dropping the terminal off the pane:

f619d5f02a/crates/terminal_view/src/terminal_panel.rs (L1464-L1471)

Release Notes:

- Fixed tasks not being stopped on reruns
2025-05-02 11:45:43 +00:00
Stanislav Alekseev
460ac96df4 Use project environment in LSP runnables context (#29761)
Release Notes:

- Fixed the tasks from LSP not inheriting the worktree environment

----

cc @SomeoneToIgnore
2025-05-02 11:01:39 +00:00
35 changed files with 641 additions and 381 deletions

View File

@@ -934,7 +934,7 @@
// Shows all diagnostics when not specified.
"max_severity": null
},
"rust": {
"cargo": {
// When enabled, Zed disables rust-analyzer's check on save and starts to query
// Cargo diagnostics separately.
"fetch_cargo_diagnostics": false

View File

@@ -1211,7 +1211,7 @@ impl Component for MessageEditor {
}
impl AgentPreview for MessageEditor {
fn create_preview(
fn agent_preview(
workspace: WeakEntity<Workspace>,
active_thread: Entity<ActiveThread>,
thread_store: WeakEntity<ThreadStore>,

View File

@@ -3,7 +3,7 @@ use component::ComponentId;
use gpui::{App, Entity, WeakEntity};
use linkme::distributed_slice;
use std::sync::OnceLock;
use ui::{AnyElement, Component, Window};
use ui::{AnyElement, Component, ComponentScope, Window};
use workspace::Workspace;
use crate::{ActiveThread, ThreadStore};
@@ -22,27 +22,20 @@ pub type PreviewFn = fn(
pub static __ALL_AGENT_PREVIEWS: [fn() -> (ComponentId, PreviewFn)] = [..];
/// Trait that must be implemented by components that provide agent previews.
pub trait AgentPreview: Component {
/// Get the ID for this component
///
/// Eventually this will move to the component trait.
fn id() -> ComponentId
where
Self: Sized,
{
ComponentId(Self::name())
pub trait AgentPreview: Component + Sized {
#[allow(unused)] // We can't know this is used due to the distributed slice
fn scope(&self) -> ComponentScope {
ComponentScope::Agent
}
/// Static method to create a preview for this component type
fn create_preview(
fn agent_preview(
workspace: WeakEntity<Workspace>,
active_thread: Entity<ActiveThread>,
thread_store: WeakEntity<ThreadStore>,
window: &mut Window,
cx: &mut App,
) -> Option<AnyElement>
where
Self: Sized;
) -> Option<AnyElement>;
}
/// Register an agent preview for the given component type
@@ -55,8 +48,8 @@ macro_rules! register_agent_preview {
$crate::ui::agent_preview::PreviewFn,
) = || {
(
<$type as $crate::ui::agent_preview::AgentPreview>::id(),
<$type as $crate::ui::agent_preview::AgentPreview>::create_preview,
<$type as component::Component>::id(),
<$type as $crate::ui::agent_preview::AgentPreview>::agent_preview,
)
};
};

View File

@@ -34,7 +34,7 @@ use assistant_settings::AssistantSettings;
use assistant_tool::ToolRegistry;
use copy_path_tool::CopyPathTool;
use feature_flags::{AgentStreamEditsFeatureFlag, FeatureFlagAppExt};
use gpui::App;
use gpui::{App, Entity};
use http_client::HttpClientWithUrl;
use language_model::LanguageModelRegistry;
use move_path_tool::MovePathTool;
@@ -48,27 +48,25 @@ use crate::code_action_tool::CodeActionTool;
use crate::code_symbols_tool::CodeSymbolsTool;
use crate::contents_tool::ContentsTool;
use crate::create_directory_tool::CreateDirectoryTool;
use crate::create_file_tool::CreateFileTool;
use crate::delete_path_tool::DeletePathTool;
use crate::diagnostics_tool::DiagnosticsTool;
use crate::edit_file_tool::EditFileTool;
use crate::fetch_tool::FetchTool;
use crate::find_path_tool::FindPathTool;
use crate::grep_tool::GrepTool;
use crate::list_directory_tool::ListDirectoryTool;
use crate::now_tool::NowTool;
use crate::open_tool::OpenTool;
use crate::read_file_tool::ReadFileTool;
use crate::rename_tool::RenameTool;
use crate::streaming_edit_file_tool::StreamingEditFileTool;
use crate::symbol_info_tool::SymbolInfoTool;
use crate::terminal_tool::TerminalTool;
use crate::thinking_tool::ThinkingTool;
pub use create_file_tool::CreateFileToolInput;
pub use edit_file_tool::EditFileToolInput;
pub use create_file_tool::{CreateFileTool, CreateFileToolInput};
pub use edit_file_tool::{EditFileTool, EditFileToolInput};
pub use find_path_tool::FindPathToolInput;
pub use open_tool::OpenTool;
pub use read_file_tool::ReadFileToolInput;
pub use terminal_tool::TerminalTool;
pub fn init(http_client: Arc<HttpClientWithUrl>, cx: &mut App) {
assistant_tool::init(cx);
@@ -101,19 +99,12 @@ pub fn init(http_client: Arc<HttpClientWithUrl>, cx: &mut App) {
cx.observe_global::<SettingsStore>(register_edit_file_tool)
.detach();
register_web_search_tool(&LanguageModelRegistry::global(cx), cx);
cx.subscribe(
&LanguageModelRegistry::global(cx),
move |registry, event, cx| match event {
language_model::Event::DefaultModelChanged => {
let using_zed_provider = registry
.read(cx)
.default_model()
.map_or(false, |default| default.is_provided_by_zed());
if using_zed_provider {
ToolRegistry::global(cx).register_tool(WebSearchTool);
} else {
ToolRegistry::global(cx).unregister_tool(WebSearchTool);
}
register_web_search_tool(&registry, cx);
}
_ => {}
},
@@ -121,6 +112,18 @@ pub fn init(http_client: Arc<HttpClientWithUrl>, cx: &mut App) {
.detach();
}
fn register_web_search_tool(registry: &Entity<LanguageModelRegistry>, cx: &mut App) {
let using_zed_provider = registry
.read(cx)
.default_model()
.map_or(false, |default| default.is_provided_by_zed());
if using_zed_provider {
ToolRegistry::global(cx).register_tool(WebSearchTool);
} else {
ToolRegistry::global(cx).unregister_tool(WebSearchTool);
}
}
fn register_edit_file_tool(cx: &mut App) {
let registry = ToolRegistry::global(cx);

View File

@@ -14,7 +14,7 @@ use futures::{
stream::BoxStream,
};
use gpui::{AppContext, AsyncApp, Entity, SharedString, Task};
use language::{Anchor, Bias, Buffer, BufferSnapshot, LineIndent, Point};
use language::{Bias, Buffer, BufferSnapshot, LineIndent, Point};
use language_model::{
LanguageModel, LanguageModelCompletionError, LanguageModelRequest, LanguageModelRequestMessage,
MessageContent, Role,
@@ -45,7 +45,7 @@ impl Template for EditFilePromptTemplate {
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum EditAgentOutputEvent {
Edited { position: Anchor },
Edited,
OldTextNotFound(SharedString),
}
@@ -139,9 +139,7 @@ impl EditAgent {
.update(cx, |log, cx| log.buffer_edited(buffer.clone(), cx));
})?;
output_events_tx
.unbounded_send(EditAgentOutputEvent::Edited {
position: Anchor::MAX,
})
.unbounded_send(EditAgentOutputEvent::Edited)
.ok();
}
@@ -277,15 +275,14 @@ impl EditAgent {
match op {
CharOperation::Insert { text } => {
let edit_start = snapshot.anchor_after(edit_start);
edits_tx
.unbounded_send((edit_start..edit_start, Arc::from(text)))?;
edits_tx.unbounded_send((edit_start..edit_start, text))?;
}
CharOperation::Delete { bytes } => {
let edit_end = edit_start + bytes;
let edit_range = snapshot.anchor_after(edit_start)
..snapshot.anchor_before(edit_end);
edit_start = edit_end;
edits_tx.unbounded_send((edit_range, Arc::from("")))?;
edits_tx.unbounded_send((edit_range, String::new()))?;
}
CharOperation::Keep { bytes } => edit_start += bytes,
}
@@ -299,32 +296,16 @@ impl EditAgent {
// TODO: group all edits into one transaction
let mut edits_rx = edits_rx.ready_chunks(32);
while let Some(edits) = edits_rx.next().await {
if edits.is_empty() {
continue;
}
// Edit the buffer and report edits to the action log as part of the
// same effect cycle, otherwise the edit will be reported as if the
// user made it.
let max_edit_end = cx.update(|cx| {
let max_edit_end = buffer.update(cx, |buffer, cx| {
buffer.edit(edits.iter().cloned(), None, cx);
let max_edit_end = buffer
.summaries_for_anchors::<Point, _>(
edits.iter().map(|(range, _)| &range.end),
)
.max()
.unwrap();
buffer.anchor_before(max_edit_end)
});
cx.update(|cx| {
buffer.update(cx, |buffer, cx| buffer.edit(edits, None, cx));
self.action_log
.update(cx, |log, cx| log.buffer_edited(buffer.clone(), cx));
max_edit_end
.update(cx, |log, cx| log.buffer_edited(buffer.clone(), cx))
})?;
output_events
.unbounded_send(EditAgentOutputEvent::Edited {
position: max_edit_end,
})
.unbounded_send(EditAgentOutputEvent::Edited)
.ok();
}
@@ -822,12 +803,7 @@ mod tests {
chunks_tx.unbounded_send("<new_text>abX").unwrap();
cx.run_until_parked();
assert_eq!(
drain_events(&mut events),
[EditAgentOutputEvent::Edited {
position: buffer.read_with(cx, |buffer, _| buffer.anchor_before(Point::new(0, 3)))
}]
);
assert_eq!(drain_events(&mut events), [EditAgentOutputEvent::Edited]);
assert_eq!(
buffer.read_with(cx, |buffer, _| buffer.snapshot().text()),
"abXc\ndef\nghi"
@@ -835,12 +811,7 @@ mod tests {
chunks_tx.unbounded_send("cY").unwrap();
cx.run_until_parked();
assert_eq!(
drain_events(&mut events),
[EditAgentOutputEvent::Edited {
position: buffer.read_with(cx, |buffer, _| buffer.anchor_before(Point::new(0, 5)))
}]
);
assert_eq!(drain_events(&mut events), [EditAgentOutputEvent::Edited]);
assert_eq!(
buffer.read_with(cx, |buffer, _| buffer.snapshot().text()),
"abXcY\ndef\nghi"
@@ -892,9 +863,7 @@ mod tests {
cx.run_until_parked();
assert_eq!(
drain_events(&mut events),
vec![EditAgentOutputEvent::Edited {
position: buffer.read_with(cx, |buffer, _| buffer.anchor_before(Point::new(2, 3)))
}]
vec![EditAgentOutputEvent::Edited]
);
assert_eq!(
buffer.read_with(cx, |buffer, _| buffer.snapshot().text()),

View File

@@ -7,7 +7,8 @@ use assistant_tool::{ActionLog, AnyToolCard, Tool, ToolCard, ToolResult, ToolUse
use buffer_diff::{BufferDiff, BufferDiffSnapshot};
use editor::{Editor, EditorMode, MultiBuffer, PathKey};
use gpui::{
AnyWindowHandle, App, AppContext, AsyncApp, Context, Entity, EntityId, Task, WeakEntity,
Animation, AnimationExt, AnyWindowHandle, App, AppContext, AsyncApp, Context, Entity, EntityId,
Task, WeakEntity, pulsating_between,
};
use language::{
Anchor, Buffer, Capability, LanguageRegistry, LineEnding, OffsetRangeExt, Rope, TextBuffer,
@@ -20,6 +21,7 @@ use serde::{Deserialize, Serialize};
use std::{
path::{Path, PathBuf},
sync::Arc,
time::Duration,
};
use ui::{Disclosure, Tooltip, Window, prelude::*};
use util::ResultExt;
@@ -323,6 +325,10 @@ impl EditFileToolCard {
}
}
pub fn has_diff(&self) -> bool {
self.total_lines.is_some()
}
pub fn set_diff(
&mut self,
path: Arc<Path>,
@@ -463,45 +469,44 @@ impl ToolCard for EditFileToolCard {
.rounded_t_md()
.when(!failed, |header| header.bg(codeblock_header_bg))
.child(path_label_button)
.map(|container| {
if failed {
container.child(
h_flex()
.gap_1()
.child(
Icon::new(IconName::Close)
.size(IconSize::Small)
.color(Color::Error),
)
.child(
Disclosure::new(
("edit-file-error-disclosure", self.editor_unique_id),
self.error_expanded,
)
.opened_icon(IconName::ChevronUp)
.closed_icon(IconName::ChevronDown)
.on_click(cx.listener(
move |this, _event, _window, _cx| {
this.error_expanded = !this.error_expanded;
},
)),
),
)
} else {
container.child(
Disclosure::new(
("edit-file-disclosure", self.editor_unique_id),
self.preview_expanded,
.when(failed, |header| {
header.child(
h_flex()
.gap_1()
.child(
Icon::new(IconName::Close)
.size(IconSize::Small)
.color(Color::Error),
)
.opened_icon(IconName::ChevronUp)
.closed_icon(IconName::ChevronDown)
.on_click(cx.listener(
move |this, _event, _window, _cx| {
this.preview_expanded = !this.preview_expanded;
},
)),
.child(
Disclosure::new(
("edit-file-error-disclosure", self.editor_unique_id),
self.error_expanded,
)
.opened_icon(IconName::ChevronUp)
.closed_icon(IconName::ChevronDown)
.on_click(cx.listener(
move |this, _event, _window, _cx| {
this.error_expanded = !this.error_expanded;
},
)),
),
)
})
.when(!failed && self.has_diff(), |header| {
header.child(
Disclosure::new(
("edit-file-disclosure", self.editor_unique_id),
self.preview_expanded,
)
}
.opened_icon(IconName::ChevronUp)
.closed_icon(IconName::ChevronDown)
.on_click(cx.listener(
move |this, _event, _window, _cx| {
this.preview_expanded = !this.preview_expanded;
},
)),
)
});
let (editor, editor_line_height) = self.editor.update(cx, |editor, cx| {
@@ -538,6 +543,50 @@ impl ToolCard for EditFileToolCard {
const DEFAULT_COLLAPSED_LINES: u32 = 10;
let is_collapsible = self.total_lines.unwrap_or(0) > DEFAULT_COLLAPSED_LINES;
let waiting_for_diff = {
let styles = [
("w_4_5", (0.1, 0.85), 2000),
("w_1_4", (0.2, 0.75), 2200),
("w_2_4", (0.15, 0.64), 1900),
("w_3_5", (0.25, 0.72), 2300),
("w_2_5", (0.3, 0.56), 1800),
];
let mut container = v_flex()
.p_3()
.gap_1p5()
.border_t_1()
.border_color(border_color)
.bg(cx.theme().colors().editor_background);
for (width_method, pulse_range, duration_ms) in styles.iter() {
let (min_opacity, max_opacity) = *pulse_range;
let placeholder = match *width_method {
"w_4_5" => div().w_3_4(),
"w_1_4" => div().w_1_4(),
"w_2_4" => div().w_2_4(),
"w_3_5" => div().w_3_5(),
"w_2_5" => div().w_2_5(),
_ => div().w_1_2(),
}
.id("loading_div")
.h_2()
.rounded_full()
.bg(cx.theme().colors().element_active)
.with_animation(
"loading_pulsate",
Animation::new(Duration::from_millis(*duration_ms))
.repeat()
.with_easing(pulsating_between(min_opacity, max_opacity)),
|label, delta| label.opacity(delta),
);
container = container.child(placeholder);
}
container
};
v_flex()
.mb_2()
.border_1()
@@ -573,50 +622,58 @@ impl ToolCard for EditFileToolCard {
),
)
})
.when(!failed && self.preview_expanded, |card| {
card.child(
v_flex()
.relative()
.h_full()
.when(!self.full_height_expanded, |editor_container| {
editor_container
.max_h(DEFAULT_COLLAPSED_LINES as f32 * editor_line_height)
})
.overflow_hidden()
.border_t_1()
.border_color(border_color)
.bg(cx.theme().colors().editor_background)
.child(div().pl_1().child(editor))
.when(
!self.full_height_expanded && is_collapsible,
|editor_container| editor_container.child(gradient_overlay),
),
)
.when(is_collapsible, |editor_container| {
editor_container.child(
h_flex()
.id(("expand-button", self.editor_unique_id))
.flex_none()
.cursor_pointer()
.h_5()
.justify_center()
.rounded_b_md()
.when(!self.has_diff() && !failed, |card| {
card.child(waiting_for_diff)
})
.when(
!failed && self.preview_expanded && self.has_diff(),
|card| {
card.child(
v_flex()
.relative()
.h_full()
.when(!self.full_height_expanded, |editor_container| {
editor_container
.max_h(DEFAULT_COLLAPSED_LINES as f32 * editor_line_height)
})
.overflow_hidden()
.border_t_1()
.border_color(border_color)
.bg(cx.theme().colors().editor_background)
.hover(|style| style.bg(cx.theme().colors().element_hover.opacity(0.1)))
.child(
Icon::new(full_height_icon)
.size(IconSize::Small)
.color(Color::Muted),
)
.tooltip(Tooltip::text(full_height_tooltip_label))
.on_click(cx.listener(move |this, _event, _window, _cx| {
this.full_height_expanded = !this.full_height_expanded;
})),
.child(div().pl_1().child(editor))
.when(
!self.full_height_expanded && is_collapsible,
|editor_container| editor_container.child(gradient_overlay),
),
)
})
})
.when(is_collapsible, |editor_container| {
editor_container.child(
h_flex()
.id(("expand-button", self.editor_unique_id))
.flex_none()
.cursor_pointer()
.h_5()
.justify_center()
.rounded_b_md()
.border_t_1()
.border_color(border_color)
.bg(cx.theme().colors().editor_background)
.hover(|style| {
style.bg(cx.theme().colors().element_hover.opacity(0.1))
})
.child(
Icon::new(full_height_icon)
.size(IconSize::Small)
.color(Color::Muted),
)
.tooltip(Tooltip::text(full_height_tooltip_label))
.on_click(cx.listener(move |this, _event, _window, _cx| {
this.full_height_expanded = !this.full_height_expanded;
})),
)
})
},
)
}
}

View File

@@ -4,3 +4,6 @@ This tool opens a file or URL with the default application associated with it on
- On Linux, it uses something like `xdg-open`, `gio open`, `gnome-open`, `kde-open`, `wslview` as appropriate
For example, it can open a web browser with a URL, open a PDF file with the default PDF viewer, etc.
You MUST ONLY use this tool when the user has explicitly requested opening something. You MUST NEVER assume that
the user would like for you to use this tool.

View File

@@ -205,7 +205,7 @@ impl Tool for StreamingEditFileTool {
let mut hallucinated_old_text = false;
while let Some(event) = events.next().await {
match event {
EditAgentOutputEvent::Edited { position } => {
EditAgentOutputEvent::Edited => {
if let Some(card) = card_clone.as_ref() {
let new_snapshot =
buffer.read_with(cx, |buffer, _cx| buffer.snapshot())?;

View File

@@ -624,7 +624,7 @@ struct MigrateToNewBillingBody {
#[derive(Debug, Serialize)]
struct MigrateToNewBillingResponse {
/// The ID of the subscription that was canceled.
canceled_subscription_id: String,
canceled_subscription_id: Option<String>,
}
async fn migrate_to_new_billing(
@@ -650,39 +650,39 @@ async fn migrate_to_new_billing(
.get_active_billing_subscriptions(HashSet::from_iter([user.id]))
.await?;
let Some((_billing_customer, billing_subscription)) =
let canceled_subscription_id = if let Some((_billing_customer, billing_subscription)) =
old_billing_subscriptions_by_user.get(&user.id)
else {
return Err(Error::http(
StatusCode::NOT_FOUND,
"No active billing subscriptions to migrate".into(),
));
{
let stripe_subscription_id = billing_subscription
.stripe_subscription_id
.parse::<stripe::SubscriptionId>()
.context("failed to parse Stripe subscription ID from database")?;
Subscription::cancel(
&stripe_client,
&stripe_subscription_id,
stripe::CancelSubscription {
invoice_now: Some(true),
..Default::default()
},
)
.await?;
Some(stripe_subscription_id)
} else {
None
};
let stripe_subscription_id = billing_subscription
.stripe_subscription_id
.parse::<stripe::SubscriptionId>()
.context("failed to parse Stripe subscription ID from database")?;
Subscription::cancel(
&stripe_client,
&stripe_subscription_id,
stripe::CancelSubscription {
invoice_now: Some(true),
..Default::default()
},
)
.await?;
let feature_flags = app.db.list_feature_flags().await?;
let all_feature_flags = app.db.list_feature_flags().await?;
let user_feature_flags = app.db.get_user_flags(user.id).await?;
for feature_flag in ["new-billing", "assistant2"] {
let already_in_feature_flag = feature_flags.iter().any(|flag| flag.flag == feature_flag);
let already_in_feature_flag = user_feature_flags.iter().any(|flag| flag == feature_flag);
if already_in_feature_flag {
continue;
}
let feature_flag = feature_flags
let feature_flag = all_feature_flags
.iter()
.find(|flag| flag.flag == feature_flag)
.context("failed to find feature flag: {feature_flag:?}")?;
@@ -691,7 +691,8 @@ async fn migrate_to_new_billing(
}
Ok(Json(MigrateToNewBillingResponse {
canceled_subscription_id: stripe_subscription_id.to_string(),
canceled_subscription_id: canceled_subscription_id
.map(|subscription_id| subscription_id.to_string()),
}))
}

View File

@@ -18,6 +18,9 @@ pub trait Component {
fn name() -> &'static str {
std::any::type_name::<Self>()
}
fn id() -> ComponentId {
ComponentId(Self::name())
}
/// 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.
@@ -81,7 +84,7 @@ pub fn register_component<T: Component>() {
let component_data = (T::scope(), T::name(), T::sort_name(), T::description());
let mut data = COMPONENT_DATA.write();
data.components.push(component_data);
data.previews.insert(T::name(), T::preview);
data.previews.insert(T::id().0, T::preview);
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]

View File

@@ -110,18 +110,7 @@ struct ComponentPreview {
active_page: PreviewPage,
components: Vec<ComponentMetadata>,
component_list: ListState,
agent_previews: Vec<
Box<
dyn Fn(
&Self,
WeakEntity<Workspace>,
Entity<ActiveThread>,
WeakEntity<ThreadStore>,
&mut Window,
&mut App,
) -> Option<AnyElement>,
>,
>,
agent_previews: Vec<ComponentId>,
cursor_index: usize,
language_registry: Arc<LanguageRegistry>,
workspace: WeakEntity<Workspace>,
@@ -191,38 +180,7 @@ impl ComponentPreview {
);
// Initialize agent previews
let agent_previews = agent::all_agent_previews()
.into_iter()
.map(|id| {
Box::new(
move |_self: &ComponentPreview,
workspace: WeakEntity<Workspace>,
active_thread: Entity<ActiveThread>,
thread_store: WeakEntity<ThreadStore>,
window: &mut Window,
cx: &mut App| {
agent::get_agent_preview(
&id,
workspace,
active_thread,
thread_store,
window,
cx,
)
},
)
as Box<
dyn Fn(
&ComponentPreview,
WeakEntity<Workspace>,
Entity<ActiveThread>,
WeakEntity<ThreadStore>,
&mut Window,
&mut App,
) -> Option<AnyElement>,
>
})
.collect::<Vec<_>>();
let agent_previews = agent::all_agent_previews();
let mut component_preview = Self {
workspace_id: None,
@@ -635,44 +593,65 @@ impl ComponentPreview {
let description = component.description();
v_flex()
.py_2()
.child(
v_flex()
.border_1()
.border_color(cx.theme().colors().border)
.rounded_sm()
.w_full()
.gap_4()
.py_4()
.px_6()
.flex_none()
.child(
v_flex()
.gap_1()
.child(
h_flex().gap_1().text_xl().child(div().child(name)).when(
!matches!(scope, ComponentScope::None),
|this| {
this.child(div().opacity(0.5).child(format!("({})", scope)))
},
),
// Build the content container
let mut preview_container = v_flex().py_2().child(
v_flex()
.border_1()
.border_color(cx.theme().colors().border)
.rounded_sm()
.w_full()
.gap_4()
.py_4()
.px_6()
.flex_none()
.child(
v_flex()
.gap_1()
.child(
h_flex()
.gap_1()
.text_xl()
.child(div().child(name))
.when(!matches!(scope, ComponentScope::None), |this| {
this.child(div().opacity(0.5).child(format!("({})", scope)))
}),
)
.when_some(description, |this, description| {
this.child(
div()
.text_ui_sm(cx)
.text_color(cx.theme().colors().text_muted)
.max_w(px(600.0))
.child(description),
)
.when_some(description, |this, description| {
this.child(
div()
.text_ui_sm(cx)
.text_color(cx.theme().colors().text_muted)
.max_w(px(600.0))
.child(description),
)
}),
)
.when_some(component.preview(), |this, preview| {
this.children(preview(window, cx))
}),
)
.into_any_element()
}),
),
);
// Check if the component's scope is Agent
if scope == ComponentScope::Agent {
if let (Some(thread_store), Some(active_thread)) = (
self.thread_store.as_ref().map(|ts| ts.downgrade()),
self.active_thread.clone(),
) {
if let Some(element) = agent::get_agent_preview(
&component.id(),
self.workspace.clone(),
active_thread,
thread_store,
window,
cx,
) {
preview_container = preview_container.child(element);
} else if let Some(preview) = component.preview() {
preview_container = preview_container.children(preview(window, cx));
}
}
} else if let Some(preview) = component.preview() {
preview_container = preview_container.children(preview(window, cx));
}
preview_container.into_any_element()
}
fn render_all_components(&self, cx: &Context<Self>) -> impl IntoElement {
@@ -711,7 +690,12 @@ impl ComponentPreview {
v_flex()
.id("render-component-page")
.size_full()
.child(ComponentPreviewPage::new(component.clone()))
.child(ComponentPreviewPage::new(
component.clone(),
self.workspace.clone(),
self.thread_store.as_ref().map(|ts| ts.downgrade()),
self.active_thread.clone(),
))
.into_any_element()
} else {
v_flex()
@@ -732,13 +716,13 @@ impl ComponentPreview {
.id("render-active-thread")
.size_full()
.child(
v_flex().children(self.agent_previews.iter().filter_map(|preview_fn| {
v_flex().children(self.agent_previews.iter().filter_map(|component_id| {
if let (Some(thread_store), Some(active_thread)) = (
self.thread_store.as_ref().map(|ts| ts.downgrade()),
self.active_thread.clone(),
) {
preview_fn(
self,
agent::get_agent_preview(
component_id,
self.workspace.clone(),
active_thread,
thread_store,
@@ -894,7 +878,7 @@ impl Default for ActivePageId {
impl From<ComponentId> for ActivePageId {
fn from(id: ComponentId) -> Self {
ActivePageId(id.0.to_string())
Self(id.0.to_string())
}
}
@@ -1073,16 +1057,25 @@ impl SerializableItem for ComponentPreview {
pub struct ComponentPreviewPage {
// languages: Arc<LanguageRegistry>,
component: ComponentMetadata,
workspace: WeakEntity<Workspace>,
thread_store: Option<WeakEntity<ThreadStore>>,
active_thread: Option<Entity<ActiveThread>>,
}
impl ComponentPreviewPage {
pub fn new(
component: ComponentMetadata,
workspace: WeakEntity<Workspace>,
thread_store: Option<WeakEntity<ThreadStore>>,
active_thread: Option<Entity<ActiveThread>>,
// languages: Arc<LanguageRegistry>
) -> Self {
Self {
// languages,
component,
workspace,
thread_store,
active_thread,
}
}
@@ -1113,12 +1106,32 @@ impl ComponentPreviewPage {
}
fn render_preview(&self, window: &mut Window, cx: &mut App) -> impl IntoElement {
// Try to get agent preview first if we have an active thread
let maybe_agent_preview = if let (Some(thread_store), Some(active_thread)) =
(self.thread_store.as_ref(), self.active_thread.as_ref())
{
agent::get_agent_preview(
&self.component.id(),
self.workspace.clone(),
active_thread.clone(),
thread_store.clone(),
window,
cx,
)
} else {
None
};
v_flex()
.flex_1()
.px_12()
.py_6()
.bg(cx.theme().colors().editor_background)
.child(if let Some(preview) = self.component.preview() {
.child(if let Some(element) = maybe_agent_preview {
// Use agent preview if available
element
} else if let Some(preview) = self.component.preview() {
// Fall back to component preview
preview(window, cx).unwrap_or_else(|| {
div()
.child("Failed to load preview. This path should be unreachable")

View File

@@ -6,7 +6,7 @@ use crate::{
persistence,
};
use crate::{new_session_modal::NewSessionModal, session::DebugSession};
use anyhow::{Result, anyhow};
use anyhow::{Context as _, Result, anyhow};
use collections::{HashMap, HashSet};
use command_palette_hooks::CommandPaletteFilter;
use dap::DebugRequest;
@@ -500,7 +500,7 @@ impl DebugPanel {
workspace.spawn_in_terminal(task.resolved.clone(), window, cx)
})?;
let exit_status = run_build.await?;
let exit_status = run_build.await.transpose()?.context("task cancelled")?;
if !exit_status.success() {
anyhow::bail!("Build failed");
}

View File

@@ -226,7 +226,7 @@ impl ProjectDiagnosticsEditor {
cx.observe_global_in::<IncludeWarnings>(window, |this, window, cx| {
this.include_warnings = cx.global::<IncludeWarnings>().0;
this.diagnostics.clear();
this.update_all_diagnostics(window, cx);
this.update_all_diagnostics(false, window, cx);
})
.detach();
cx.observe_release(&cx.entity(), |editor, _, cx| {
@@ -254,7 +254,7 @@ impl ProjectDiagnosticsEditor {
},
_subscription: project_event_subscription,
};
this.update_all_diagnostics(window, cx);
this.update_all_diagnostics(true, window, cx);
this
}
@@ -346,13 +346,13 @@ impl ProjectDiagnosticsEditor {
if self.cargo_diagnostics_fetch.fetch_task.is_some() {
self.stop_cargo_diagnostics_fetch(cx);
} else {
self.update_all_diagnostics(window, cx);
self.update_all_diagnostics(false, window, cx);
}
} else {
if self.update_excerpts_task.is_some() {
self.update_excerpts_task = None;
} else {
self.update_all_diagnostics(window, cx);
self.update_all_diagnostics(false, window, cx);
}
}
cx.notify();
@@ -371,10 +371,17 @@ impl ProjectDiagnosticsEditor {
}
}
fn update_all_diagnostics(&mut self, window: &mut Window, cx: &mut Context<Self>) {
fn update_all_diagnostics(
&mut self,
first_launch: bool,
window: &mut Window,
cx: &mut Context<Self>,
) {
let cargo_diagnostics_sources = self.cargo_diagnostics_sources(cx);
if cargo_diagnostics_sources.is_empty() {
self.update_all_excerpts(window, cx);
} else if first_launch && !self.summary.is_empty() {
self.update_all_excerpts(window, cx);
} else {
self.fetch_cargo_diagnostics(Arc::new(cargo_diagnostics_sources), cx);
}

View File

@@ -3,6 +3,7 @@ use std::sync::Arc;
use crate::Editor;
use collections::HashMap;
use futures::stream::FuturesUnordered;
use gpui::AsyncApp;
use gpui::{App, AppContext as _, Entity, Task};
use itertools::Itertools;
use language::Buffer;
@@ -74,6 +75,39 @@ where
})
}
async fn lsp_task_context(
project: &Entity<Project>,
buffer: &Entity<Buffer>,
cx: &mut AsyncApp,
) -> Option<TaskContext> {
let worktree_store = project
.update(cx, |project, _| project.worktree_store())
.ok()?;
let worktree_abs_path = cx
.update(|cx| {
let worktree_id = buffer.read(cx).file().map(|f| f.worktree_id(cx));
worktree_id
.and_then(|worktree_id| worktree_store.read(cx).worktree_for_id(worktree_id, cx))
.and_then(|worktree| worktree.read(cx).root_dir())
})
.ok()?;
let project_env = project
.update(cx, |project, cx| {
project.buffer_environment(&buffer, &worktree_store, cx)
})
.ok()?
.await;
Some(TaskContext {
cwd: worktree_abs_path.map(|p| p.to_path_buf()),
project_env: project_env.unwrap_or_default(),
..TaskContext::default()
})
}
pub fn lsp_tasks(
project: Entity<Project>,
task_sources: &HashMap<LanguageServerName, Vec<BufferId>>,
@@ -97,13 +131,16 @@ pub fn lsp_tasks(
cx.spawn(async move |cx| {
let mut lsp_tasks = Vec::new();
let lsp_task_context = TaskContext::default();
while let Some(server_to_query) = lsp_task_sources.next().await {
if let Some((server_id, buffers)) = server_to_query {
let source_kind = TaskSourceKind::Lsp(server_id);
let id_base = source_kind.to_id_base();
let mut new_lsp_tasks = Vec::new();
for buffer in buffers {
let lsp_buffer_context = lsp_task_context(&project, &buffer, cx)
.await
.unwrap_or_default();
if let Ok(runnables_task) = project.update(cx, |project, cx| {
let buffer_id = buffer.read(cx).remote_id();
project.request_lsp(
@@ -120,7 +157,7 @@ pub fn lsp_tasks(
new_lsp_tasks.extend(new_runnables.runnables.into_iter().filter_map(
|(location, runnable)| {
let resolved_task =
runnable.resolve_task(&id_base, &lsp_task_context)?;
runnable.resolve_task(&id_base, &lsp_buffer_context)?;
Some((location, resolved_task))
},
));

View File

@@ -169,11 +169,14 @@ fn main() {
continue;
}
if meta.language_server.map_or(false, |language| {
!languages.contains(&language.file_extension)
}) {
skipped.push(meta.name);
continue;
if let Some(language) = meta.language_server {
if !languages.contains(&language.file_extension) {
panic!(
"Eval for {:?} could not be run because no language server was found for extension {:?}",
meta.name,
language.file_extension
);
}
}
// TODO: This creates a worktree per repetition. Ideally these examples should

View File

@@ -14,12 +14,14 @@ use crate::example::{Example, ExampleContext, ExampleMetadata, JudgeAssertion};
mod add_arg_to_trait_method;
mod code_block_citations;
mod file_search;
mod planets;
pub fn all(examples_dir: &Path) -> Vec<Rc<dyn Example>> {
let mut threads: Vec<Rc<dyn Example>> = vec![
Rc::new(file_search::FileSearchExample),
Rc::new(add_arg_to_trait_method::AddArgToTraitMethod),
Rc::new(code_block_citations::CodeBlockCitations),
Rc::new(planets::Planets),
];
for example_path in list_declarative_examples(examples_dir).unwrap() {

View File

@@ -0,0 +1,73 @@
use anyhow::Result;
use assistant_tool::Tool;
use assistant_tools::{OpenTool, TerminalTool};
use async_trait::async_trait;
use crate::example::{Example, ExampleContext, ExampleMetadata, JudgeAssertion};
pub struct Planets;
#[async_trait(?Send)]
impl Example for Planets {
fn meta(&self) -> ExampleMetadata {
ExampleMetadata {
name: "planets".to_string(),
url: "https://github.com/roc-lang/roc".to_string(), // This commit in this repo is just the Apache2 license,
revision: "59e49c75214f60b4dc4a45092292061c8c26ce27".to_string(), // so effectively a blank project.
language_server: None,
max_assertions: None,
}
}
async fn conversation(&self, cx: &mut ExampleContext) -> Result<()> {
cx.push_user_message(
r#"
Make a plain JavaScript web page which renders an animated 3D solar system.
Let me drag to rotate the camera around.
Do not use npm.
"#
.to_string(),
);
let response = cx.run_to_end().await?;
let mut open_tool_uses = 0;
let mut terminal_tool_uses = 0;
for tool_use in response.tool_uses() {
if tool_use.name == OpenTool.name() {
open_tool_uses += 1;
} else if tool_use.name == TerminalTool.name() {
terminal_tool_uses += 1;
}
}
// The open tool should only be used when requested, which it was not.
cx.assert_eq(open_tool_uses, 0, "`open` tool was not used")
.ok();
// No reason to use the terminal if not using npm.
cx.assert_eq(terminal_tool_uses, 0, "`terminal` tool was not used")
.ok();
Ok(())
}
fn diff_assertions(&self) -> Vec<JudgeAssertion> {
vec![
JudgeAssertion {
id: "animated solar system".to_string(),
description: "This page should render a solar system, and it should be animated."
.to_string(),
},
JudgeAssertion {
id: "drag to rotate camera".to_string(),
description: "The user can drag to rotate the camera around.".to_string(),
},
JudgeAssertion {
id: "plain JavaScript".to_string(),
description:
"The code base uses plain JavaScript and no npm, along with HTML and CSS."
.to_string(),
},
]
}
}

View File

@@ -1,10 +1,10 @@
/// Configuration for a context server.
/// Configuration for context server setup and installation.
#[derive(Debug, Clone)]
pub struct ContextServerConfiguration {
/// Installation instructions for the user.
/// Installation instructions in Markdown format.
pub installation_instructions: String,
/// Default settings for the context server.
pub default_settings: String,
/// JSON schema describing server settings.
/// JSON schema for settings validation.
pub settings_schema: serde_json::Value,
/// Default settings template.
pub default_settings: String,
}

View File

@@ -6,8 +6,7 @@ repository = "https://github.com/zed-industries/zed"
documentation = "https://docs.rs/zed_extension_api"
keywords = ["zed", "extension"]
edition.workspace = true
# Change back to `true` when we're ready to publish v0.5.0.
publish = false
publish = true
license = "Apache-2.0"
[lints]

View File

@@ -23,7 +23,7 @@ need to set your `crate-type` accordingly:
```toml
[dependencies]
zed_extension_api = "0.4.0"
zed_extension_api = "0.5.0"
[lib]
crate-type = ["cdylib"]
@@ -63,6 +63,7 @@ Here is the compatibility of the `zed_extension_api` with versions of Zed:
| Zed version | `zed_extension_api` version |
| ----------- | --------------------------- |
| `0.186.x` | `0.0.1` - `0.5.0` |
| `0.184.x` | `0.0.1` - `0.4.0` |
| `0.178.x` | `0.0.1` - `0.3.0` |
| `0.162.x` | `0.0.1` - `0.2.0` |

View File

@@ -1,11 +1,11 @@
interface context-server {
///
/// Configuration for context server setup and installation.
record context-server-configuration {
///
/// Installation instructions in Markdown format.
installation-instructions: string,
///
/// JSON schema for settings validation.
settings-schema: string,
///
/// Default settings template.
default-settings: string,
}
}

View File

@@ -62,7 +62,7 @@ pub fn wasm_api_version_range(release_channel: ReleaseChannel) -> RangeInclusive
let max_version = match release_channel {
ReleaseChannel::Dev | ReleaseChannel::Nightly => latest::MAX_VERSION,
ReleaseChannel::Stable | ReleaseChannel::Preview => since_v0_4_0::MAX_VERSION,
ReleaseChannel::Stable | ReleaseChannel::Preview => latest::MAX_VERSION,
};
since_v0_0_1::MIN_VERSION..=max_version
@@ -113,8 +113,6 @@ impl Extension {
let _ = release_channel;
if version >= latest::MIN_VERSION {
authorize_access_to_unreleased_wasm_api_version(release_channel)?;
let extension =
latest::Extension::instantiate_async(store, component, latest::linker())
.await

View File

@@ -8,7 +8,6 @@ use wasmtime::component::{Linker, Resource};
use super::latest;
pub const MIN_VERSION: SemanticVersion = SemanticVersion::new(0, 4, 0);
pub const MAX_VERSION: SemanticVersion = SemanticVersion::new(0, 4, 0);
wasmtime::component::bindgen!({
async: true,

View File

@@ -242,6 +242,38 @@ async fn test_matching_paths(cx: &mut TestAppContext) {
}
}
#[gpui::test]
async fn test_unicode_paths(cx: &mut TestAppContext) {
let app_state = init_test(cx);
app_state
.fs
.as_fake()
.insert_tree(
path!("/root"),
json!({
"a": {
"İg": " ",
}
}),
)
.await;
let project = Project::test(app_state.fs.clone(), [path!("/root").as_ref()], cx).await;
let (picker, workspace, cx) = build_find_picker(project, cx);
cx.simulate_input("g");
picker.update(cx, |picker, _| {
assert_eq!(picker.delegate.matches.len(), 1);
});
cx.dispatch_action(SelectNext);
cx.dispatch_action(Confirm);
cx.read(|cx| {
let active_editor = workspace.read(cx).active_item_as::<Editor>(cx).unwrap();
assert_eq!(active_editor.read(cx).title(cx), "İg");
});
}
#[gpui::test]
async fn test_absolute_paths(cx: &mut TestAppContext) {
let app_state = init_test(cx);

View File

@@ -1,5 +1,6 @@
use std::{
borrow::{Borrow, Cow},
collections::BTreeMap,
sync::atomic::{self, AtomicBool},
};
@@ -50,7 +51,7 @@ impl<'a> Matcher<'a> {
/// Filter and score fuzzy match candidates. Results are returned unsorted, in the same order as
/// the input candidates.
pub fn match_candidates<C, R, F, T>(
pub(crate) fn match_candidates<C, R, F, T>(
&mut self,
prefix: &[char],
lowercase_prefix: &[char],
@@ -65,6 +66,7 @@ impl<'a> Matcher<'a> {
{
let mut candidate_chars = Vec::new();
let mut lowercase_candidate_chars = Vec::new();
let mut extra_lowercase_chars = BTreeMap::new();
for candidate in candidates {
if !candidate.borrow().has_chars(self.query_char_bag) {
@@ -77,9 +79,14 @@ impl<'a> Matcher<'a> {
candidate_chars.clear();
lowercase_candidate_chars.clear();
for c in candidate.borrow().to_string().chars() {
extra_lowercase_chars.clear();
for (i, c) in candidate.borrow().to_string().chars().enumerate() {
candidate_chars.push(c);
lowercase_candidate_chars.append(&mut c.to_lowercase().collect::<Vec<_>>());
let mut char_lowercased = c.to_lowercase().collect::<Vec<_>>();
if char_lowercased.len() > 1 {
extra_lowercase_chars.insert(i, char_lowercased.len() - 1);
}
lowercase_candidate_chars.append(&mut char_lowercased);
}
if !self.find_last_positions(lowercase_prefix, &lowercase_candidate_chars) {
@@ -97,6 +104,7 @@ impl<'a> Matcher<'a> {
&lowercase_candidate_chars,
prefix,
lowercase_prefix,
&extra_lowercase_chars,
);
if score > 0.0 {
@@ -131,18 +139,20 @@ impl<'a> Matcher<'a> {
fn score_match(
&mut self,
path: &[char],
path_cased: &[char],
path_lowercased: &[char],
prefix: &[char],
lowercase_prefix: &[char],
extra_lowercase_chars: &BTreeMap<usize, usize>,
) -> f64 {
let score = self.recursive_score_match(
path,
path_cased,
path_lowercased,
prefix,
lowercase_prefix,
0,
0,
self.query.len() as f64,
extra_lowercase_chars,
) * self.query.len() as f64;
if score <= 0.0 {
@@ -173,12 +183,13 @@ impl<'a> Matcher<'a> {
fn recursive_score_match(
&mut self,
path: &[char],
path_cased: &[char],
path_lowercased: &[char],
prefix: &[char],
lowercase_prefix: &[char],
query_idx: usize,
path_idx: usize,
cur_score: f64,
extra_lowercase_chars: &BTreeMap<usize, usize>,
) -> f64 {
use std::path::MAIN_SEPARATOR;
@@ -200,15 +211,22 @@ impl<'a> Matcher<'a> {
let mut last_slash = 0;
for j in path_idx..=limit {
let path_char = if j < prefix.len() {
let extra_lowercase_chars_count = extra_lowercase_chars
.iter()
.take_while(|(i, _)| i < &&j)
.map(|(_, increment)| increment)
.sum::<usize>();
let j_regular = j - extra_lowercase_chars_count;
let path_char = if j_regular < prefix.len() {
lowercase_prefix[j]
} else {
path_cased[j - prefix.len()]
path_lowercased[j - prefix.len()]
};
let is_path_sep = path_char == MAIN_SEPARATOR;
if query_idx == 0 && is_path_sep {
last_slash = j;
last_slash = j_regular;
}
#[cfg(not(target_os = "windows"))]
@@ -218,18 +236,18 @@ impl<'a> Matcher<'a> {
#[cfg(target_os = "windows")]
let need_to_score = query_char == path_char || (is_path_sep && query_char == '_');
if need_to_score {
let curr = if j < prefix.len() {
prefix[j]
let curr = if j_regular < prefix.len() {
prefix[j_regular]
} else {
path[j - prefix.len()]
path[j_regular - prefix.len()]
};
let mut char_score = 1.0;
if j > path_idx {
let last = if j - 1 < prefix.len() {
prefix[j - 1]
let last = if j_regular - 1 < prefix.len() {
prefix[j_regular - 1]
} else {
path[j - 1 - prefix.len()]
path[j_regular - 1 - prefix.len()]
};
if last == MAIN_SEPARATOR {
@@ -279,17 +297,18 @@ impl<'a> Matcher<'a> {
let new_score = self.recursive_score_match(
path,
path_cased,
path_lowercased,
prefix,
lowercase_prefix,
query_idx + 1,
j + 1,
next_score,
extra_lowercase_chars,
) * multiplier;
if new_score > score {
score = new_score;
best_position = j;
best_position = j_regular;
// Optimization: can't score better than 1.
if new_score == 1.0 {
break;

View File

@@ -59,8 +59,8 @@ impl ProjectEnvironment {
pub(crate) fn get_buffer_environment(
&mut self,
buffer: Entity<Buffer>,
worktree_store: Entity<WorktreeStore>,
buffer: &Entity<Buffer>,
worktree_store: &Entity<WorktreeStore>,
cx: &mut Context<Self>,
) -> Shared<Task<Option<HashMap<String, String>>>> {
if cfg!(any(test, feature = "test-support")) {

View File

@@ -8190,7 +8190,7 @@ impl LspStore {
) -> Shared<Task<Option<HashMap<String, String>>>> {
if let Some(environment) = &self.as_local().map(|local| local.environment.clone()) {
environment.update(cx, |env, cx| {
env.get_buffer_environment(buffer.clone(), self.worktree_store.clone(), cx)
env.get_buffer_environment(&buffer, &self.worktree_store, cx)
})
} else {
Task::ready(None).shared()

View File

@@ -56,7 +56,7 @@ use futures::future::join_all;
use futures::{
StreamExt,
channel::mpsc::{self, UnboundedReceiver},
future::try_join_all,
future::{Shared, try_join_all},
};
pub use image_store::{ImageItem, ImageStore};
use image_store::{ImageItemEvent, ImageStoreEvent};
@@ -1605,6 +1605,17 @@ impl Project {
self.environment.read(cx).get_cli_environment()
}
pub fn buffer_environment<'a>(
&'a self,
buffer: &Entity<Buffer>,
worktree_store: &Entity<WorktreeStore>,
cx: &'a mut App,
) -> Shared<Task<Option<HashMap<String, String>>>> {
self.environment.update(cx, |environment, cx| {
environment.get_buffer_environment(&buffer, &worktree_store, cx)
})
}
pub fn shell_environment_errors<'a>(
&'a self,
cx: &'a App,

View File

@@ -315,11 +315,7 @@ fn local_task_context_for_location(
cx.spawn(async move |cx| {
let project_env = environment
.update(cx, |environment, cx| {
environment.get_buffer_environment(
location.buffer.clone(),
worktree_store.clone(),
cx,
)
environment.get_buffer_environment(&location.buffer, &worktree_store, cx)
})
.ok()?
.await;

View File

@@ -46,7 +46,7 @@ use smol::channel::{Receiver, Sender};
use task::{HideStrategy, Shell, TaskId};
use terminal_settings::{AlternateScroll, CursorShape, TerminalSettings};
use theme::{ActiveTheme, Theme};
use util::{ResultExt, paths::home_dir, truncate_and_trailoff};
use util::{paths::home_dir, truncate_and_trailoff};
use std::{
cmp::{self, min},
@@ -1851,8 +1851,7 @@ impl Terminal {
if let Some(task) = self.task() {
if task.status == TaskStatus::Running {
let completion_receiver = task.completion_rx.clone();
return cx
.spawn(async move |_| completion_receiver.recv().await.log_err().flatten());
return cx.spawn(async move |_| completion_receiver.recv().await.ok().flatten());
} else if let Ok(status) = task.completion_rx.try_recv() {
return Task::ready(status);
}

View File

@@ -483,7 +483,7 @@ impl TerminalPanel {
task: &SpawnInTerminal,
window: &mut Window,
cx: &mut Context<Self>,
) -> Task<Result<Entity<Terminal>>> {
) -> Task<Result<WeakEntity<Terminal>>> {
let Ok(is_local) = self
.workspace
.update(cx, |workspace, cx| workspace.project().read(cx).is_local())
@@ -552,12 +552,12 @@ impl TerminalPanel {
cx.spawn(async move |_, _| rx.await?)
}
pub fn spawn_in_new_terminal(
fn spawn_in_new_terminal(
&mut self,
spawn_task: SpawnInTerminal,
window: &mut Window,
cx: &mut Context<Self>,
) -> Task<Result<Entity<Terminal>>> {
) -> Task<Result<WeakEntity<Terminal>>> {
let reveal = spawn_task.reveal;
let reveal_target = spawn_task.reveal_target;
let kind = TerminalKind::Task(spawn_task);
@@ -652,7 +652,7 @@ impl TerminalPanel {
kind: TerminalKind,
window: &mut Window,
cx: &mut Context<Workspace>,
) -> Task<Result<Entity<Terminal>>> {
) -> Task<Result<WeakEntity<Terminal>>> {
if !is_enabled_in_workspace(workspace, cx) {
return Task::ready(Err(anyhow!(
"terminal not yet supported for remote projects"
@@ -680,7 +680,7 @@ impl TerminalPanel {
});
workspace.add_item_to_active_pane(Box::new(terminal_view), None, true, window, cx);
})?;
Ok(terminal)
Ok(terminal.downgrade())
})
}
@@ -690,7 +690,7 @@ impl TerminalPanel {
reveal_strategy: RevealStrategy,
window: &mut Window,
cx: &mut Context<Self>,
) -> Task<Result<Entity<Terminal>>> {
) -> Task<Result<WeakEntity<Terminal>>> {
let workspace = self.workspace.clone();
cx.spawn_in(window, async move |terminal_panel, cx| {
if workspace.update(cx, |workspace, cx| !is_enabled_in_workspace(workspace, cx))? {
@@ -735,11 +735,12 @@ impl TerminalPanel {
pane.add_item(terminal_view, true, focus, None, window, cx);
});
Ok(terminal)
Ok(terminal.downgrade())
})?;
terminal_panel.update(cx, |this, cx| {
this.pending_terminals_to_add = this.pending_terminals_to_add.saturating_sub(1);
this.serialize(cx)
terminal_panel.update(cx, |terminal_panel, cx| {
terminal_panel.pending_terminals_to_add =
terminal_panel.pending_terminals_to_add.saturating_sub(1);
terminal_panel.serialize(cx)
})?;
result
})
@@ -802,7 +803,7 @@ impl TerminalPanel {
terminal_to_replace: Entity<TerminalView>,
window: &mut Window,
cx: &mut Context<Self>,
) -> Task<Result<Entity<Terminal>>> {
) -> Task<Result<WeakEntity<Terminal>>> {
let reveal = spawn_task.reveal;
let reveal_target = spawn_task.reveal_target;
let window_handle = window.window_handle();
@@ -884,7 +885,7 @@ impl TerminalPanel {
RevealStrategy::Never => {}
}
Ok(new_terminal)
Ok(new_terminal.downgrade())
})
}
@@ -1458,22 +1459,25 @@ impl workspace::TerminalProvider for TerminalProvider {
task: SpawnInTerminal,
window: &mut Window,
cx: &mut App,
) -> Task<Result<ExitStatus>> {
let this = self.0.clone();
) -> Task<Option<Result<ExitStatus>>> {
let terminal_panel = self.0.clone();
window.spawn(cx, async move |cx| {
let terminal = this
let terminal = terminal_panel
.update_in(cx, |terminal_panel, window, cx| {
terminal_panel.spawn_task(&task, window, cx)
})?
.await?;
let Some(exit_code) = terminal
.read_with(cx, |terminal, cx| terminal.wait_for_completed_task(cx))?
.await
else {
return Err(anyhow!("Task cancelled"));
};
Ok(exit_code)
})
.ok()?
.await;
match terminal {
Ok(terminal) => {
let exit_status = terminal
.read_with(cx, |terminal, cx| terminal.wait_for_completed_task(cx))
.ok()?
.await?;
Some(Ok(exit_status))
}
Err(e) => Some(Err(e)),
}
})
}
}

View File

@@ -1466,9 +1466,22 @@ impl ShellExec {
show_command: false,
show_rerun: false,
};
workspace
.spawn_in_terminal(spawn_in_terminal, window, cx)
.detach_and_log_err(cx);
let task_status = workspace.spawn_in_terminal(spawn_in_terminal, window, cx);
cx.background_spawn(async move {
match task_status.await {
Some(Ok(status)) => {
if status.success() {
log::debug!("Vim shell exec succeeded");
} else {
log::debug!("Vim shell exec failed, code: {:?}", status.code());
}
}
Some(Err(e)) => log::error!("Vim shell exec failed: {e}"),
None => log::debug!("Vim shell exec got cancelled"),
}
})
.detach();
});
return;
};

View File

@@ -1,7 +1,7 @@
mod cloud;
use client::Client;
use gpui::{App, Context};
use gpui::{App, Context, Entity};
use language_model::LanguageModelRegistry;
use std::sync::Arc;
use web_search::{WebSearchProviderId, WebSearchRegistry};
@@ -14,31 +14,44 @@ pub fn init(client: Arc<Client>, cx: &mut App) {
}
fn register_web_search_providers(
_registry: &mut WebSearchRegistry,
registry: &mut WebSearchRegistry,
client: Arc<Client>,
cx: &mut Context<WebSearchRegistry>,
) {
register_zed_web_search_provider(
registry,
client.clone(),
&LanguageModelRegistry::global(cx),
cx,
);
cx.subscribe(
&LanguageModelRegistry::global(cx),
move |this, registry, event, cx| match event {
language_model::Event::DefaultModelChanged => {
let using_zed_provider = registry
.read(cx)
.default_model()
.map_or(false, |default| default.is_provided_by_zed());
if using_zed_provider {
this.register_provider(
cloud::CloudWebSearchProvider::new(client.clone(), cx),
cx,
)
} else {
this.unregister_provider(WebSearchProviderId(
cloud::ZED_WEB_SEARCH_PROVIDER_ID.into(),
));
}
register_zed_web_search_provider(this, client.clone(), &registry, cx)
}
_ => {}
},
)
.detach();
}
fn register_zed_web_search_provider(
registry: &mut WebSearchRegistry,
client: Arc<Client>,
language_model_registry: &Entity<LanguageModelRegistry>,
cx: &mut Context<WebSearchRegistry>,
) {
let using_zed_provider = language_model_registry
.read(cx)
.default_model()
.map_or(false, |default| default.is_provided_by_zed());
if using_zed_provider {
registry.register_provider(cloud::CloudWebSearchProvider::new(client, cx), cx)
} else {
registry.unregister_provider(WebSearchProviderId(
cloud::ZED_WEB_SEARCH_PROVIDER_ID.into(),
));
}
}

View File

@@ -1,7 +1,7 @@
use std::process::ExitStatus;
use anyhow::{Result, anyhow};
use gpui::{Context, Entity, Task};
use anyhow::Result;
use gpui::{AppContext, Context, Entity, Task};
use language::Buffer;
use project::TaskSourceKind;
use remote::ConnectionState;
@@ -68,9 +68,21 @@ impl Workspace {
}
if let Some(terminal_provider) = self.terminal_provider.as_ref() {
terminal_provider
.spawn(spawn_in_terminal, window, cx)
.detach_and_log_err(cx);
let task_status = terminal_provider.spawn(spawn_in_terminal, window, cx);
cx.background_spawn(async move {
match task_status.await {
Some(Ok(status)) => {
if status.success() {
log::debug!("Task spawn succeeded");
} else {
log::debug!("Task spawn failed, code: {:?}", status.code());
}
}
Some(Err(e)) => log::error!("Task spawn failed: {e}"),
None => log::debug!("Task spawn got cancelled"),
}
})
.detach();
}
}
@@ -92,11 +104,11 @@ impl Workspace {
spawn_in_terminal: SpawnInTerminal,
window: &mut Window,
cx: &mut Context<Workspace>,
) -> Task<Result<ExitStatus>> {
) -> Task<Option<Result<ExitStatus>>> {
if let Some(terminal_provider) = self.terminal_provider.as_ref() {
terminal_provider.spawn(spawn_in_terminal, window, cx)
} else {
Task::ready(Err(anyhow!("No terminal provider")))
Task::ready(None)
}
}
}

View File

@@ -136,7 +136,7 @@ pub trait TerminalProvider {
task: SpawnInTerminal,
window: &mut Window,
cx: &mut App,
) -> Task<Result<ExitStatus>>;
) -> Task<Option<Result<ExitStatus>>>;
}
pub trait DebuggerProvider {