{
- let colors = cx.theme().colors();
- div().id(("message-container", ix)).py_1().px_2().child(
- v_flex()
- .w_full()
- .bg(colors.editor_background)
- .rounded_sm()
- .child(
- h_flex()
- .w_full()
- .p_2()
- .gap_2()
- .child(
- div().flex_none().child(
- Icon::new(IconName::Warning)
- .size(IconSize::Small)
- .color(Color::Warning),
- ),
- )
- .child(
- v_flex()
- .flex_1()
- .min_w_0()
- .text_size(TextSize::Small.rems(cx))
- .text_color(cx.theme().colors().text_muted)
- .children(message_content),
- ),
- ),
- )
+ let message = div()
+ .flex_1()
+ .min_w_0()
+ .text_size(TextSize::XSmall.rems(cx))
+ .text_color(cx.theme().colors().text_muted)
+ .children(message_content);
+
+ div()
+ .id(("message-container", ix))
+ .py_1()
+ .px_2p5()
+ .child(Banner::new().severity(ui::Severity::Warning).child(message))
}
fn render_message_thinking_segment(
diff --git a/crates/agent_ui/src/agent_configuration.rs b/crates/agent_ui/src/agent_configuration.rs
index e91a0f7ebe590d1f0480741f6eec3ebda220ccea..4aa9c3fc38b2555e2675d668aa534d9c0125da7e 100644
--- a/crates/agent_ui/src/agent_configuration.rs
+++ b/crates/agent_ui/src/agent_configuration.rs
@@ -16,7 +16,9 @@ use gpui::{
Focusable, ScrollHandle, Subscription, Task, Transformation, WeakEntity, percentage,
};
use language::LanguageRegistry;
-use language_model::{LanguageModelProvider, LanguageModelProviderId, LanguageModelRegistry};
+use language_model::{
+ LanguageModelProvider, LanguageModelProviderId, LanguageModelRegistry, ZED_CLOUD_PROVIDER_ID,
+};
use notifications::status_toast::{StatusToast, ToastIcon};
use project::{
context_server_store::{ContextServerConfiguration, ContextServerStatus, ContextServerStore},
@@ -86,6 +88,14 @@ impl AgentConfiguration {
let scroll_handle = ScrollHandle::new();
let scrollbar_state = ScrollbarState::new(scroll_handle.clone());
+ let mut expanded_provider_configurations = HashMap::default();
+ if LanguageModelRegistry::read_global(cx)
+ .provider(&ZED_CLOUD_PROVIDER_ID)
+ .map_or(false, |cloud_provider| cloud_provider.must_accept_terms(cx))
+ {
+ expanded_provider_configurations.insert(ZED_CLOUD_PROVIDER_ID, true);
+ }
+
let mut this = Self {
fs,
language_registry,
@@ -94,7 +104,7 @@ impl AgentConfiguration {
configuration_views_by_provider: HashMap::default(),
context_server_store,
expanded_context_server_tools: HashMap::default(),
- expanded_provider_configurations: HashMap::default(),
+ expanded_provider_configurations,
tools,
_registry_subscription: registry_subscription,
scroll_handle,
diff --git a/crates/agent_ui/src/agent_configuration/configure_context_server_modal.rs b/crates/agent_ui/src/agent_configuration/configure_context_server_modal.rs
index 30fad51cfcbc100bdf469278c0210a220c7e2833..299f3cee34b1c7635c3c0a8f46a52cc730993b01 100644
--- a/crates/agent_ui/src/agent_configuration/configure_context_server_modal.rs
+++ b/crates/agent_ui/src/agent_configuration/configure_context_server_modal.rs
@@ -180,7 +180,7 @@ impl ConfigurationSource {
}
fn context_server_input(existing: Option<(ContextServerId, ContextServerCommand)>) -> String {
- let (name, path, args, env) = match existing {
+ let (name, command, args, env) = match existing {
Some((id, cmd)) => {
let args = serde_json::to_string(&cmd.args).unwrap();
let env = serde_json::to_string(&cmd.env.unwrap_or_default()).unwrap();
@@ -198,14 +198,12 @@ fn context_server_input(existing: Option<(ContextServerId, ContextServerCommand)
r#"{{
/// The name of your MCP server
"{name}": {{
- "command": {{
- /// The path to the executable
- "path": "{path}",
- /// The arguments to pass to the executable
- "args": {args},
- /// The environment variables to set for the executable
- "env": {env}
- }}
+ /// The command which runs the MCP server
+ "command": "{command}",
+ /// The arguments to pass to the MCP server
+ "args": {args},
+ /// The environment variables to set
+ "env": {env}
}}
}}"#
)
@@ -439,8 +437,7 @@ fn parse_input(text: &str) -> Result<(ContextServerId, ContextServerCommand)> {
let object = value.as_object().context("Expected object")?;
anyhow::ensure!(object.len() == 1, "Expected exactly one key-value pair");
let (context_server_name, value) = object.into_iter().next().unwrap();
- let command = value.get("command").context("Expected command")?;
- let command: ContextServerCommand = serde_json::from_value(command.clone())?;
+ let command: ContextServerCommand = serde_json::from_value(value.clone())?;
Ok((ContextServerId(context_server_name.clone().into()), command))
}
@@ -748,7 +745,7 @@ pub(crate) fn default_markdown_style(window: &Window, cx: &App) -> MarkdownStyle
MarkdownStyle {
base_text_style: text_style.clone(),
- selection_background_color: cx.theme().players().local().selection,
+ selection_background_color: colors.element_selection_background,
link: TextStyleRefinement {
background_color: Some(colors.editor_foreground.opacity(0.025)),
underline: Some(UnderlineStyle {
diff --git a/crates/agent_ui/src/agent_diff.rs b/crates/agent_ui/src/agent_diff.rs
index b8e67512e2b069f2a4f19c4903512f385c4eeab7..1a0f3ff27d83a98d343985b3f827aab26afd192a 100644
--- a/crates/agent_ui/src/agent_diff.rs
+++ b/crates/agent_ui/src/agent_diff.rs
@@ -5,7 +5,8 @@ use anyhow::Result;
use buffer_diff::DiffHunkStatus;
use collections::{HashMap, HashSet};
use editor::{
- Direction, Editor, EditorEvent, EditorSettings, MultiBuffer, MultiBufferSnapshot, ToPoint,
+ Direction, Editor, EditorEvent, EditorSettings, MultiBuffer, MultiBufferSnapshot,
+ SelectionEffects, ToPoint,
actions::{GoToHunk, GoToPreviousHunk},
scroll::Autoscroll,
};
@@ -171,15 +172,9 @@ impl AgentDiffPane {
if let Some(first_hunk) = first_hunk {
let first_hunk_start = first_hunk.multi_buffer_range().start;
- editor.change_selections(
- Some(Autoscroll::fit()),
- window,
- cx,
- |selections| {
- selections
- .select_anchor_ranges([first_hunk_start..first_hunk_start]);
- },
- )
+ editor.change_selections(Default::default(), window, cx, |selections| {
+ selections.select_anchor_ranges([first_hunk_start..first_hunk_start]);
+ })
}
}
@@ -242,7 +237,7 @@ impl AgentDiffPane {
if let Some(first_hunk) = first_hunk {
let first_hunk_start = first_hunk.multi_buffer_range().start;
- editor.change_selections(Some(Autoscroll::fit()), window, cx, |selections| {
+ editor.change_selections(Default::default(), window, cx, |selections| {
selections.select_anchor_ranges([first_hunk_start..first_hunk_start]);
})
}
@@ -416,7 +411,7 @@ fn update_editor_selection(
};
if let Some(target_hunk) = target_hunk {
- editor.change_selections(Some(Autoscroll::fit()), window, cx, |selections| {
+ editor.change_selections(Default::default(), window, cx, |selections| {
let next_hunk_start = target_hunk.multi_buffer_range().start;
selections.select_anchor_ranges([next_hunk_start..next_hunk_start]);
})
@@ -1544,7 +1539,7 @@ impl AgentDiff {
let first_hunk_start = first_hunk.multi_buffer_range().start;
editor.change_selections(
- Some(Autoscroll::center()),
+ SelectionEffects::scroll(Autoscroll::center()),
window,
cx,
|selections| {
@@ -1868,7 +1863,7 @@ mod tests {
// Rejecting a hunk also moves the cursor to the next hunk, possibly cycling if it's at the end.
editor.update_in(cx, |editor, window, cx| {
- editor.change_selections(None, window, cx, |selections| {
+ editor.change_selections(SelectionEffects::no_scroll(), window, cx, |selections| {
selections.select_ranges([Point::new(10, 0)..Point::new(10, 0)])
});
});
@@ -2124,7 +2119,7 @@ mod tests {
// Rejecting a hunk also moves the cursor to the next hunk, possibly cycling if it's at the end.
editor1.update_in(cx, |editor, window, cx| {
- editor.change_selections(None, window, cx, |selections| {
+ editor.change_selections(SelectionEffects::no_scroll(), window, cx, |selections| {
selections.select_ranges([Point::new(10, 0)..Point::new(10, 0)])
});
});
diff --git a/crates/agent_ui/src/agent_model_selector.rs b/crates/agent_ui/src/agent_model_selector.rs
index c8b628c938e5be37ee3e10ca2a46dd2e59c78e84..f7b9157bbb9c07abac6a80dddfc014443165a712 100644
--- a/crates/agent_ui/src/agent_model_selector.rs
+++ b/crates/agent_ui/src/agent_model_selector.rs
@@ -11,7 +11,7 @@ use language_model::{ConfiguredModel, LanguageModelRegistry};
use picker::popover_menu::PickerPopoverMenu;
use settings::update_settings_file;
use std::sync::Arc;
-use ui::{PopoverMenuHandle, Tooltip, prelude::*};
+use ui::{ButtonLike, PopoverMenuHandle, Tooltip, prelude::*};
pub struct AgentModelSelector {
selector: Entity
,
@@ -94,20 +94,35 @@ impl Render for AgentModelSelector {
fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement {
let model = self.selector.read(cx).delegate.active_model(cx);
let model_name = model
+ .as_ref()
.map(|model| model.model.name().0)
.unwrap_or_else(|| SharedString::from("No model selected"));
+ let provider_icon = model
+ .as_ref()
+ .map(|model| model.provider.icon())
+ .unwrap_or_else(|| IconName::Ai);
let focus_handle = self.focus_handle.clone();
PickerPopoverMenu::new(
self.selector.clone(),
- Button::new("active-model", model_name)
- .label_size(LabelSize::Small)
- .color(Color::Muted)
- .icon(IconName::ChevronDown)
- .icon_size(IconSize::XSmall)
- .icon_position(IconPosition::End)
- .icon_color(Color::Muted),
+ ButtonLike::new("active-model")
+ .child(
+ Icon::new(provider_icon)
+ .color(Color::Muted)
+ .size(IconSize::XSmall),
+ )
+ .child(
+ Label::new(model_name)
+ .color(Color::Muted)
+ .size(LabelSize::Small)
+ .ml_0p5(),
+ )
+ .child(
+ Icon::new(IconName::ChevronDown)
+ .color(Color::Muted)
+ .size(IconSize::XSmall),
+ ),
move |window, cx| {
Tooltip::for_action_in(
"Change Model",
diff --git a/crates/agent_ui/src/agent_panel.rs b/crates/agent_ui/src/agent_panel.rs
index 938c1771bad1bb6d6ac56f2cf41d77aa5a8de308..d07521f3ee0fbe2c5310e4a232b6213039bed6bd 100644
--- a/crates/agent_ui/src/agent_panel.rs
+++ b/crates/agent_ui/src/agent_panel.rs
@@ -43,7 +43,7 @@ use editor::{Anchor, AnchorRangeExt as _, Editor, EditorEvent, MultiBuffer};
use fs::Fs;
use gpui::{
Action, Animation, AnimationExt as _, AnyElement, App, AsyncWindowContext, ClipboardItem,
- Corner, DismissEvent, Entity, EventEmitter, ExternalPaths, FocusHandle, Focusable, FontWeight,
+ Corner, DismissEvent, Entity, EventEmitter, ExternalPaths, FocusHandle, Focusable, Hsla,
KeyContext, Pixels, Subscription, Task, UpdateGlobal, WeakEntity, linear_color_stop,
linear_gradient, prelude::*, pulsating_between,
};
@@ -61,7 +61,7 @@ use theme::ThemeSettings;
use time::UtcOffset;
use ui::utils::WithRemSize;
use ui::{
- Banner, CheckboxWithLabel, ContextMenu, ElevationIndex, KeyBinding, PopoverMenu,
+ Banner, Callout, CheckboxWithLabel, ContextMenu, ElevationIndex, KeyBinding, PopoverMenu,
PopoverMenuHandle, ProgressBar, Tab, Tooltip, Vector, VectorName, prelude::*,
};
use util::ResultExt as _;
@@ -2124,9 +2124,7 @@ impl AgentPanel {
.thread()
.read(cx)
.configured_model()
- .map_or(false, |model| {
- model.provider.id().0 == ZED_CLOUD_PROVIDER_ID
- });
+ .map_or(false, |model| model.provider.id() == ZED_CLOUD_PROVIDER_ID);
if !is_using_zed_provider {
return false;
@@ -2703,7 +2701,7 @@ impl AgentPanel {
Some(ConfigurationError::ProviderPendingTermsAcceptance(provider)) => {
parent.child(Banner::new().severity(ui::Severity::Warning).child(
h_flex().w_full().children(provider.render_accept_terms(
- LanguageModelProviderTosView::ThreadtEmptyState,
+ LanguageModelProviderTosView::ThreadEmptyState,
cx,
)),
))
@@ -2763,7 +2761,7 @@ impl AgentPanel {
this.continue_conversation(window, cx);
})),
)
- .when(model.supports_max_mode(), |this| {
+ .when(model.supports_burn_mode(), |this| {
this.child(
Button::new("continue-burn-mode", "Continue with Burn Mode")
.style(ButtonStyle::Filled)
@@ -2798,58 +2796,90 @@ impl AgentPanel {
Some(div().px_2().pb_2().child(banner).into_any_element())
}
+ fn create_copy_button(&self, message: impl Into) -> impl IntoElement {
+ let message = message.into();
+
+ IconButton::new("copy", IconName::Copy)
+ .icon_size(IconSize::Small)
+ .icon_color(Color::Muted)
+ .tooltip(Tooltip::text("Copy Error Message"))
+ .on_click(move |_, _, cx| {
+ cx.write_to_clipboard(ClipboardItem::new_string(message.clone()))
+ })
+ }
+
+ fn dismiss_error_button(
+ &self,
+ thread: &Entity,
+ cx: &mut Context,
+ ) -> impl IntoElement {
+ IconButton::new("dismiss", IconName::Close)
+ .icon_size(IconSize::Small)
+ .icon_color(Color::Muted)
+ .tooltip(Tooltip::text("Dismiss Error"))
+ .on_click(cx.listener({
+ let thread = thread.clone();
+ move |_, _, _, cx| {
+ thread.update(cx, |this, _cx| {
+ this.clear_last_error();
+ });
+
+ cx.notify();
+ }
+ }))
+ }
+
+ fn upgrade_button(
+ &self,
+ thread: &Entity,
+ cx: &mut Context,
+ ) -> impl IntoElement {
+ Button::new("upgrade", "Upgrade")
+ .label_size(LabelSize::Small)
+ .style(ButtonStyle::Tinted(ui::TintColor::Accent))
+ .on_click(cx.listener({
+ let thread = thread.clone();
+ move |_, _, _, cx| {
+ thread.update(cx, |this, _cx| {
+ this.clear_last_error();
+ });
+
+ cx.open_url(&zed_urls::account_url(cx));
+ cx.notify();
+ }
+ }))
+ }
+
+ fn error_callout_bg(&self, cx: &Context) -> Hsla {
+ cx.theme().status().error.opacity(0.08)
+ }
+
fn render_payment_required_error(
&self,
thread: &Entity,
cx: &mut Context,
) -> AnyElement {
- const ERROR_MESSAGE: &str = "Free tier exceeded. Subscribe and add payment to continue using Zed LLMs. You'll be billed at cost for tokens used.";
-
- v_flex()
- .gap_0p5()
- .child(
- h_flex()
- .gap_1p5()
- .items_center()
- .child(Icon::new(IconName::XCircle).color(Color::Error))
- .child(Label::new("Free Usage Exceeded").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()
- .gap_1()
- .child(self.create_copy_button(ERROR_MESSAGE))
- .child(Button::new("subscribe", "Subscribe").on_click(cx.listener({
- let thread = thread.clone();
- move |_, _, _, cx| {
- thread.update(cx, |this, _cx| {
- this.clear_last_error();
- });
+ const ERROR_MESSAGE: &str =
+ "You reached your free usage limit. Upgrade to Zed Pro for more prompts.";
- cx.open_url(&zed_urls::account_url(cx));
- cx.notify();
- }
- })))
- .child(Button::new("dismiss", "Dismiss").on_click(cx.listener({
- let thread = thread.clone();
- move |_, _, _, cx| {
- thread.update(cx, |this, _cx| {
- this.clear_last_error();
- });
+ let icon = Icon::new(IconName::XCircle)
+ .size(IconSize::Small)
+ .color(Color::Error);
- cx.notify();
- }
- }))),
+ div()
+ .border_t_1()
+ .border_color(cx.theme().colors().border)
+ .child(
+ Callout::new()
+ .icon(icon)
+ .title("Free Usage Exceeded")
+ .description(ERROR_MESSAGE)
+ .tertiary_action(self.upgrade_button(thread, cx))
+ .secondary_action(self.create_copy_button(ERROR_MESSAGE))
+ .primary_action(self.dismiss_error_button(thread, cx))
+ .bg_color(self.error_callout_bg(cx)),
)
- .into_any()
+ .into_any_element()
}
fn render_model_request_limit_reached_error(
@@ -2859,67 +2889,28 @@ impl AgentPanel {
cx: &mut Context,
) -> 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",
+ Plan::ZedPro => "Upgrade to usage-based billing for more prompts.",
+ Plan::ZedProTrial | Plan::Free => "Upgrade to Zed Pro for more prompts.",
};
- 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()
- .gap_1()
- .child(self.create_copy_button(error_message))
- .child(
- Button::new("subscribe", call_to_action).on_click(cx.listener({
- let thread = thread.clone();
- move |_, _, _, cx| {
- 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({
- let thread = thread.clone();
- move |_, _, _, cx| {
- thread.update(cx, |this, _cx| {
- this.clear_last_error();
- });
+ let icon = Icon::new(IconName::XCircle)
+ .size(IconSize::Small)
+ .color(Color::Error);
- cx.notify();
- }
- }))),
+ div()
+ .border_t_1()
+ .border_color(cx.theme().colors().border)
+ .child(
+ Callout::new()
+ .icon(icon)
+ .title("Model Prompt Limit Reached")
+ .description(error_message)
+ .tertiary_action(self.upgrade_button(thread, cx))
+ .secondary_action(self.create_copy_button(error_message))
+ .primary_action(self.dismiss_error_button(thread, cx))
+ .bg_color(self.error_callout_bg(cx)),
)
- .into_any()
+ .into_any_element()
}
fn render_error_message(
@@ -2930,40 +2921,24 @@ impl AgentPanel {
cx: &mut Context,
) -> AnyElement {
let message_with_header = format!("{}\n{}", header, message);
- v_flex()
- .gap_0p5()
- .child(
- h_flex()
- .gap_1p5()
- .items_center()
- .child(Icon::new(IconName::XCircle).color(Color::Error))
- .child(Label::new(header).weight(FontWeight::MEDIUM)),
- )
- .child(
- div()
- .id("error-message")
- .max_h_32()
- .overflow_y_scroll()
- .child(Label::new(message.clone())),
- )
- .child(
- h_flex()
- .justify_end()
- .mt_1()
- .gap_1()
- .child(self.create_copy_button(message_with_header))
- .child(Button::new("dismiss", "Dismiss").on_click(cx.listener({
- let thread = thread.clone();
- move |_, _, _, cx| {
- thread.update(cx, |this, _cx| {
- this.clear_last_error();
- });
- cx.notify();
- }
- }))),
+ let icon = Icon::new(IconName::XCircle)
+ .size(IconSize::Small)
+ .color(Color::Error);
+
+ div()
+ .border_t_1()
+ .border_color(cx.theme().colors().border)
+ .child(
+ Callout::new()
+ .icon(icon)
+ .title(header)
+ .description(message.clone())
+ .primary_action(self.dismiss_error_button(thread, cx))
+ .secondary_action(self.create_copy_button(message_with_header))
+ .bg_color(self.error_callout_bg(cx)),
)
- .into_any()
+ .into_any_element()
}
fn render_prompt_editor(
@@ -3111,15 +3086,6 @@ impl AgentPanel {
}
}
- fn create_copy_button(&self, message: impl Into) -> impl IntoElement {
- let message = message.into();
- IconButton::new("copy", IconName::Copy)
- .on_click(move |_, _, cx| {
- cx.write_to_clipboard(ClipboardItem::new_string(message.clone()))
- })
- .tooltip(Tooltip::text("Copy Error Message"))
- }
-
fn key_context(&self) -> KeyContext {
let mut key_context = KeyContext::new_with_defaults();
key_context.add("AgentPanel");
@@ -3204,18 +3170,9 @@ impl Render for AgentPanel {
thread.clone().into_any_element()
})
.children(self.render_tool_use_limit_reached(window, cx))
- .child(h_flex().child(message_editor.clone()))
.when_some(thread.read(cx).last_error(), |this, last_error| {
this.child(
div()
- .absolute()
- .right_3()
- .bottom_12()
- .max_w_96()
- .py_2()
- .px_3()
- .elevation_2(cx)
- .occlude()
.child(match last_error {
ThreadError::PaymentRequired => {
self.render_payment_required_error(thread, cx)
@@ -3229,6 +3186,7 @@ impl Render for AgentPanel {
.into_any(),
)
})
+ .child(h_flex().child(message_editor.clone()))
.child(self.render_drag_target(cx)),
ActiveView::AcpThread { thread_element, .. } => parent
.relative()
diff --git a/crates/agent_ui/src/agent_ui.rs b/crates/agent_ui/src/agent_ui.rs
index 62f1eb7bf6294f58cb48a3368b7df05cd0804a6c..55a1cba94f436de386ba9621d0c359fd1523c781 100644
--- a/crates/agent_ui/src/agent_ui.rs
+++ b/crates/agent_ui/src/agent_ui.rs
@@ -4,6 +4,7 @@ mod agent_diff;
mod agent_model_selector;
mod agent_panel;
mod buffer_codegen;
+mod burn_mode_tooltip;
mod context_picker;
mod context_server_configuration;
mod context_strip;
@@ -11,7 +12,6 @@ mod debug;
mod inline_assistant;
mod inline_prompt_editor;
mod language_model_selector;
-mod max_mode_tooltip;
mod message_editor;
mod profile_selector;
mod slash_command;
@@ -92,6 +92,7 @@ actions!(
#[derive(Default, Clone, PartialEq, Deserialize, JsonSchema, Action)]
#[action(namespace = agent)]
+#[serde(deny_unknown_fields)]
pub struct NewThread {
#[serde(default)]
from_thread_id: Option,
@@ -99,6 +100,7 @@ pub struct NewThread {
#[derive(PartialEq, Clone, Default, Debug, Deserialize, JsonSchema, Action)]
#[action(namespace = agent)]
+#[serde(deny_unknown_fields)]
pub struct ManageProfiles {
#[serde(default)]
pub customize_tools: Option,
@@ -209,7 +211,7 @@ fn update_active_language_model_from_settings(cx: &mut App) {
}
}
- let default = to_selected_model(&settings.default_model);
+ let default = settings.default_model.as_ref().map(to_selected_model);
let inline_assistant = settings
.inline_assistant_model
.as_ref()
@@ -229,7 +231,7 @@ fn update_active_language_model_from_settings(cx: &mut App) {
.collect::>();
LanguageModelRegistry::global(cx).update(cx, |registry, cx| {
- registry.select_default_model(Some(&default), cx);
+ registry.select_default_model(default.as_ref(), cx);
registry.select_inline_assistant_model(inline_assistant.as_ref(), cx);
registry.select_commit_message_model(commit_message.as_ref(), cx);
registry.select_thread_summary_model(thread_summary.as_ref(), cx);
diff --git a/crates/agent_ui/src/buffer_codegen.rs b/crates/agent_ui/src/buffer_codegen.rs
index f3919a958f8cdcc7f1114406350de4cec5afd77a..117dcf4f8e17bc99c4bd6ed75af070d84e5b1015 100644
--- a/crates/agent_ui/src/buffer_codegen.rs
+++ b/crates/agent_ui/src/buffer_codegen.rs
@@ -1094,15 +1094,9 @@ mod tests {
};
use language_model::{LanguageModelRegistry, TokenUsage};
use rand::prelude::*;
- use serde::Serialize;
use settings::SettingsStore;
use std::{future, sync::Arc};
- #[derive(Serialize)]
- pub struct DummyCompletionRequest {
- pub name: String,
- }
-
#[gpui::test(iterations = 10)]
async fn test_transform_autoindent(cx: &mut TestAppContext, mut rng: StdRng) {
init_test(cx);
diff --git a/crates/agent_ui/src/max_mode_tooltip.rs b/crates/agent_ui/src/burn_mode_tooltip.rs
similarity index 95%
rename from crates/agent_ui/src/max_mode_tooltip.rs
rename to crates/agent_ui/src/burn_mode_tooltip.rs
index a3100d4367c7a7b43edc3cc2b9b3a821941074e6..6354c07760f5aa0261b69e8dd08ce1f1b1be6023 100644
--- a/crates/agent_ui/src/max_mode_tooltip.rs
+++ b/crates/agent_ui/src/burn_mode_tooltip.rs
@@ -1,11 +1,11 @@
use gpui::{Context, FontWeight, IntoElement, Render, Window};
use ui::{prelude::*, tooltip_container};
-pub struct MaxModeTooltip {
+pub struct BurnModeTooltip {
selected: bool,
}
-impl MaxModeTooltip {
+impl BurnModeTooltip {
pub fn new() -> Self {
Self { selected: false }
}
@@ -16,7 +16,7 @@ impl MaxModeTooltip {
}
}
-impl Render for MaxModeTooltip {
+impl Render for BurnModeTooltip {
fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement {
let (icon, color) = if self.selected {
(IconName::ZedBurnModeOn, Color::Error)
diff --git a/crates/agent_ui/src/context_picker.rs b/crates/agent_ui/src/context_picker.rs
index b0069a2446bdce30968518d2af7f6d60ab0ad59e..f303f34a52856a068f1d2da33cf1f0a4fb5813a5 100644
--- a/crates/agent_ui/src/context_picker.rs
+++ b/crates/agent_ui/src/context_picker.rs
@@ -930,8 +930,8 @@ impl MentionLink {
format!(
"[@{} ({}-{})]({}:{}:{}-{})",
file_name,
- line_range.start,
- line_range.end,
+ line_range.start + 1,
+ line_range.end + 1,
Self::SELECTION,
full_path,
line_range.start,
diff --git a/crates/agent_ui/src/inline_assistant.rs b/crates/agent_ui/src/inline_assistant.rs
index 6e77e764a5ed172f0948d7d76f476377cafd04b7..c9c173a68be5191e77690e826378ca52d3db9684 100644
--- a/crates/agent_ui/src/inline_assistant.rs
+++ b/crates/agent_ui/src/inline_assistant.rs
@@ -18,6 +18,7 @@ use agent_settings::AgentSettings;
use anyhow::{Context as _, Result};
use client::telemetry::Telemetry;
use collections::{HashMap, HashSet, VecDeque, hash_map};
+use editor::SelectionEffects;
use editor::{
Anchor, AnchorRangeExt, CodeActionProvider, Editor, EditorEvent, ExcerptId, ExcerptRange,
MultiBuffer, MultiBufferSnapshot, ToOffset as _, ToPoint,
@@ -1159,7 +1160,7 @@ impl InlineAssistant {
let position = assist.range.start;
editor.update(cx, |editor, cx| {
- editor.change_selections(None, window, cx, |selections| {
+ editor.change_selections(SelectionEffects::no_scroll(), window, cx, |selections| {
selections.select_anchor_ranges([position..position])
});
diff --git a/crates/agent_ui/src/language_model_selector.rs b/crates/agent_ui/src/language_model_selector.rs
index d9d11231edbe128fcfd9a486278ed8e1542b4397..55c0974fc1d2bdbd65e0b6d746abf7f4ef10654d 100644
--- a/crates/agent_ui/src/language_model_selector.rs
+++ b/crates/agent_ui/src/language_model_selector.rs
@@ -399,7 +399,7 @@ impl PickerDelegate for LanguageModelPickerDelegate {
cx: &mut Context>,
) -> Task<()> {
let all_models = self.all_models.clone();
- let current_index = self.selected_index;
+ let active_model = (self.get_active_model)(cx);
let bg_executor = cx.background_executor();
let language_model_registry = LanguageModelRegistry::global(cx);
@@ -441,12 +441,9 @@ impl PickerDelegate for LanguageModelPickerDelegate {
cx.spawn_in(window, async move |this, cx| {
this.update_in(cx, |this, window, cx| {
this.delegate.filtered_entries = filtered_models.entries();
- // Preserve selection focus
- let new_index = if current_index >= this.delegate.filtered_entries.len() {
- 0
- } else {
- current_index
- };
+ // Finds the currently selected model in the list
+ let new_index =
+ Self::get_active_model_index(&this.delegate.filtered_entries, active_model);
this.set_selected_index(new_index, Some(picker::Direction::Down), true, window, cx);
cx.notify();
})
diff --git a/crates/agent_ui/src/message_editor.rs b/crates/agent_ui/src/message_editor.rs
index a5b7537da0a78e271b5eab3df23cd576576c1632..d8d97853dc94bbcceffe9e956842931123e2f37f 100644
--- a/crates/agent_ui/src/message_editor.rs
+++ b/crates/agent_ui/src/message_editor.rs
@@ -576,7 +576,7 @@ impl MessageEditor {
fn render_burn_mode_toggle(&self, cx: &mut Context) -> Option {
let thread = self.thread.read(cx);
let model = thread.configured_model();
- if !model?.model.supports_max_mode() {
+ if !model?.model.supports_burn_mode() {
return None;
}
@@ -1251,9 +1251,7 @@ impl MessageEditor {
self.thread
.read(cx)
.configured_model()
- .map_or(false, |model| {
- model.provider.id().0 == ZED_CLOUD_PROVIDER_ID
- })
+ .map_or(false, |model| model.provider.id() == ZED_CLOUD_PROVIDER_ID)
}
fn render_usage_callout(&self, line_height: Pixels, cx: &mut Context) -> Option {
diff --git a/crates/agent_ui/src/text_thread_editor.rs b/crates/agent_ui/src/text_thread_editor.rs
index 0a1013a6f29f86ede4580565d2f5df67ac9e263d..d11deb790820ba18a7437ac50ed3d5b2e8d4c9c0 100644
--- a/crates/agent_ui/src/text_thread_editor.rs
+++ b/crates/agent_ui/src/text_thread_editor.rs
@@ -1,8 +1,8 @@
use crate::{
+ burn_mode_tooltip::BurnModeTooltip,
language_model_selector::{
LanguageModelSelector, ToggleModelSelector, language_model_selector,
},
- max_mode_tooltip::MaxModeTooltip,
};
use agent_settings::{AgentSettings, CompletionMode};
use anyhow::Result;
@@ -21,7 +21,6 @@ use editor::{
BlockPlacement, BlockProperties, BlockStyle, Crease, CreaseMetadata, CustomBlockId, FoldId,
RenderBlock, ToDisplayPoint,
},
- scroll::Autoscroll,
};
use editor::{FoldPlaceholder, display_map::CreaseId};
use fs::Fs;
@@ -69,7 +68,7 @@ use workspace::{
searchable::{Direction, SearchableItemHandle},
};
use workspace::{
- Save, Toast, ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView, Workspace,
+ Save, Toast, Workspace,
item::{self, FollowableItem, Item, ItemHandle},
notifications::NotificationId,
pane,
@@ -389,7 +388,7 @@ impl TextThreadEditor {
cursor..cursor
};
self.editor.update(cx, |editor, cx| {
- editor.change_selections(Some(Autoscroll::fit()), window, cx, |selections| {
+ editor.change_selections(Default::default(), window, cx, |selections| {
selections.select_ranges([new_selection])
});
});
@@ -449,8 +448,7 @@ impl TextThreadEditor {
if let Some(command) = self.slash_commands.command(name, cx) {
self.editor.update(cx, |editor, cx| {
editor.transact(window, cx, |editor, window, cx| {
- editor
- .change_selections(Some(Autoscroll::fit()), window, cx, |s| s.try_cancel());
+ editor.change_selections(Default::default(), window, cx, |s| s.try_cancel());
let snapshot = editor.buffer().read(cx).snapshot(cx);
let newest_cursor = editor.selections.newest::
(cx).head();
if newest_cursor.column > 0
@@ -1583,7 +1581,7 @@ impl TextThreadEditor {
self.editor.update(cx, |editor, cx| {
editor.transact(window, cx, |this, window, cx| {
- this.change_selections(Some(Autoscroll::fit()), window, cx, |s| {
+ this.change_selections(Default::default(), window, cx, |s| {
s.select(selections);
});
this.insert("", window, cx);
@@ -2075,12 +2073,12 @@ impl TextThreadEditor {
)
}
- fn render_max_mode_toggle(&self, cx: &mut Context) -> Option {
+ fn render_burn_mode_toggle(&self, cx: &mut Context) -> Option {
let context = self.context().read(cx);
let active_model = LanguageModelRegistry::read_global(cx)
.default_model()
.map(|default| default.model)?;
- if !active_model.supports_max_mode() {
+ if !active_model.supports_burn_mode() {
return None;
}
@@ -2107,7 +2105,7 @@ impl TextThreadEditor {
});
}))
.tooltip(move |_window, cx| {
- cx.new(|_| MaxModeTooltip::new().selected(burn_mode_enabled))
+ cx.new(|_| BurnModeTooltip::new().selected(burn_mode_enabled))
.into()
})
.into_any_element(),
@@ -2122,12 +2120,21 @@ impl TextThreadEditor {
let active_model = LanguageModelRegistry::read_global(cx)
.default_model()
.map(|default| default.model);
- let focus_handle = self.editor().focus_handle(cx).clone();
let model_name = match active_model {
Some(model) => model.name().0,
None => SharedString::from("No model selected"),
};
+ let active_provider = LanguageModelRegistry::read_global(cx)
+ .default_model()
+ .map(|default| default.provider);
+ let provider_icon = match active_provider {
+ Some(provider) => provider.icon(),
+ None => IconName::Ai,
+ };
+
+ let focus_handle = self.editor().focus_handle(cx).clone();
+
PickerPopoverMenu::new(
self.language_model_selector.clone(),
ButtonLike::new("active-model")
@@ -2135,10 +2142,16 @@ impl TextThreadEditor {
.child(
h_flex()
.gap_0p5()
+ .child(
+ Icon::new(provider_icon)
+ .color(Color::Muted)
+ .size(IconSize::XSmall),
+ )
.child(
Label::new(model_name)
+ .color(Color::Muted)
.size(LabelSize::Small)
- .color(Color::Muted),
+ .ml_0p5(),
)
.child(
Icon::new(IconName::ChevronDown)
@@ -2575,7 +2588,7 @@ impl Render for TextThreadEditor {
};
let language_model_selector = self.language_model_selector_menu_handle.clone();
- let max_mode_toggle = self.render_max_mode_toggle(cx);
+ let burn_mode_toggle = self.render_burn_mode_toggle(cx);
v_flex()
.key_context("ContextEditor")
@@ -2630,7 +2643,7 @@ impl Render for TextThreadEditor {
h_flex()
.gap_0p5()
.child(self.render_inject_context_menu(cx))
- .when_some(max_mode_toggle, |this, element| this.child(element)),
+ .when_some(burn_mode_toggle, |this, element| this.child(element)),
)
.child(
h_flex()
@@ -2924,13 +2937,6 @@ impl FollowableItem for TextThreadEditor {
}
}
-pub struct ContextEditorToolbarItem {
- active_context_editor: Option>,
- model_summary_editor: Entity,
-}
-
-impl ContextEditorToolbarItem {}
-
pub fn render_remaining_tokens(
context_editor: &Entity,
cx: &App,
@@ -2983,98 +2989,6 @@ pub fn render_remaining_tokens(
)
}
-impl Render for ContextEditorToolbarItem {
- fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement {
- let left_side = h_flex()
- .group("chat-title-group")
- .gap_1()
- .items_center()
- .flex_grow()
- .child(
- div()
- .w_full()
- .when(self.active_context_editor.is_some(), |left_side| {
- left_side.child(self.model_summary_editor.clone())
- }),
- )
- .child(
- div().visible_on_hover("chat-title-group").child(
- IconButton::new("regenerate-context", IconName::RefreshTitle)
- .shape(ui::IconButtonShape::Square)
- .tooltip(Tooltip::text("Regenerate Title"))
- .on_click(cx.listener(move |_, _, _window, cx| {
- cx.emit(ContextEditorToolbarItemEvent::RegenerateSummary)
- })),
- ),
- );
-
- let right_side = h_flex()
- .gap_2()
- // TODO display this in a nicer way, once we have a design for it.
- // .children({
- // let project = self
- // .workspace
- // .upgrade()
- // .map(|workspace| workspace.read(cx).project().downgrade());
- //
- // let scan_items_remaining = cx.update_global(|db: &mut SemanticDb, cx| {
- // project.and_then(|project| db.remaining_summaries(&project, cx))
- // });
- // scan_items_remaining
- // .map(|remaining_items| format!("Files to scan: {}", remaining_items))
- // })
- .children(
- self.active_context_editor
- .as_ref()
- .and_then(|editor| editor.upgrade())
- .and_then(|editor| render_remaining_tokens(&editor, cx)),
- );
-
- h_flex()
- .px_0p5()
- .size_full()
- .gap_2()
- .justify_between()
- .child(left_side)
- .child(right_side)
- }
-}
-
-impl ToolbarItemView for ContextEditorToolbarItem {
- fn set_active_pane_item(
- &mut self,
- active_pane_item: Option<&dyn ItemHandle>,
- _window: &mut Window,
- cx: &mut Context,
- ) -> ToolbarItemLocation {
- self.active_context_editor = active_pane_item
- .and_then(|item| item.act_as::(cx))
- .map(|editor| editor.downgrade());
- cx.notify();
- if self.active_context_editor.is_none() {
- ToolbarItemLocation::Hidden
- } else {
- ToolbarItemLocation::PrimaryRight
- }
- }
-
- fn pane_focus_update(
- &mut self,
- _pane_focused: bool,
- _window: &mut Window,
- cx: &mut Context,
- ) {
- cx.notify();
- }
-}
-
-impl EventEmitter for ContextEditorToolbarItem {}
-
-pub enum ContextEditorToolbarItemEvent {
- RegenerateSummary,
-}
-impl EventEmitter for ContextEditorToolbarItem {}
-
enum PendingSlashCommand {}
fn invoked_slash_command_fold_placeholder(
@@ -3240,6 +3154,7 @@ pub fn make_lsp_adapter_delegate(
#[cfg(test)]
mod tests {
use super::*;
+ use editor::SelectionEffects;
use fs::FakeFs;
use gpui::{App, TestAppContext, VisualTestContext};
use indoc::indoc;
@@ -3465,7 +3380,9 @@ mod tests {
) {
context_editor.update_in(cx, |context_editor, window, cx| {
context_editor.editor.update(cx, |editor, cx| {
- editor.change_selections(None, window, cx, |s| s.select_ranges([range]));
+ editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
+ s.select_ranges([range])
+ });
});
context_editor.copy(&Default::default(), window, cx);
diff --git a/crates/agent_ui/src/ui.rs b/crates/agent_ui/src/ui.rs
index c9adc2a63177bfad71203f37f79a140639605702..c076d113b8946c8bc9d85dd89672f3417f4bc15a 100644
--- a/crates/agent_ui/src/ui.rs
+++ b/crates/agent_ui/src/ui.rs
@@ -1,13 +1,13 @@
mod agent_notification;
mod animated_label;
+mod burn_mode_tooltip;
mod context_pill;
-mod max_mode_tooltip;
mod onboarding_modal;
pub mod preview;
mod upsell;
pub use agent_notification::*;
pub use animated_label::*;
+pub use burn_mode_tooltip::*;
pub use context_pill::*;
-pub use max_mode_tooltip::*;
pub use onboarding_modal::*;
diff --git a/crates/agent_ui/src/ui/max_mode_tooltip.rs b/crates/agent_ui/src/ui/burn_mode_tooltip.rs
similarity index 100%
rename from crates/agent_ui/src/ui/max_mode_tooltip.rs
rename to crates/agent_ui/src/ui/burn_mode_tooltip.rs
diff --git a/crates/anthropic/src/anthropic.rs b/crates/anthropic/src/anthropic.rs
index 7f0ab7550d8df12ea08f0bd955e83aa72d25e6b3..c73f6060458783f22b4d846dbe6d4a619d7e791c 100644
--- a/crates/anthropic/src/anthropic.rs
+++ b/crates/anthropic/src/anthropic.rs
@@ -6,7 +6,7 @@ use anyhow::{Context as _, Result, anyhow};
use chrono::{DateTime, Utc};
use futures::{AsyncBufReadExt, AsyncReadExt, StreamExt, io::BufReader, stream::BoxStream};
use http_client::http::{self, HeaderMap, HeaderValue};
-use http_client::{AsyncBody, HttpClient, Method, Request as HttpRequest};
+use http_client::{AsyncBody, HttpClient, Method, Request as HttpRequest, StatusCode};
use serde::{Deserialize, Serialize};
use strum::{EnumIter, EnumString};
use thiserror::Error;
@@ -356,7 +356,7 @@ pub async fn complete(
.send(request)
.await
.map_err(AnthropicError::HttpSend)?;
- let status = response.status();
+ let status_code = response.status();
let mut body = String::new();
response
.body_mut()
@@ -364,12 +364,12 @@ pub async fn complete(
.await
.map_err(AnthropicError::ReadResponse)?;
- if status.is_success() {
+ if status_code.is_success() {
Ok(serde_json::from_str(&body).map_err(AnthropicError::DeserializeResponse)?)
} else {
Err(AnthropicError::HttpResponseError {
- status: status.as_u16(),
- body,
+ status_code,
+ message: body,
})
}
}
@@ -444,11 +444,7 @@ impl RateLimitInfo {
}
Self {
- retry_after: headers
- .get("retry-after")
- .and_then(|v| v.to_str().ok())
- .and_then(|v| v.parse::().ok())
- .map(Duration::from_secs),
+ retry_after: parse_retry_after(headers),
requests: RateLimit::from_headers("requests", headers).ok(),
tokens: RateLimit::from_headers("tokens", headers).ok(),
input_tokens: RateLimit::from_headers("input-tokens", headers).ok(),
@@ -457,6 +453,17 @@ impl RateLimitInfo {
}
}
+/// Parses the Retry-After header value as an integer number of seconds (anthropic always uses
+/// seconds). Note that other services might specify an HTTP date or some other format for this
+/// header. Returns `None` if the header is not present or cannot be parsed.
+pub fn parse_retry_after(headers: &HeaderMap) -> Option {
+ headers
+ .get("retry-after")
+ .and_then(|v| v.to_str().ok())
+ .and_then(|v| v.parse::().ok())
+ .map(Duration::from_secs)
+}
+
fn get_header<'a>(key: &str, headers: &'a HeaderMap) -> anyhow::Result<&'a str> {
Ok(headers
.get(key)
@@ -520,6 +527,10 @@ pub async fn stream_completion_with_rate_limit_info(
})
.boxed();
Ok((stream, Some(rate_limits)))
+ } else if response.status().as_u16() == 529 {
+ Err(AnthropicError::ServerOverloaded {
+ retry_after: rate_limits.retry_after,
+ })
} else if let Some(retry_after) = rate_limits.retry_after {
Err(AnthropicError::RateLimit { retry_after })
} else {
@@ -532,10 +543,9 @@ pub async fn stream_completion_with_rate_limit_info(
match serde_json::from_str::(&body) {
Ok(Event::Error { error }) => Err(AnthropicError::ApiError(error)),
- Ok(_) => Err(AnthropicError::UnexpectedResponseFormat(body)),
- Err(_) => Err(AnthropicError::HttpResponseError {
- status: response.status().as_u16(),
- body: body,
+ Ok(_) | Err(_) => Err(AnthropicError::HttpResponseError {
+ status_code: response.status(),
+ message: body,
}),
}
}
@@ -801,16 +811,19 @@ pub enum AnthropicError {
ReadResponse(io::Error),
/// HTTP error response from the API
- HttpResponseError { status: u16, body: String },
+ HttpResponseError {
+ status_code: StatusCode,
+ message: String,
+ },
/// Rate limit exceeded
RateLimit { retry_after: Duration },
+ /// Server overloaded
+ ServerOverloaded { retry_after: Option },
+
/// API returned an error response
ApiError(ApiError),
-
- /// Unexpected response format
- UnexpectedResponseFormat(String),
}
#[derive(Debug, Serialize, Deserialize, Error)]
diff --git a/crates/assistant_context/src/assistant_context.rs b/crates/assistant_context/src/assistant_context.rs
index cef9d2f0fd60c842883fcff80766416ca3db66de..aaaef152503e477c0bff4e8036c6460d6e9fde46 100644
--- a/crates/assistant_context/src/assistant_context.rs
+++ b/crates/assistant_context/src/assistant_context.rs
@@ -2140,7 +2140,8 @@ impl AssistantContext {
);
}
LanguageModelCompletionEvent::ToolUse(_) |
- LanguageModelCompletionEvent::UsageUpdate(_) => {}
+ LanguageModelCompletionEvent::ToolUseJsonParseError { .. } |
+ LanguageModelCompletionEvent::UsageUpdate(_) => {}
}
});
@@ -2346,13 +2347,13 @@ impl AssistantContext {
completion_request.messages.push(request_message);
}
}
- let supports_max_mode = if let Some(model) = model {
- model.supports_max_mode()
+ let supports_burn_mode = if let Some(model) = model {
+ model.supports_burn_mode()
} else {
false
};
- if supports_max_mode {
+ if supports_burn_mode {
completion_request.mode = Some(self.completion_mode.into());
}
completion_request
diff --git a/crates/assistant_slash_commands/src/delta_command.rs b/crates/assistant_slash_commands/src/delta_command.rs
index 047d2899082891ad5e1cfc5e8ec9188dd1aa4e4f..8c840c17b2c7fe9d8c8995b21c35cb35980dd71b 100644
--- a/crates/assistant_slash_commands/src/delta_command.rs
+++ b/crates/assistant_slash_commands/src/delta_command.rs
@@ -74,7 +74,7 @@ impl SlashCommand for DeltaSlashCommand {
.slice(section.range.to_offset(&context_buffer)),
);
file_command_new_outputs.push(Arc::new(FileSlashCommand).run(
- &[metadata.path.clone()],
+ std::slice::from_ref(&metadata.path),
context_slash_command_output_sections,
context_buffer.clone(),
workspace.clone(),
diff --git a/crates/assistant_tools/src/edit_agent/evals.rs b/crates/assistant_tools/src/edit_agent/evals.rs
index 7beb2ec9190c4e6e65ed7d48211328dc51073ea4..8df8f677f20861c2cd5834bdcec6ac3ba414cdb0 100644
--- a/crates/assistant_tools/src/edit_agent/evals.rs
+++ b/crates/assistant_tools/src/edit_agent/evals.rs
@@ -29,6 +29,7 @@ use std::{
path::Path,
str::FromStr,
sync::mpsc,
+ time::Duration,
};
use util::path;
@@ -1658,12 +1659,14 @@ async fn retry_on_rate_limit(mut request: impl AsyncFnMut() -> Result) ->
match request().await {
Ok(result) => return Ok(result),
Err(err) => match err.downcast::() {
- Ok(err) => match err {
- LanguageModelCompletionError::RateLimitExceeded { retry_after } => {
+ Ok(err) => match &err {
+ LanguageModelCompletionError::RateLimitExceeded { retry_after, .. }
+ | LanguageModelCompletionError::ServerOverloaded { retry_after, .. } => {
+ let retry_after = retry_after.unwrap_or(Duration::from_secs(5));
// Wait for the duration supplied, with some jitter to avoid all requests being made at the same time.
let jitter = retry_after.mul_f64(rand::thread_rng().gen_range(0.0..1.0));
eprintln!(
- "Attempt #{attempt}: Rate limit exceeded. Retry after {retry_after:?} + jitter of {jitter:?}"
+ "Attempt #{attempt}: {err}. Retry after {retry_after:?} + jitter of {jitter:?}"
);
Timer::after(retry_after + jitter).await;
continue;
diff --git a/crates/assistant_tools/src/edit_file_tool.rs b/crates/assistant_tools/src/edit_file_tool.rs
index fde697b00eb2177bf3ac0382fd7d0c78daa0907e..8c7728b4b72c9aa52c717e58fbdd63591dd88f0f 100644
--- a/crates/assistant_tools/src/edit_file_tool.rs
+++ b/crates/assistant_tools/src/edit_file_tool.rs
@@ -10,7 +10,7 @@ use assistant_tool::{
ToolUseStatus,
};
use buffer_diff::{BufferDiff, BufferDiffSnapshot};
-use editor::{Editor, EditorMode, MinimapVisibility, MultiBuffer, PathKey, scroll::Autoscroll};
+use editor::{Editor, EditorMode, MinimapVisibility, MultiBuffer, PathKey};
use futures::StreamExt;
use gpui::{
Animation, AnimationExt, AnyWindowHandle, App, AppContext, AsyncApp, Entity, Task,
@@ -823,7 +823,7 @@ impl ToolCard for EditFileToolCard {
let first_hunk_start =
first_hunk.multi_buffer_range().start;
editor.change_selections(
- Some(Autoscroll::fit()),
+ Default::default(),
window,
cx,
|selections| {
@@ -1065,7 +1065,7 @@ fn markdown_style(window: &Window, cx: &App) -> MarkdownStyle {
MarkdownStyle {
base_text_style: text_style.clone(),
- selection_background_color: cx.theme().players().local().selection,
+ selection_background_color: cx.theme().colors().element_selection_background,
..Default::default()
}
}
diff --git a/crates/assistant_tools/src/schema.rs b/crates/assistant_tools/src/schema.rs
index 4a71d47d2cdba4d92711c9dd4549d036df58b0ad..888e11de4e83df853d5d1c252d30cecf84c701a2 100644
--- a/crates/assistant_tools/src/schema.rs
+++ b/crates/assistant_tools/src/schema.rs
@@ -1,8 +1,9 @@
use anyhow::Result;
use language_model::LanguageModelToolSchemaFormat;
use schemars::{
- JsonSchema,
- schema::{RootSchema, Schema, SchemaObject},
+ JsonSchema, Schema,
+ generate::SchemaSettings,
+ transform::{Transform, transform_subschemas},
};
pub fn json_schema_for(
@@ -13,7 +14,7 @@ pub fn json_schema_for(
}
fn schema_to_json(
- schema: &RootSchema,
+ schema: &Schema,
format: LanguageModelToolSchemaFormat,
) -> Result {
let mut value = serde_json::to_value(schema)?;
@@ -21,58 +22,42 @@ fn schema_to_json(
Ok(value)
}
-fn root_schema_for(format: LanguageModelToolSchemaFormat) -> RootSchema {
+fn root_schema_for(format: LanguageModelToolSchemaFormat) -> Schema {
let mut generator = match format {
- LanguageModelToolSchemaFormat::JsonSchema => schemars::SchemaGenerator::default(),
- LanguageModelToolSchemaFormat::JsonSchemaSubset => {
- schemars::r#gen::SchemaSettings::default()
- .with(|settings| {
- settings.meta_schema = None;
- settings.inline_subschemas = true;
- settings
- .visitors
- .push(Box::new(TransformToJsonSchemaSubsetVisitor));
- })
- .into_generator()
- }
+ LanguageModelToolSchemaFormat::JsonSchema => SchemaSettings::draft07().into_generator(),
+ // TODO: Gemini docs mention using a subset of OpenAPI 3, so this may benefit from using
+ // `SchemaSettings::openapi3()`.
+ LanguageModelToolSchemaFormat::JsonSchemaSubset => SchemaSettings::draft07()
+ .with(|settings| {
+ settings.meta_schema = None;
+ settings.inline_subschemas = true;
+ })
+ .with_transform(ToJsonSchemaSubsetTransform)
+ .into_generator(),
};
generator.root_schema_for::()
}
#[derive(Debug, Clone)]
-struct TransformToJsonSchemaSubsetVisitor;
-
-impl schemars::visit::Visitor for TransformToJsonSchemaSubsetVisitor {
- fn visit_root_schema(&mut self, root: &mut RootSchema) {
- schemars::visit::visit_root_schema(self, root)
- }
+struct ToJsonSchemaSubsetTransform;
- fn visit_schema(&mut self, schema: &mut Schema) {
- schemars::visit::visit_schema(self, schema)
- }
-
- fn visit_schema_object(&mut self, schema: &mut SchemaObject) {
+impl Transform for ToJsonSchemaSubsetTransform {
+ fn transform(&mut self, schema: &mut Schema) {
// Ensure that the type field is not an array, this happens when we use
// Option, the type will be [T, "null"].
- if let Some(instance_type) = schema.instance_type.take() {
- schema.instance_type = match instance_type {
- schemars::schema::SingleOrVec::Single(t) => {
- Some(schemars::schema::SingleOrVec::Single(t))
+ if let Some(type_field) = schema.get_mut("type") {
+ if let Some(types) = type_field.as_array() {
+ if let Some(first_type) = types.first() {
+ *type_field = first_type.clone();
}
- schemars::schema::SingleOrVec::Vec(items) => items
- .into_iter()
- .next()
- .map(schemars::schema::SingleOrVec::from),
- };
+ }
}
- // One of is not supported, use anyOf instead.
- if let Some(subschema) = schema.subschemas.as_mut() {
- if let Some(one_of) = subschema.one_of.take() {
- subschema.any_of = Some(one_of);
- }
+ // oneOf is not supported, use anyOf instead
+ if let Some(one_of) = schema.remove("oneOf") {
+ schema.insert("anyOf".to_string(), one_of);
}
- schemars::visit::visit_schema_object(self, schema)
+ transform_subschemas(self, schema);
}
}
diff --git a/crates/assistant_tools/src/terminal_tool.rs b/crates/assistant_tools/src/terminal_tool.rs
index 5ec0ce7b8f0a4f5f5a34efd01069cd0487104b58..2c582a531069eb9a81340af7eb07731e8df8a96e 100644
--- a/crates/assistant_tools/src/terminal_tool.rs
+++ b/crates/assistant_tools/src/terminal_tool.rs
@@ -691,7 +691,7 @@ fn markdown_style(window: &Window, cx: &App) -> MarkdownStyle {
MarkdownStyle {
base_text_style: text_style.clone(),
- selection_background_color: cx.theme().players().local().selection,
+ selection_background_color: cx.theme().colors().element_selection_background,
..Default::default()
}
}
diff --git a/crates/auto_update_ui/src/auto_update_ui.rs b/crates/auto_update_ui/src/auto_update_ui.rs
index 30c1cddec2935d82f2ecc9fe0cfc569999d80d7b..afb135bc974f56d04db93e2a902fe48a64ab8ea7 100644
--- a/crates/auto_update_ui/src/auto_update_ui.rs
+++ b/crates/auto_update_ui/src/auto_update_ui.rs
@@ -1,7 +1,7 @@
use auto_update::AutoUpdater;
use client::proto::UpdateNotification;
use editor::{Editor, MultiBuffer};
-use gpui::{App, Context, DismissEvent, Entity, SharedString, Window, actions, prelude::*};
+use gpui::{App, Context, DismissEvent, Entity, Window, actions, prelude::*};
use http_client::HttpClient;
use markdown_preview::markdown_preview_view::{MarkdownPreviewMode, MarkdownPreviewView};
use release_channel::{AppVersion, ReleaseChannel};
@@ -94,7 +94,6 @@ fn view_release_notes_locally(
let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
- let tab_content = Some(SharedString::from(body.title.to_string()));
let editor = cx.new(|cx| {
Editor::for_multibuffer(buffer, Some(project), window, cx)
});
@@ -105,7 +104,6 @@ fn view_release_notes_locally(
editor,
workspace_handle,
language_registry,
- tab_content,
window,
cx,
);
diff --git a/crates/bedrock/Cargo.toml b/crates/bedrock/Cargo.toml
index 84fd58460185eafef2196aabf15380b2abbbe390..3000af50bb71be18784a8e6a8f6da0ca8a66d7f9 100644
--- a/crates/bedrock/Cargo.toml
+++ b/crates/bedrock/Cargo.toml
@@ -25,5 +25,4 @@ serde.workspace = true
serde_json.workspace = true
strum.workspace = true
thiserror.workspace = true
-tokio = { workspace = true, features = ["rt", "rt-multi-thread"] }
workspace-hack.workspace = true
diff --git a/crates/bedrock/src/bedrock.rs b/crates/bedrock/src/bedrock.rs
index e32a456dbae184c41c1d4268db05af08a9fc6f4d..1c6a9bd0a1e745da1dd4577741fc7cb4cab771ad 100644
--- a/crates/bedrock/src/bedrock.rs
+++ b/crates/bedrock/src/bedrock.rs
@@ -1,9 +1,6 @@
mod models;
-use std::collections::HashMap;
-use std::pin::Pin;
-
-use anyhow::{Context as _, Error, Result, anyhow};
+use anyhow::{Context, Error, Result, anyhow};
use aws_sdk_bedrockruntime as bedrock;
pub use aws_sdk_bedrockruntime as bedrock_client;
pub use aws_sdk_bedrockruntime::types::{
@@ -24,9 +21,10 @@ pub use bedrock::types::{
ToolResultContentBlock as BedrockToolResultContentBlock,
ToolResultStatus as BedrockToolResultStatus, ToolUseBlock as BedrockToolUseBlock,
};
-use futures::stream::{self, BoxStream, Stream};
+use futures::stream::{self, BoxStream};
use serde::{Deserialize, Serialize};
use serde_json::{Number, Value};
+use std::collections::HashMap;
use thiserror::Error;
pub use crate::models::*;
@@ -34,70 +32,59 @@ pub use crate::models::*;
pub async fn stream_completion(
client: bedrock::Client,
request: Request,
- handle: tokio::runtime::Handle,
) -> Result>, Error> {
- handle
- .spawn(async move {
- let mut response = bedrock::Client::converse_stream(&client)
- .model_id(request.model.clone())
- .set_messages(request.messages.into());
+ let mut response = bedrock::Client::converse_stream(&client)
+ .model_id(request.model.clone())
+ .set_messages(request.messages.into());
- if let Some(Thinking::Enabled {
- budget_tokens: Some(budget_tokens),
- }) = request.thinking
- {
- response =
- response.additional_model_request_fields(Document::Object(HashMap::from([(
- "thinking".to_string(),
- Document::from(HashMap::from([
- ("type".to_string(), Document::String("enabled".to_string())),
- (
- "budget_tokens".to_string(),
- Document::Number(AwsNumber::PosInt(budget_tokens)),
- ),
- ])),
- )])));
- }
+ if let Some(Thinking::Enabled {
+ budget_tokens: Some(budget_tokens),
+ }) = request.thinking
+ {
+ let thinking_config = HashMap::from([
+ ("type".to_string(), Document::String("enabled".to_string())),
+ (
+ "budget_tokens".to_string(),
+ Document::Number(AwsNumber::PosInt(budget_tokens)),
+ ),
+ ]);
+ response = response.additional_model_request_fields(Document::Object(HashMap::from([(
+ "thinking".to_string(),
+ Document::from(thinking_config),
+ )])));
+ }
- if request.tools.is_some() && !request.tools.as_ref().unwrap().tools.is_empty() {
- response = response.set_tool_config(request.tools);
- }
+ if request
+ .tools
+ .as_ref()
+ .map_or(false, |t| !t.tools.is_empty())
+ {
+ response = response.set_tool_config(request.tools);
+ }
- let response = response.send().await;
+ let output = response
+ .send()
+ .await
+ .context("Failed to send API request to Bedrock");
- match response {
- Ok(output) => {
- let stream: Pin<
- Box<
- dyn Stream- >
- + Send,
- >,
- > = Box::pin(stream::unfold(output.stream, |mut stream| async move {
- match stream.recv().await {
- Ok(Some(output)) => Some(({ Ok(output) }, stream)),
- Ok(None) => None,
- Err(err) => {
- Some((
- // TODO: Figure out how we can capture Throttling Exceptions
- Err(BedrockError::ClientError(anyhow!(
- "{:?}",
- aws_sdk_bedrockruntime::error::DisplayErrorContext(err)
- ))),
- stream,
- ))
- }
- }
- }));
- Ok(stream)
- }
- Err(err) => Err(anyhow!(
- "{:?}",
- aws_sdk_bedrockruntime::error::DisplayErrorContext(err)
+ let stream = Box::pin(stream::unfold(
+ output?.stream,
+ move |mut stream| async move {
+ match stream.recv().await {
+ Ok(Some(output)) => Some((Ok(output), stream)),
+ Ok(None) => None,
+ Err(err) => Some((
+ Err(BedrockError::ClientError(anyhow!(
+ "{:?}",
+ aws_sdk_bedrockruntime::error::DisplayErrorContext(err)
+ ))),
+ stream,
)),
}
- })
- .await
- .context("spawning a task")?
+ },
+ ));
+
+ Ok(stream)
}
pub fn aws_document_to_value(document: &Document) -> Value {
diff --git a/crates/buffer_diff/src/buffer_diff.rs b/crates/buffer_diff/src/buffer_diff.rs
index 3812d48bf7a064017b654ea82cdf3e19a7810fb2..ee09fda46e008c903120eb0430ff18fae57dc3da 100644
--- a/crates/buffer_diff/src/buffer_diff.rs
+++ b/crates/buffer_diff/src/buffer_diff.rs
@@ -1867,7 +1867,7 @@ mod tests {
let hunk = diff.hunks(&buffer, cx).next().unwrap();
let new_index_text = diff
- .stage_or_unstage_hunks(true, &[hunk.clone()], &buffer, true, cx)
+ .stage_or_unstage_hunks(true, std::slice::from_ref(&hunk), &buffer, true, cx)
.unwrap()
.to_string();
assert_eq!(new_index_text, buffer_text);
diff --git a/crates/call/src/call_settings.rs b/crates/call/src/call_settings.rs
index dd6999a17090bc970fe46cb49acc13c3e16cd57c..c8f51e0c1a2019dd2c266210e469989946ed8a35 100644
--- a/crates/call/src/call_settings.rs
+++ b/crates/call/src/call_settings.rs
@@ -12,7 +12,6 @@ pub struct CallSettings {
/// Configuration of voice calls in Zed.
#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug)]
-#[schemars(deny_unknown_fields)]
pub struct CallSettingsContent {
/// Whether the microphone should be muted when joining a channel or a call.
///
diff --git a/crates/collab/src/db/queries/channels.rs b/crates/collab/src/db/queries/channels.rs
index a4899f408d9d60d8222e558cd12964597cf4228d..9a370bb73b91b6f6434fc53d6a024e4cd00dae1e 100644
--- a/crates/collab/src/db/queries/channels.rs
+++ b/crates/collab/src/db/queries/channels.rs
@@ -734,8 +734,8 @@ impl Database {
users.push(proto::User {
id: user.id.to_proto(),
avatar_url: format!(
- "https://github.com/{}.png?size=128",
- user.github_login
+ "https://avatars.githubusercontent.com/u/{}?s=128&v=4",
+ user.github_user_id
),
github_login: user.github_login,
name: user.name,
diff --git a/crates/collab/src/db/tests/embedding_tests.rs b/crates/collab/src/db/tests/embedding_tests.rs
index 8659d4b4a1165ab9d3a2ed591fc9a6dfbd727c56..bfc238dd9ab7027cb2506b4c2d7130e070da8a04 100644
--- a/crates/collab/src/db/tests/embedding_tests.rs
+++ b/crates/collab/src/db/tests/embedding_tests.rs
@@ -76,7 +76,10 @@ async fn test_purge_old_embeddings(cx: &mut gpui::TestAppContext) {
db.purge_old_embeddings().await.unwrap();
// Try to retrieve the purged embeddings
- let retrieved_embeddings = db.get_embeddings(model, &[digest.clone()]).await.unwrap();
+ let retrieved_embeddings = db
+ .get_embeddings(model, std::slice::from_ref(&digest))
+ .await
+ .unwrap();
assert!(
retrieved_embeddings.is_empty(),
"Old embeddings should have been purged"
diff --git a/crates/collab/src/rpc.rs b/crates/collab/src/rpc.rs
index 22daab491c499bf568f155cd6e049868c58192ce..753e591914f45ea962367130d1ecce9a4fd2620f 100644
--- a/crates/collab/src/rpc.rs
+++ b/crates/collab/src/rpc.rs
@@ -179,7 +179,7 @@ struct Session {
}
impl Session {
- async fn db(&self) -> tokio::sync::MutexGuard {
+ async fn db(&self) -> tokio::sync::MutexGuard<'_, DbHandle> {
#[cfg(test)]
tokio::task::yield_now().await;
let guard = self.db.lock().await;
@@ -1037,7 +1037,7 @@ impl Server {
}
}
- pub async fn snapshot(self: &Arc) -> ServerSnapshot {
+ pub async fn snapshot(self: &Arc) -> ServerSnapshot<'_> {
ServerSnapshot {
connection_pool: ConnectionPoolGuard {
guard: self.connection_pool.lock(),
diff --git a/crates/collab/src/tests/channel_buffer_tests.rs b/crates/collab/src/tests/channel_buffer_tests.rs
index 4069f61f90b48bfedfd4780f0865a061e4ab6971..0b331ff1e66279f5e2f5e52f9d83f0eaca6cfcdb 100644
--- a/crates/collab/src/tests/channel_buffer_tests.rs
+++ b/crates/collab/src/tests/channel_buffer_tests.rs
@@ -178,7 +178,7 @@ async fn test_channel_notes_participant_indices(
channel_view_a.update_in(cx_a, |notes, window, cx| {
notes.editor.update(cx, |editor, cx| {
editor.insert("a", window, cx);
- editor.change_selections(None, window, cx, |selections| {
+ editor.change_selections(Default::default(), window, cx, |selections| {
selections.select_ranges(vec![0..1]);
});
});
@@ -188,7 +188,7 @@ async fn test_channel_notes_participant_indices(
notes.editor.update(cx, |editor, cx| {
editor.move_down(&Default::default(), window, cx);
editor.insert("b", window, cx);
- editor.change_selections(None, window, cx, |selections| {
+ editor.change_selections(Default::default(), window, cx, |selections| {
selections.select_ranges(vec![1..2]);
});
});
@@ -198,7 +198,7 @@ async fn test_channel_notes_participant_indices(
notes.editor.update(cx, |editor, cx| {
editor.move_down(&Default::default(), window, cx);
editor.insert("c", window, cx);
- editor.change_selections(None, window, cx, |selections| {
+ editor.change_selections(Default::default(), window, cx, |selections| {
selections.select_ranges(vec![2..3]);
});
});
@@ -273,12 +273,12 @@ async fn test_channel_notes_participant_indices(
.unwrap();
editor_a.update_in(cx_a, |editor, window, cx| {
- editor.change_selections(None, window, cx, |selections| {
+ editor.change_selections(Default::default(), window, cx, |selections| {
selections.select_ranges(vec![0..1]);
});
});
editor_b.update_in(cx_b, |editor, window, cx| {
- editor.change_selections(None, window, cx, |selections| {
+ editor.change_selections(Default::default(), window, cx, |selections| {
selections.select_ranges(vec![2..3]);
});
});
diff --git a/crates/collab/src/tests/editor_tests.rs b/crates/collab/src/tests/editor_tests.rs
index 7a51caefa1c2f7f6a3e7f702ae9594b790760d7d..2cc3ca76d1b639cc479cb44cde93a73570d5eb7f 100644
--- a/crates/collab/src/tests/editor_tests.rs
+++ b/crates/collab/src/tests/editor_tests.rs
@@ -4,7 +4,7 @@ use crate::{
};
use call::ActiveCall;
use editor::{
- DocumentColorsRenderMode, Editor, EditorSettings, RowInfo,
+ DocumentColorsRenderMode, Editor, EditorSettings, RowInfo, SelectionEffects,
actions::{
ConfirmCodeAction, ConfirmCompletion, ConfirmRename, ContextMenuFirst,
ExpandMacroRecursively, MoveToEnd, Redo, Rename, SelectAll, ToggleCodeActions, Undo,
@@ -348,7 +348,9 @@ async fn test_collaborating_with_completion(cx_a: &mut TestAppContext, cx_b: &mu
// Type a completion trigger character as the guest.
editor_b.update_in(cx_b, |editor, window, cx| {
- editor.change_selections(None, window, cx, |s| s.select_ranges([13..13]));
+ editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
+ s.select_ranges([13..13])
+ });
editor.handle_input(".", window, cx);
});
cx_b.focus(&editor_b);
@@ -461,7 +463,9 @@ async fn test_collaborating_with_completion(cx_a: &mut TestAppContext, cx_b: &mu
// Now we do a second completion, this time to ensure that documentation/snippets are
// resolved
editor_b.update_in(cx_b, |editor, window, cx| {
- editor.change_selections(None, window, cx, |s| s.select_ranges([46..46]));
+ editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
+ s.select_ranges([46..46])
+ });
editor.handle_input("; a", window, cx);
editor.handle_input(".", window, cx);
});
@@ -613,7 +617,7 @@ async fn test_collaborating_with_code_actions(
// Move cursor to a location that contains code actions.
editor_b.update_in(cx_b, |editor, window, cx| {
- editor.change_selections(None, window, cx, |s| {
+ editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
s.select_ranges([Point::new(1, 31)..Point::new(1, 31)])
});
});
@@ -817,7 +821,9 @@ async fn test_collaborating_with_renames(cx_a: &mut TestAppContext, cx_b: &mut T
// Move cursor to a location that can be renamed.
let prepare_rename = editor_b.update_in(cx_b, |editor, window, cx| {
- editor.change_selections(None, window, cx, |s| s.select_ranges([7..7]));
+ editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
+ s.select_ranges([7..7])
+ });
editor.rename(&Rename, window, cx).unwrap()
});
@@ -863,7 +869,9 @@ async fn test_collaborating_with_renames(cx_a: &mut TestAppContext, cx_b: &mut T
editor.cancel(&editor::actions::Cancel, window, cx);
});
let prepare_rename = editor_b.update_in(cx_b, |editor, window, cx| {
- editor.change_selections(None, window, cx, |s| s.select_ranges([7..8]));
+ editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
+ s.select_ranges([7..8])
+ });
editor.rename(&Rename, window, cx).unwrap()
});
@@ -1364,7 +1372,9 @@ async fn test_on_input_format_from_host_to_guest(
// Type a on type formatting trigger character as the guest.
cx_a.focus(&editor_a);
editor_a.update_in(cx_a, |editor, window, cx| {
- editor.change_selections(None, window, cx, |s| s.select_ranges([13..13]));
+ editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
+ s.select_ranges([13..13])
+ });
editor.handle_input(">", window, cx);
});
@@ -1460,7 +1470,9 @@ async fn test_on_input_format_from_guest_to_host(
// Type a on type formatting trigger character as the guest.
cx_b.focus(&editor_b);
editor_b.update_in(cx_b, |editor, window, cx| {
- editor.change_selections(None, window, cx, |s| s.select_ranges([13..13]));
+ editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
+ s.select_ranges([13..13])
+ });
editor.handle_input(":", window, cx);
});
@@ -1697,7 +1709,9 @@ async fn test_mutual_editor_inlay_hint_cache_update(
let after_client_edit = edits_made.fetch_add(1, atomic::Ordering::Release) + 1;
editor_b.update_in(cx_b, |editor, window, cx| {
- editor.change_selections(None, window, cx, |s| s.select_ranges([13..13].clone()));
+ editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
+ s.select_ranges([13..13].clone())
+ });
editor.handle_input(":", window, cx);
});
cx_b.focus(&editor_b);
@@ -1718,7 +1732,9 @@ async fn test_mutual_editor_inlay_hint_cache_update(
let after_host_edit = edits_made.fetch_add(1, atomic::Ordering::Release) + 1;
editor_a.update_in(cx_a, |editor, window, cx| {
- editor.change_selections(None, window, cx, |s| s.select_ranges([13..13]));
+ editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
+ s.select_ranges([13..13])
+ });
editor.handle_input("a change to increment both buffers' versions", window, cx);
});
cx_a.focus(&editor_a);
@@ -2121,7 +2137,9 @@ async fn test_lsp_document_color(cx_a: &mut TestAppContext, cx_b: &mut TestAppCo
});
editor_a.update_in(cx_a, |editor, window, cx| {
- editor.change_selections(None, window, cx, |s| s.select_ranges([13..13].clone()));
+ editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
+ s.select_ranges([13..13].clone())
+ });
editor.handle_input(":", window, cx);
});
color_request_handle.next().await.unwrap();
diff --git a/crates/collab/src/tests/following_tests.rs b/crates/collab/src/tests/following_tests.rs
index 99f9b3350512f8d7eb126cb7a427979ab360d509..a77112213f195190e613c2382300bfbbeca70066 100644
--- a/crates/collab/src/tests/following_tests.rs
+++ b/crates/collab/src/tests/following_tests.rs
@@ -6,7 +6,7 @@ use collab_ui::{
channel_view::ChannelView,
notifications::project_shared_notification::ProjectSharedNotification,
};
-use editor::{Editor, MultiBuffer, PathKey};
+use editor::{Editor, MultiBuffer, PathKey, SelectionEffects};
use gpui::{
AppContext as _, BackgroundExecutor, BorrowAppContext, Entity, SharedString, TestAppContext,
VisualContext, VisualTestContext, point,
@@ -376,7 +376,9 @@ async fn test_basic_following(
// Changes to client A's editor are reflected on client B.
editor_a1.update_in(cx_a, |editor, window, cx| {
- editor.change_selections(None, window, cx, |s| s.select_ranges([1..1, 2..2]));
+ editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
+ s.select_ranges([1..1, 2..2])
+ });
});
executor.advance_clock(workspace::item::LEADER_UPDATE_THROTTLE);
executor.run_until_parked();
@@ -393,7 +395,9 @@ async fn test_basic_following(
editor_b1.update(cx_b, |editor, cx| assert_eq!(editor.text(cx), "TWO"));
editor_a1.update_in(cx_a, |editor, window, cx| {
- editor.change_selections(None, window, cx, |s| s.select_ranges([3..3]));
+ editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
+ s.select_ranges([3..3])
+ });
editor.set_scroll_position(point(0., 100.), window, cx);
});
executor.advance_clock(workspace::item::LEADER_UPDATE_THROTTLE);
@@ -1647,7 +1651,9 @@ async fn test_following_stops_on_unshare(cx_a: &mut TestAppContext, cx_b: &mut T
// b should follow a to position 1
editor_a.update_in(cx_a, |editor, window, cx| {
- editor.change_selections(None, window, cx, |s| s.select_ranges([1..1]))
+ editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
+ s.select_ranges([1..1])
+ })
});
cx_a.executor()
.advance_clock(workspace::item::LEADER_UPDATE_THROTTLE);
@@ -1667,7 +1673,9 @@ async fn test_following_stops_on_unshare(cx_a: &mut TestAppContext, cx_b: &mut T
// b should not follow a to position 2
editor_a.update_in(cx_a, |editor, window, cx| {
- editor.change_selections(None, window, cx, |s| s.select_ranges([2..2]))
+ editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
+ s.select_ranges([2..2])
+ })
});
cx_a.executor()
.advance_clock(workspace::item::LEADER_UPDATE_THROTTLE);
@@ -1968,7 +1976,7 @@ async fn test_following_to_channel_notes_without_a_shared_project(
assert_eq!(notes.channel(cx).unwrap().name, "channel-1");
notes.editor.update(cx, |editor, cx| {
editor.insert("Hello from A.", window, cx);
- editor.change_selections(None, window, cx, |selections| {
+ editor.change_selections(SelectionEffects::no_scroll(), window, cx, |selections| {
selections.select_ranges(vec![3..4]);
});
});
@@ -2109,7 +2117,7 @@ async fn test_following_after_replacement(cx_a: &mut TestAppContext, cx_b: &mut
workspace.add_item_to_center(Box::new(editor.clone()) as _, window, cx)
});
editor.update_in(cx_a, |editor, window, cx| {
- editor.change_selections(None, window, cx, |s| {
+ editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
s.select_ranges([Point::row_range(4..4)]);
})
});
diff --git a/crates/collab/src/tests/integration_tests.rs b/crates/collab/src/tests/integration_tests.rs
index 145a31a179c38eba5ad0312b24ba96afcaa69b49..55427b1aa70fe59dd330e274ddade4839c73affd 100644
--- a/crates/collab/src/tests/integration_tests.rs
+++ b/crates/collab/src/tests/integration_tests.rs
@@ -22,9 +22,7 @@ use gpui::{
use language::{
Diagnostic, DiagnosticEntry, DiagnosticSourceKind, FakeLspAdapter, Language, LanguageConfig,
LanguageMatcher, LineEnding, OffsetRangeExt, Point, Rope,
- language_settings::{
- AllLanguageSettings, Formatter, FormatterList, PrettierSettings, SelectedFormatter,
- },
+ language_settings::{AllLanguageSettings, Formatter, PrettierSettings, SelectedFormatter},
tree_sitter_rust, tree_sitter_typescript,
};
use lsp::{LanguageServerId, OneOf};
@@ -4591,15 +4589,13 @@ async fn test_formatting_buffer(
cx_a.update(|cx| {
SettingsStore::update_global(cx, |store, cx| {
store.update_user_settings::(cx, |file| {
- file.defaults.formatter = Some(SelectedFormatter::List(FormatterList(
- vec![Formatter::External {
+ file.defaults.formatter =
+ Some(SelectedFormatter::List(vec![Formatter::External {
command: "awk".into(),
arguments: Some(
vec!["{sub(/two/,\"{buffer_path}\")}1".to_string()].into(),
),
- }]
- .into(),
- )));
+ }]));
});
});
});
@@ -4699,9 +4695,10 @@ async fn test_prettier_formatting_buffer(
cx_b.update(|cx| {
SettingsStore::update_global(cx, |store, cx| {
store.update_user_settings::(cx, |file| {
- file.defaults.formatter = Some(SelectedFormatter::List(FormatterList(
- vec![Formatter::LanguageServer { name: None }].into(),
- )));
+ file.defaults.formatter =
+ Some(SelectedFormatter::List(vec![Formatter::LanguageServer {
+ name: None,
+ }]));
file.defaults.prettier = Some(PrettierSettings {
allowed: true,
..PrettierSettings::default()
diff --git a/crates/collab/src/tests/remote_editing_collaboration_tests.rs b/crates/collab/src/tests/remote_editing_collaboration_tests.rs
index 217273a38787730c1cf6d5535e3e8e456cffb64a..0e9b25dc380fee929274d2a84607e55ee6832cb8 100644
--- a/crates/collab/src/tests/remote_editing_collaboration_tests.rs
+++ b/crates/collab/src/tests/remote_editing_collaboration_tests.rs
@@ -14,8 +14,7 @@ use http_client::BlockedHttpClient;
use language::{
FakeLspAdapter, Language, LanguageConfig, LanguageMatcher, LanguageRegistry,
language_settings::{
- AllLanguageSettings, Formatter, FormatterList, PrettierSettings, SelectedFormatter,
- language_settings,
+ AllLanguageSettings, Formatter, PrettierSettings, SelectedFormatter, language_settings,
},
tree_sitter_typescript,
};
@@ -505,9 +504,10 @@ async fn test_ssh_collaboration_formatting_with_prettier(
cx_b.update(|cx| {
SettingsStore::update_global(cx, |store, cx| {
store.update_user_settings::(cx, |file| {
- file.defaults.formatter = Some(SelectedFormatter::List(FormatterList(
- vec![Formatter::LanguageServer { name: None }].into(),
- )));
+ file.defaults.formatter =
+ Some(SelectedFormatter::List(vec![Formatter::LanguageServer {
+ name: None,
+ }]));
file.defaults.prettier = Some(PrettierSettings {
allowed: true,
..PrettierSettings::default()
diff --git a/crates/collab_ui/src/channel_view.rs b/crates/collab_ui/src/channel_view.rs
index 80cc504308b30579d80e42e35e3267117a8bc456..c872f99aa10ee160ed499621d9aceb2aa7c06a05 100644
--- a/crates/collab_ui/src/channel_view.rs
+++ b/crates/collab_ui/src/channel_view.rs
@@ -7,8 +7,8 @@ use client::{
};
use collections::HashMap;
use editor::{
- CollaborationHub, DisplayPoint, Editor, EditorEvent, display_map::ToDisplayPoint,
- scroll::Autoscroll,
+ CollaborationHub, DisplayPoint, Editor, EditorEvent, SelectionEffects,
+ display_map::ToDisplayPoint, scroll::Autoscroll,
};
use gpui::{
AnyView, App, ClipboardItem, Context, Entity, EventEmitter, Focusable, Pixels, Point, Render,
@@ -260,9 +260,16 @@ impl ChannelView {
.find(|item| &Channel::slug(&item.text).to_lowercase() == &position)
{
self.editor.update(cx, |editor, cx| {
- editor.change_selections(Some(Autoscroll::focused()), window, cx, |s| {
- s.replace_cursors_with(|map| vec![item.range.start.to_display_point(map)])
- })
+ editor.change_selections(
+ SelectionEffects::scroll(Autoscroll::focused()),
+ window,
+ cx,
+ |s| {
+ s.replace_cursors_with(|map| {
+ vec![item.range.start.to_display_point(map)]
+ })
+ },
+ )
});
return;
}
diff --git a/crates/collab_ui/src/panel_settings.rs b/crates/collab_ui/src/panel_settings.rs
index 497b403019bfddf2c90be501a8e85c175accc8c7..652d9eb67f6ce1f0ab583e20e4feab05cfb743e3 100644
--- a/crates/collab_ui/src/panel_settings.rs
+++ b/crates/collab_ui/src/panel_settings.rs
@@ -28,7 +28,6 @@ pub struct ChatPanelSettings {
}
#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug)]
-#[schemars(deny_unknown_fields)]
pub struct ChatPanelSettingsContent {
/// When to show the panel button in the status bar.
///
@@ -52,7 +51,6 @@ pub struct NotificationPanelSettings {
}
#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug)]
-#[schemars(deny_unknown_fields)]
pub struct PanelSettingsContent {
/// Whether to show the panel button in the status bar.
///
@@ -69,7 +67,6 @@ pub struct PanelSettingsContent {
}
#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug)]
-#[schemars(deny_unknown_fields)]
pub struct MessageEditorSettings {
/// Whether to automatically replace emoji shortcodes with emoji characters.
/// For example: typing `:wave:` gets replaced with `👋`.
diff --git a/crates/command_palette/src/command_palette.rs b/crates/command_palette/src/command_palette.rs
index 2e411fd139c4d6410bee6512dd3537a9592f2420..abb8978d5a103fb66f862af6c5ee69beee0f6251 100644
--- a/crates/command_palette/src/command_palette.rs
+++ b/crates/command_palette/src/command_palette.rs
@@ -41,7 +41,7 @@ pub struct CommandPalette {
/// Removes subsequent whitespace characters and double colons from the query.
///
/// This improves the likelihood of a match by either humanized name or keymap-style name.
-fn normalize_query(input: &str) -> String {
+pub fn normalize_action_query(input: &str) -> String {
let mut result = String::with_capacity(input.len());
let mut last_char = None;
@@ -297,7 +297,7 @@ impl PickerDelegate for CommandPaletteDelegate {
let mut commands = self.all_commands.clone();
let hit_counts = self.hit_counts();
let executor = cx.background_executor().clone();
- let query = normalize_query(query.as_str());
+ let query = normalize_action_query(query.as_str());
async move {
commands.sort_by_key(|action| {
(
@@ -311,29 +311,17 @@ impl PickerDelegate for CommandPaletteDelegate {
.enumerate()
.map(|(ix, command)| StringMatchCandidate::new(ix, &command.name))
.collect::>();
- let matches = if query.is_empty() {
- candidates
- .into_iter()
- .enumerate()
- .map(|(index, candidate)| StringMatch {
- candidate_id: index,
- string: candidate.string,
- positions: Vec::new(),
- score: 0.0,
- })
- .collect()
- } else {
- fuzzy::match_strings(
- &candidates,
- &query,
- true,
- true,
- 10000,
- &Default::default(),
- executor,
- )
- .await
- };
+
+ let matches = fuzzy::match_strings(
+ &candidates,
+ &query,
+ true,
+ true,
+ 10000,
+ &Default::default(),
+ executor,
+ )
+ .await;
tx.send((commands, matches)).await.log_err();
}
@@ -422,8 +410,8 @@ impl PickerDelegate for CommandPaletteDelegate {
window: &mut Window,
cx: &mut Context>,
) -> Option {
- let r#match = self.matches.get(ix)?;
- let command = self.commands.get(r#match.candidate_id)?;
+ let matching_command = self.matches.get(ix)?;
+ let command = self.commands.get(matching_command.candidate_id)?;
Some(
ListItem::new(ix)
.inset(true)
@@ -436,7 +424,7 @@ impl PickerDelegate for CommandPaletteDelegate {
.justify_between()
.child(HighlightedLabel::new(
command.name.clone(),
- r#match.positions.clone(),
+ matching_command.positions.clone(),
))
.children(KeyBinding::for_action_in(
&*command.action,
@@ -512,19 +500,28 @@ mod tests {
#[test]
fn test_normalize_query() {
- assert_eq!(normalize_query("editor: backspace"), "editor: backspace");
- assert_eq!(normalize_query("editor: backspace"), "editor: backspace");
- assert_eq!(normalize_query("editor: backspace"), "editor: backspace");
assert_eq!(
- normalize_query("editor::GoToDefinition"),
+ normalize_action_query("editor: backspace"),
+ "editor: backspace"
+ );
+ assert_eq!(
+ normalize_action_query("editor: backspace"),
+ "editor: backspace"
+ );
+ assert_eq!(
+ normalize_action_query("editor: backspace"),
+ "editor: backspace"
+ );
+ assert_eq!(
+ normalize_action_query("editor::GoToDefinition"),
"editor:GoToDefinition"
);
assert_eq!(
- normalize_query("editor::::GoToDefinition"),
+ normalize_action_query("editor::::GoToDefinition"),
"editor:GoToDefinition"
);
assert_eq!(
- normalize_query("editor: :GoToDefinition"),
+ normalize_action_query("editor: :GoToDefinition"),
"editor: :GoToDefinition"
);
}
diff --git a/crates/context_server/src/context_server.rs b/crates/context_server/src/context_server.rs
index f774deef170086f4b17e8f83d8fd5ec0557528e0..905435fcce57dc8ce8719e5056b28118168e9a04 100644
--- a/crates/context_server/src/context_server.rs
+++ b/crates/context_server/src/context_server.rs
@@ -29,6 +29,7 @@ impl Display for ContextServerId {
#[derive(Deserialize, Serialize, Clone, PartialEq, Eq, JsonSchema)]
pub struct ContextServerCommand {
+ #[serde(rename = "command")]
pub path: String,
pub args: Vec,
pub env: Option>,
diff --git a/crates/copilot/src/copilot_chat.rs b/crates/copilot/src/copilot_chat.rs
index 19cff56c911b2d85f91b5075f238f8320f846e3f..b1fa1565f30ed79fdff763964708fe01c62d023f 100644
--- a/crates/copilot/src/copilot_chat.rs
+++ b/crates/copilot/src/copilot_chat.rs
@@ -698,16 +698,16 @@ async fn stream_completion(
completion_url: Arc,
request: Request,
) -> Result>> {
- let is_vision_request = request.messages.last().map_or(false, |message| match message {
- ChatMessage::User { content }
- | ChatMessage::Assistant { content, .. }
- | ChatMessage::Tool { content, .. } => {
- matches!(content, ChatMessageContent::Multipart(parts) if parts.iter().any(|part| matches!(part, ChatMessagePart::Image { .. })))
- }
- _ => false,
- });
-
- let request_builder = HttpRequest::builder()
+ let is_vision_request = request.messages.iter().any(|message| match message {
+ ChatMessage::User { content }
+ | ChatMessage::Assistant { content, .. }
+ | ChatMessage::Tool { content, .. } => {
+ matches!(content, ChatMessageContent::Multipart(parts) if parts.iter().any(|part| matches!(part, ChatMessagePart::Image { .. })))
+ }
+ _ => false,
+ });
+
+ let mut request_builder = HttpRequest::builder()
.method(Method::POST)
.uri(completion_url.as_ref())
.header(
@@ -719,8 +719,12 @@ async fn stream_completion(
)
.header("Authorization", format!("Bearer {}", api_key))
.header("Content-Type", "application/json")
- .header("Copilot-Integration-Id", "vscode-chat")
- .header("Copilot-Vision-Request", is_vision_request.to_string());
+ .header("Copilot-Integration-Id", "vscode-chat");
+
+ if is_vision_request {
+ request_builder =
+ request_builder.header("Copilot-Vision-Request", is_vision_request.to_string());
+ }
let is_streaming = request.stream;
diff --git a/crates/copilot/src/copilot_completion_provider.rs b/crates/copilot/src/copilot_completion_provider.rs
index ff636178753b11bbe3be920a27a27a5c467cef5e..8dc04622f9020c2fe175304764157b409c7936c1 100644
--- a/crates/copilot/src/copilot_completion_provider.rs
+++ b/crates/copilot/src/copilot_completion_provider.rs
@@ -264,7 +264,8 @@ fn common_prefix, T2: Iterator
- >(a: T1, b:
mod tests {
use super::*;
use editor::{
- Editor, ExcerptRange, MultiBuffer, test::editor_lsp_test_context::EditorLspTestContext,
+ Editor, ExcerptRange, MultiBuffer, SelectionEffects,
+ test::editor_lsp_test_context::EditorLspTestContext,
};
use fs::FakeFs;
use futures::StreamExt;
@@ -478,7 +479,7 @@ mod tests {
// Reset the editor to verify how suggestions behave when tabbing on leading indentation.
cx.update_editor(|editor, window, cx| {
editor.set_text("fn foo() {\n \n}", window, cx);
- editor.change_selections(None, window, cx, |s| {
+ editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
s.select_ranges([Point::new(1, 2)..Point::new(1, 2)])
});
});
@@ -767,7 +768,7 @@ mod tests {
);
_ = editor.update(cx, |editor, window, cx| {
// Ensure copilot suggestions are shown for the first excerpt.
- editor.change_selections(None, window, cx, |s| {
+ editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
s.select_ranges([Point::new(1, 5)..Point::new(1, 5)])
});
editor.next_edit_prediction(&Default::default(), window, cx);
@@ -793,7 +794,7 @@ mod tests {
);
_ = editor.update(cx, |editor, window, cx| {
// Move to another excerpt, ensuring the suggestion gets cleared.
- editor.change_selections(None, window, cx, |s| {
+ editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
s.select_ranges([Point::new(4, 5)..Point::new(4, 5)])
});
assert!(!editor.has_active_inline_completion());
@@ -1019,7 +1020,7 @@ mod tests {
);
_ = editor.update(cx, |editor, window, cx| {
- editor.change_selections(None, window, cx, |selections| {
+ editor.change_selections(SelectionEffects::no_scroll(), window, cx, |selections| {
selections.select_ranges([Point::new(0, 0)..Point::new(0, 0)])
});
editor.refresh_inline_completion(true, false, window, cx);
@@ -1029,7 +1030,7 @@ mod tests {
assert!(copilot_requests.try_next().is_err());
_ = editor.update(cx, |editor, window, cx| {
- editor.change_selections(None, window, cx, |s| {
+ editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
s.select_ranges([Point::new(5, 0)..Point::new(5, 0)])
});
editor.refresh_inline_completion(true, false, window, cx);
diff --git a/crates/dap/src/adapters.rs b/crates/dap/src/adapters.rs
index 8e1c84083f18835dee6c4bc3bea4ce7c45147499..d9f26b3b348985f2e52423cb217b1c1446960bbf 100644
--- a/crates/dap/src/adapters.rs
+++ b/crates/dap/src/adapters.rs
@@ -10,6 +10,7 @@ use gpui::{AsyncApp, SharedString};
pub use http_client::{HttpClient, github::latest_github_release};
use language::{LanguageName, LanguageToolchainStore};
use node_runtime::NodeRuntime;
+use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use settings::WorktreeId;
use smol::fs::File;
@@ -47,7 +48,10 @@ pub trait DapDelegate: Send + Sync + 'static {
async fn shell_env(&self) -> collections::HashMap;
}
-#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Deserialize, Serialize)]
+#[derive(
+ Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Deserialize, Serialize, JsonSchema,
+)]
+#[serde(transparent)]
pub struct DebugAdapterName(pub SharedString);
impl Deref for DebugAdapterName {
diff --git a/crates/dap_adapters/Cargo.toml b/crates/dap_adapters/Cargo.toml
index e2e922bd56ca3edcede5184358bfca443905b50e..65544fbb6a1b7565c4fe641058e4e6c725b21016 100644
--- a/crates/dap_adapters/Cargo.toml
+++ b/crates/dap_adapters/Cargo.toml
@@ -25,7 +25,9 @@ anyhow.workspace = true
async-trait.workspace = true
collections.workspace = true
dap.workspace = true
+dotenvy.workspace = true
futures.workspace = true
+fs.workspace = true
gpui.workspace = true
json_dotpath.workspace = true
language.workspace = true
@@ -33,6 +35,7 @@ log.workspace = true
paths.workspace = true
serde.workspace = true
serde_json.workspace = true
+shlex.workspace = true
task.workspace = true
util.workspace = true
workspace-hack.workspace = true
diff --git a/crates/dap_adapters/src/codelldb.rs b/crates/dap_adapters/src/codelldb.rs
index 5d14cc87475c814639ab8e15b54df46d9a01dd4c..5b88db4432d3823e8a85228a82ca064cfacad23c 100644
--- a/crates/dap_adapters/src/codelldb.rs
+++ b/crates/dap_adapters/src/codelldb.rs
@@ -22,17 +22,16 @@ impl CodeLldbDebugAdapter {
async fn request_args(
&self,
delegate: &Arc,
- task_definition: &DebugTaskDefinition,
+ mut configuration: Value,
+ label: &str,
) -> Result {
- // CodeLLDB uses `name` for a terminal label.
- let mut configuration = task_definition.config.clone();
-
let obj = configuration
.as_object_mut()
.context("CodeLLDB is not a valid json object")?;
+ // CodeLLDB uses `name` for a terminal label.
obj.entry("name")
- .or_insert(Value::String(String::from(task_definition.label.as_ref())));
+ .or_insert(Value::String(String::from(label)));
obj.entry("cwd")
.or_insert(delegate.worktree_root_path().to_string_lossy().into());
@@ -361,17 +360,31 @@ impl DebugAdapter for CodeLldbDebugAdapter {
self.path_to_codelldb.set(path.clone()).ok();
command = Some(path);
};
-
+ let mut json_config = config.config.clone();
Ok(DebugAdapterBinary {
command: Some(command.unwrap()),
cwd: Some(delegate.worktree_root_path().to_path_buf()),
arguments: user_args.unwrap_or_else(|| {
- vec![
- "--settings".into(),
- json!({"sourceLanguages": ["cpp", "rust"]}).to_string(),
- ]
+ if let Some(config) = json_config.as_object_mut()
+ && let Some(source_languages) = config.get("sourceLanguages").filter(|value| {
+ value
+ .as_array()
+ .map_or(false, |array| array.iter().all(Value::is_string))
+ })
+ {
+ let ret = vec![
+ "--settings".into(),
+ json!({"sourceLanguages": source_languages}).to_string(),
+ ];
+ config.remove("sourceLanguages");
+ ret
+ } else {
+ vec![]
+ }
}),
- request_args: self.request_args(delegate, &config).await?,
+ request_args: self
+ .request_args(delegate, json_config, &config.label)
+ .await?,
envs: HashMap::default(),
connection: None,
})
diff --git a/crates/dap_adapters/src/dap_adapters.rs b/crates/dap_adapters/src/dap_adapters.rs
index 79c56fdf25583e6cbe3a182b3abf464ac449eb27..c254302e7144b53500fd2a3b84be06e8ec30c2a0 100644
--- a/crates/dap_adapters/src/dap_adapters.rs
+++ b/crates/dap_adapters/src/dap_adapters.rs
@@ -4,7 +4,6 @@ mod go;
mod javascript;
mod php;
mod python;
-mod ruby;
use std::sync::Arc;
@@ -25,7 +24,6 @@ use gpui::{App, BorrowAppContext};
use javascript::JsDebugAdapter;
use php::PhpDebugAdapter;
use python::PythonDebugAdapter;
-use ruby::RubyDebugAdapter;
use serde_json::json;
use task::{DebugScenario, ZedDebugConfig};
@@ -35,7 +33,6 @@ pub fn init(cx: &mut App) {
registry.add_adapter(Arc::from(PythonDebugAdapter::default()));
registry.add_adapter(Arc::from(PhpDebugAdapter::default()));
registry.add_adapter(Arc::from(JsDebugAdapter::default()));
- registry.add_adapter(Arc::from(RubyDebugAdapter));
registry.add_adapter(Arc::from(GoDebugAdapter::default()));
registry.add_adapter(Arc::from(GdbDebugAdapter));
diff --git a/crates/dap_adapters/src/go.rs b/crates/dap_adapters/src/go.rs
index bc3f5007454adee4cfcbc8a3cf09c87ae0100b97..d32f5cbf3426f1b669132e74e389862e7944267b 100644
--- a/crates/dap_adapters/src/go.rs
+++ b/crates/dap_adapters/src/go.rs
@@ -7,13 +7,22 @@ use dap::{
latest_github_release,
},
};
-
+use fs::Fs;
use gpui::{AsyncApp, SharedString};
use language::LanguageName;
-use std::{env::consts, ffi::OsStr, path::PathBuf, sync::OnceLock};
+use log::warn;
+use serde_json::{Map, Value};
use task::TcpArgumentsTemplate;
use util;
+use std::{
+ env::consts,
+ ffi::OsStr,
+ path::{Path, PathBuf},
+ str::FromStr,
+ sync::OnceLock,
+};
+
use crate::*;
#[derive(Default, Debug)]
@@ -437,22 +446,34 @@ impl DebugAdapter for GoDebugAdapter {
adapter_path.join("dlv").to_string_lossy().to_string()
};
- let cwd = task_definition
- .config
- .get("cwd")
- .and_then(|s| s.as_str())
- .map(PathBuf::from)
- .unwrap_or_else(|| delegate.worktree_root_path().to_path_buf());
+ let cwd = Some(
+ task_definition
+ .config
+ .get("cwd")
+ .and_then(|s| s.as_str())
+ .map(PathBuf::from)
+ .unwrap_or_else(|| delegate.worktree_root_path().to_path_buf()),
+ );
let arguments;
let command;
let connection;
let mut configuration = task_definition.config.clone();
+ let mut envs = HashMap::default();
+
if let Some(configuration) = configuration.as_object_mut() {
configuration
.entry("cwd")
.or_insert_with(|| delegate.worktree_root_path().to_string_lossy().into());
+
+ handle_envs(
+ configuration,
+ &mut envs,
+ cwd.as_deref(),
+ delegate.fs().clone(),
+ )
+ .await;
}
if let Some(connection_options) = &task_definition.tcp_connection {
@@ -494,8 +515,8 @@ impl DebugAdapter for GoDebugAdapter {
Ok(DebugAdapterBinary {
command,
arguments,
- cwd: Some(cwd),
- envs: HashMap::default(),
+ cwd,
+ envs,
connection,
request_args: StartDebuggingRequestArguments {
configuration,
@@ -504,3 +525,44 @@ impl DebugAdapter for GoDebugAdapter {
})
}
}
+
+// delve doesn't do anything with the envFile setting, so we intercept it
+async fn handle_envs(
+ config: &mut Map,
+ envs: &mut HashMap,
+ cwd: Option<&Path>,
+ fs: Arc,
+) -> Option<()> {
+ let env_files = match config.get("envFile")? {
+ Value::Array(arr) => arr.iter().map(|v| v.as_str()).collect::>(),
+ Value::String(s) => vec![Some(s.as_str())],
+ _ => return None,
+ };
+
+ let rebase_path = |path: PathBuf| {
+ if path.is_absolute() {
+ Some(path)
+ } else {
+ cwd.map(|p| p.join(path))
+ }
+ };
+
+ for path in env_files {
+ let Some(path) = path
+ .and_then(|s| PathBuf::from_str(s).ok())
+ .and_then(rebase_path)
+ else {
+ continue;
+ };
+
+ if let Ok(file) = fs.open_sync(&path).await {
+ envs.extend(dotenvy::from_read_iter(file).filter_map(Result::ok))
+ } else {
+ warn!("While starting Go debug session: failed to read env file {path:?}");
+ };
+ }
+
+ // remove envFile now that it's been handled
+ config.remove("entry");
+ Some(())
+}
diff --git a/crates/dap_adapters/src/javascript.rs b/crates/dap_adapters/src/javascript.rs
index d5d78186acc9c76fc2dda5d096b099bd52aaf2a4..67adb5629bdb3d32730fe0bbb24d3c1ee6893ab1 100644
--- a/crates/dap_adapters/src/javascript.rs
+++ b/crates/dap_adapters/src/javascript.rs
@@ -5,7 +5,7 @@ use gpui::AsyncApp;
use serde_json::Value;
use std::{collections::HashMap, path::PathBuf, sync::OnceLock};
use task::DebugRequest;
-use util::ResultExt;
+use util::{ResultExt, maybe};
use crate::*;
@@ -72,6 +72,24 @@ impl JsDebugAdapter {
let mut configuration = task_definition.config.clone();
if let Some(configuration) = configuration.as_object_mut() {
+ maybe!({
+ configuration
+ .get("type")
+ .filter(|value| value == &"node-terminal")?;
+ let command = configuration.get("command")?.as_str()?.to_owned();
+ let mut args = shlex::split(&command)?.into_iter();
+ let program = args.next()?;
+ configuration.insert("program".to_owned(), program.into());
+ configuration.insert(
+ "args".to_owned(),
+ args.map(Value::from).collect::>().into(),
+ );
+ configuration.insert("console".to_owned(), "externalTerminal".into());
+ Some(())
+ });
+
+ configuration.entry("type").and_modify(normalize_task_type);
+
if let Some(program) = configuration
.get("program")
.cloned()
@@ -96,7 +114,6 @@ impl JsDebugAdapter {
.entry("cwd")
.or_insert(delegate.worktree_root_path().to_string_lossy().into());
- configuration.entry("type").and_modify(normalize_task_type);
configuration
.entry("console")
.or_insert("externalTerminal".into());
@@ -265,6 +282,10 @@ impl DebugAdapter for JsDebugAdapter {
"description": "Automatically stop program after launch",
"default": false
},
+ "attachSimplePort": {
+ "type": "number",
+ "description": "If set, attaches to the process via the given port. This is generally no longer necessary for Node.js programs and loses the ability to debug child processes, but can be useful in more esoteric scenarios such as with Deno and Docker launches. If set to 0, a random port will be chosen and --inspect-brk added to the launch arguments automatically."
+ },
"runtimeExecutable": {
"type": ["string", "null"],
"description": "Runtime to use, an absolute path or the name of a runtime available on PATH",
@@ -512,7 +533,7 @@ fn normalize_task_type(task_type: &mut Value) {
};
let new_name = match task_type_str {
- "node" | "pwa-node" => "pwa-node",
+ "node" | "pwa-node" | "node-terminal" => "pwa-node",
"chrome" | "pwa-chrome" => "pwa-chrome",
"edge" | "msedge" | "pwa-edge" | "pwa-msedge" => "pwa-msedge",
_ => task_type_str,
diff --git a/crates/dap_adapters/src/ruby.rs b/crates/dap_adapters/src/ruby.rs
deleted file mode 100644
index 28f1fb1f5ff155329a0629889cfb7d197dd6ce68..0000000000000000000000000000000000000000
--- a/crates/dap_adapters/src/ruby.rs
+++ /dev/null
@@ -1,208 +0,0 @@
-use anyhow::{Result, bail};
-use async_trait::async_trait;
-use collections::FxHashMap;
-use dap::{
- DebugRequest, StartDebuggingRequestArguments, StartDebuggingRequestArgumentsRequest,
- adapters::{
- DapDelegate, DebugAdapter, DebugAdapterBinary, DebugAdapterName, DebugTaskDefinition,
- },
-};
-use gpui::{AsyncApp, SharedString};
-use language::LanguageName;
-use serde::{Deserialize, Serialize};
-use serde_json::json;
-use std::path::PathBuf;
-use std::{ffi::OsStr, sync::Arc};
-use task::{DebugScenario, ZedDebugConfig};
-use util::command::new_smol_command;
-
-#[derive(Default)]
-pub(crate) struct RubyDebugAdapter;
-
-impl RubyDebugAdapter {
- const ADAPTER_NAME: &'static str = "Ruby";
-}
-
-#[derive(Serialize, Deserialize)]
-struct RubyDebugConfig {
- script_or_command: Option,
- script: Option,
- command: Option,
- #[serde(default)]
- args: Vec,
- #[serde(default)]
- env: FxHashMap,
- cwd: Option,
-}
-
-#[async_trait(?Send)]
-impl DebugAdapter for RubyDebugAdapter {
- fn name(&self) -> DebugAdapterName {
- DebugAdapterName(Self::ADAPTER_NAME.into())
- }
-
- fn adapter_language_name(&self) -> Option {
- Some(SharedString::new_static("Ruby").into())
- }
-
- async fn request_kind(
- &self,
- _: &serde_json::Value,
- ) -> Result {
- Ok(StartDebuggingRequestArgumentsRequest::Launch)
- }
-
- fn dap_schema(&self) -> serde_json::Value {
- json!({
- "type": "object",
- "properties": {
- "command": {
- "type": "string",
- "description": "Command name (ruby, rake, bin/rails, bundle exec ruby, etc)",
- },
- "script": {
- "type": "string",
- "description": "Absolute path to a Ruby file."
- },
- "cwd": {
- "type": "string",
- "description": "Directory to execute the program in",
- "default": "${ZED_WORKTREE_ROOT}"
- },
- "args": {
- "type": "array",
- "description": "Command line arguments passed to the program",
- "items": {
- "type": "string"
- },
- "default": []
- },
- "env": {
- "type": "object",
- "description": "Additional environment variables to pass to the debugging (and debugged) process",
- "default": {}
- },
- }
- })
- }
-
- async fn config_from_zed_format(&self, zed_scenario: ZedDebugConfig) -> Result {
- match zed_scenario.request {
- DebugRequest::Launch(launch) => {
- let config = RubyDebugConfig {
- script_or_command: Some(launch.program),
- script: None,
- command: None,
- args: launch.args,
- env: launch.env,
- cwd: launch.cwd.clone(),
- };
-
- let config = serde_json::to_value(config)?;
-
- Ok(DebugScenario {
- adapter: zed_scenario.adapter,
- label: zed_scenario.label,
- config,
- tcp_connection: None,
- build: None,
- })
- }
- DebugRequest::Attach(_) => {
- anyhow::bail!("Attach requests are unsupported");
- }
- }
- }
-
- async fn get_binary(
- &self,
- delegate: &Arc,
- definition: &DebugTaskDefinition,
- _user_installed_path: Option,
- _user_args: Option>,
- _cx: &mut AsyncApp,
- ) -> Result {
- let adapter_path = paths::debug_adapters_dir().join(self.name().as_ref());
- let mut rdbg_path = adapter_path.join("rdbg");
- if !delegate.fs().is_file(&rdbg_path).await {
- match delegate.which("rdbg".as_ref()).await {
- Some(path) => rdbg_path = path,
- None => {
- delegate.output_to_console(
- "rdbg not found on path, trying `gem install debug`".to_string(),
- );
- let output = new_smol_command("gem")
- .arg("install")
- .arg("--no-document")
- .arg("--bindir")
- .arg(adapter_path)
- .arg("debug")
- .output()
- .await?;
- anyhow::ensure!(
- output.status.success(),
- "Failed to install rdbg:\n{}",
- String::from_utf8_lossy(&output.stderr).to_string()
- );
- }
- }
- }
-
- let tcp_connection = definition.tcp_connection.clone().unwrap_or_default();
- let (host, port, timeout) = crate::configure_tcp_connection(tcp_connection).await?;
- let ruby_config = serde_json::from_value::(definition.config.clone())?;
-
- let mut arguments = vec![
- "--open".to_string(),
- format!("--port={}", port),
- format!("--host={}", host),
- ];
-
- if let Some(script) = &ruby_config.script {
- arguments.push(script.clone());
- } else if let Some(command) = &ruby_config.command {
- arguments.push("--command".to_string());
- arguments.push(command.clone());
- } else if let Some(command_or_script) = &ruby_config.script_or_command {
- if delegate
- .which(OsStr::new(&command_or_script))
- .await
- .is_some()
- {
- arguments.push("--command".to_string());
- }
- arguments.push(command_or_script.clone());
- } else {
- bail!("Ruby debug config must have 'script' or 'command' args");
- }
-
- arguments.extend(ruby_config.args);
-
- let mut configuration = definition.config.clone();
- if let Some(configuration) = configuration.as_object_mut() {
- configuration
- .entry("cwd")
- .or_insert_with(|| delegate.worktree_root_path().to_string_lossy().into());
- }
-
- Ok(DebugAdapterBinary {
- command: Some(rdbg_path.to_string_lossy().to_string()),
- arguments,
- connection: Some(dap::adapters::TcpArguments {
- host,
- port,
- timeout,
- }),
- cwd: Some(
- ruby_config
- .cwd
- .unwrap_or(delegate.worktree_root_path().to_owned()),
- ),
- envs: ruby_config.env.into_iter().collect(),
- request_args: StartDebuggingRequestArguments {
- request: self.request_kind(&definition.config).await?,
- configuration,
- },
- })
- }
-}
diff --git a/crates/debugger_tools/src/dap_log.rs b/crates/debugger_tools/src/dap_log.rs
index fb5a345725dbcd93a08676a7cd85de6c10088bb5..532107f63302e9e057d2f90c28a2b32bcd0622d7 100644
--- a/crates/debugger_tools/src/dap_log.rs
+++ b/crates/debugger_tools/src/dap_log.rs
@@ -21,7 +21,7 @@ use project::{
use settings::Settings as _;
use std::{
borrow::Cow,
- collections::{HashMap, VecDeque},
+ collections::{BTreeMap, HashMap, VecDeque},
sync::Arc,
};
use util::maybe;
@@ -32,13 +32,6 @@ use workspace::{
ui::{Button, Clickable, ContextMenu, Label, LabelCommon, PopoverMenu, h_flex},
};
-// TODO:
-// - [x] stop sorting by session ID
-// - [x] pick the most recent session by default (logs if available, RPC messages otherwise)
-// - [ ] dump the launch/attach request somewhere (logs?)
-
-const MAX_SESSIONS: usize = 10;
-
struct DapLogView {
editor: Entity,
focus_handle: FocusHandle,
@@ -49,14 +42,34 @@ struct DapLogView {
_subscriptions: Vec,
}
+struct LogStoreEntryIdentifier<'a> {
+ session_id: SessionId,
+ project: Cow<'a, WeakEntity>,
+}
+impl LogStoreEntryIdentifier<'_> {
+ fn to_owned(&self) -> LogStoreEntryIdentifier<'static> {
+ LogStoreEntryIdentifier {
+ session_id: self.session_id,
+ project: Cow::Owned(self.project.as_ref().clone()),
+ }
+ }
+}
+
+struct LogStoreMessage {
+ id: LogStoreEntryIdentifier<'static>,
+ kind: IoKind,
+ command: Option,
+ message: SharedString,
+}
+
pub struct LogStore {
projects: HashMap, ProjectState>,
- debug_sessions: VecDeque,
- rpc_tx: UnboundedSender<(SessionId, IoKind, Option, SharedString)>,
- adapter_log_tx: UnboundedSender<(SessionId, IoKind, Option, SharedString)>,
+ rpc_tx: UnboundedSender,
+ adapter_log_tx: UnboundedSender,
}
struct ProjectState {
+ debug_sessions: BTreeMap,
_subscriptions: [gpui::Subscription; 2],
}
@@ -122,13 +135,12 @@ impl DebugAdapterState {
impl LogStore {
pub fn new(cx: &Context) -> Self {
- let (rpc_tx, mut rpc_rx) =
- unbounded::<(SessionId, IoKind, Option, SharedString)>();
+ let (rpc_tx, mut rpc_rx) = unbounded::();
cx.spawn(async move |this, cx| {
- while let Some((session_id, io_kind, command, message)) = rpc_rx.next().await {
+ while let Some(message) = rpc_rx.next().await {
if let Some(this) = this.upgrade() {
this.update(cx, |this, cx| {
- this.add_debug_adapter_message(session_id, io_kind, command, message, cx);
+ this.add_debug_adapter_message(message, cx);
})?;
}
@@ -138,13 +150,12 @@ impl LogStore {
})
.detach_and_log_err(cx);
- let (adapter_log_tx, mut adapter_log_rx) =
- unbounded::<(SessionId, IoKind, Option, SharedString)>();
+ let (adapter_log_tx, mut adapter_log_rx) = unbounded::();
cx.spawn(async move |this, cx| {
- while let Some((session_id, io_kind, _, message)) = adapter_log_rx.next().await {
+ while let Some(message) = adapter_log_rx.next().await {
if let Some(this) = this.upgrade() {
this.update(cx, |this, cx| {
- this.add_debug_adapter_log(session_id, io_kind, message, cx);
+ this.add_debug_adapter_log(message, cx);
})?;
}
@@ -157,57 +168,76 @@ impl LogStore {
rpc_tx,
adapter_log_tx,
projects: HashMap::new(),
- debug_sessions: Default::default(),
}
}
pub fn add_project(&mut self, project: &Entity, cx: &mut Context) {
- let weak_project = project.downgrade();
self.projects.insert(
project.downgrade(),
ProjectState {
_subscriptions: [
- cx.observe_release(project, move |this, _, _| {
- this.projects.remove(&weak_project);
+ cx.observe_release(project, {
+ let weak_project = project.downgrade();
+ move |this, _, _| {
+ this.projects.remove(&weak_project);
+ }
}),
- cx.subscribe(
- &project.read(cx).dap_store(),
- |this, dap_store, event, cx| match event {
+ cx.subscribe(&project.read(cx).dap_store(), {
+ let weak_project = project.downgrade();
+ move |this, dap_store, event, cx| match event {
dap_store::DapStoreEvent::DebugClientStarted(session_id) => {
let session = dap_store.read(cx).session_by_id(session_id);
if let Some(session) = session {
- this.add_debug_session(*session_id, session, cx);
+ this.add_debug_session(
+ LogStoreEntryIdentifier {
+ project: Cow::Owned(weak_project.clone()),
+ session_id: *session_id,
+ },
+ session,
+ cx,
+ );
}
}
dap_store::DapStoreEvent::DebugClientShutdown(session_id) => {
- this.get_debug_adapter_state(*session_id)
- .iter_mut()
- .for_each(|state| state.is_terminated = true);
+ let id = LogStoreEntryIdentifier {
+ project: Cow::Borrowed(&weak_project),
+ session_id: *session_id,
+ };
+ if let Some(state) = this.get_debug_adapter_state(&id) {
+ state.is_terminated = true;
+ }
+
this.clean_sessions(cx);
}
_ => {}
- },
- ),
+ }
+ }),
],
+ debug_sessions: Default::default(),
},
);
}
- fn get_debug_adapter_state(&mut self, id: SessionId) -> Option<&mut DebugAdapterState> {
- self.debug_sessions
- .iter_mut()
- .find(|adapter_state| adapter_state.id == id)
+ fn get_debug_adapter_state(
+ &mut self,
+ id: &LogStoreEntryIdentifier<'_>,
+ ) -> Option<&mut DebugAdapterState> {
+ self.projects
+ .get_mut(&id.project)
+ .and_then(|state| state.debug_sessions.get_mut(&id.session_id))
}
fn add_debug_adapter_message(
&mut self,
- id: SessionId,
- io_kind: IoKind,
- command: Option,
- message: SharedString,
+ LogStoreMessage {
+ id,
+ kind: io_kind,
+ command,
+ message,
+ }: LogStoreMessage,
cx: &mut Context,
) {
- let Some(debug_client_state) = self.get_debug_adapter_state(id) else {
+ let Some(debug_client_state) = self.get_debug_adapter_state(&id) else {
return;
};
@@ -229,7 +259,7 @@ impl LogStore {
if rpc_messages.last_message_kind != Some(kind) {
Self::get_debug_adapter_entry(
&mut rpc_messages.messages,
- id,
+ id.to_owned(),
kind.label().into(),
LogKind::Rpc,
cx,
@@ -239,7 +269,7 @@ impl LogStore {
let entry = Self::get_debug_adapter_entry(
&mut rpc_messages.messages,
- id,
+ id.to_owned(),
message,
LogKind::Rpc,
cx,
@@ -260,12 +290,15 @@ impl LogStore {
fn add_debug_adapter_log(
&mut self,
- id: SessionId,
- io_kind: IoKind,
- message: SharedString,
+ LogStoreMessage {
+ id,
+ kind: io_kind,
+ message,
+ ..
+ }: LogStoreMessage,
cx: &mut Context,
) {
- let Some(debug_adapter_state) = self.get_debug_adapter_state(id) else {
+ let Some(debug_adapter_state) = self.get_debug_adapter_state(&id) else {
return;
};
@@ -276,7 +309,7 @@ impl LogStore {
Self::get_debug_adapter_entry(
&mut debug_adapter_state.log_messages,
- id,
+ id.to_owned(),
message,
LogKind::Adapter,
cx,
@@ -286,13 +319,17 @@ impl LogStore {
fn get_debug_adapter_entry(
log_lines: &mut VecDeque,
- id: SessionId,
+ id: LogStoreEntryIdentifier<'static>,
message: SharedString,
kind: LogKind,
cx: &mut Context,
) -> SharedString {
- while log_lines.len() >= RpcMessages::MESSAGE_QUEUE_LIMIT {
- log_lines.pop_front();
+ if let Some(excess) = log_lines
+ .len()
+ .checked_sub(RpcMessages::MESSAGE_QUEUE_LIMIT)
+ && excess > 0
+ {
+ log_lines.drain(..excess);
}
let format_messages = DebuggerSettings::get_global(cx).format_dap_log_messages;
@@ -322,118 +359,116 @@ impl LogStore {
fn add_debug_session(
&mut self,
- session_id: SessionId,
+ id: LogStoreEntryIdentifier<'static>,
session: Entity,
cx: &mut Context,
) {
- if self
- .debug_sessions
- .iter_mut()
- .any(|adapter_state| adapter_state.id == session_id)
- {
- return;
- }
-
- let (adapter_name, has_adapter_logs) = session.read_with(cx, |session, _| {
- (
- session.adapter(),
- session
- .adapter_client()
- .map(|client| client.has_adapter_logs())
- .unwrap_or(false),
- )
- });
-
- self.debug_sessions.push_back(DebugAdapterState::new(
- session_id,
- adapter_name,
- has_adapter_logs,
- ));
-
- self.clean_sessions(cx);
-
- let io_tx = self.rpc_tx.clone();
-
- let Some(client) = session.read(cx).adapter_client() else {
- return;
- };
+ maybe!({
+ let project_entry = self.projects.get_mut(&id.project)?;
+ let std::collections::btree_map::Entry::Vacant(state) =
+ project_entry.debug_sessions.entry(id.session_id)
+ else {
+ return None;
+ };
+
+ let (adapter_name, has_adapter_logs) = session.read_with(cx, |session, _| {
+ (
+ session.adapter(),
+ session
+ .adapter_client()
+ .map_or(false, |client| client.has_adapter_logs()),
+ )
+ });
- client.add_log_handler(
- move |io_kind, command, message| {
- io_tx
- .unbounded_send((
- session_id,
- io_kind,
- command.map(|command| command.to_owned().into()),
- message.to_owned().into(),
- ))
- .ok();
- },
- LogKind::Rpc,
- );
+ state.insert(DebugAdapterState::new(
+ id.session_id,
+ adapter_name,
+ has_adapter_logs,
+ ));
+
+ self.clean_sessions(cx);
+
+ let io_tx = self.rpc_tx.clone();
+
+ let client = session.read(cx).adapter_client()?;
+ let project = id.project.clone();
+ let session_id = id.session_id;
+ client.add_log_handler(
+ move |kind, command, message| {
+ io_tx
+ .unbounded_send(LogStoreMessage {
+ id: LogStoreEntryIdentifier {
+ session_id,
+ project: project.clone(),
+ },
+ kind,
+ command: command.map(|command| command.to_owned().into()),
+ message: message.to_owned().into(),
+ })
+ .ok();
+ },
+ LogKind::Rpc,
+ );
- let log_io_tx = self.adapter_log_tx.clone();
- client.add_log_handler(
- move |io_kind, command, message| {
- log_io_tx
- .unbounded_send((
- session_id,
- io_kind,
- command.map(|command| command.to_owned().into()),
- message.to_owned().into(),
- ))
- .ok();
- },
- LogKind::Adapter,
- );
+ let log_io_tx = self.adapter_log_tx.clone();
+ let project = id.project;
+ client.add_log_handler(
+ move |kind, command, message| {
+ log_io_tx
+ .unbounded_send(LogStoreMessage {
+ id: LogStoreEntryIdentifier {
+ session_id,
+ project: project.clone(),
+ },
+ kind,
+ command: command.map(|command| command.to_owned().into()),
+ message: message.to_owned().into(),
+ })
+ .ok();
+ },
+ LogKind::Adapter,
+ );
+ Some(())
+ });
}
fn clean_sessions(&mut self, cx: &mut Context) {
- let mut to_remove = self.debug_sessions.len().saturating_sub(MAX_SESSIONS);
- self.debug_sessions.retain(|session| {
- if to_remove > 0 && session.is_terminated {
- to_remove -= 1;
- return false;
- }
- true
+ self.projects.values_mut().for_each(|project| {
+ let mut allowed_terminated_sessions = 10u32;
+ project.debug_sessions.retain(|_, session| {
+ if !session.is_terminated {
+ return true;
+ }
+ allowed_terminated_sessions = allowed_terminated_sessions.saturating_sub(1);
+ allowed_terminated_sessions > 0
+ });
});
+
cx.notify();
}
fn log_messages_for_session(
&mut self,
- session_id: SessionId,
+ id: &LogStoreEntryIdentifier<'_>,
) -> Option<&mut VecDeque> {
- self.debug_sessions
- .iter_mut()
- .find(|session| session.id == session_id)
+ self.get_debug_adapter_state(id)
.map(|state| &mut state.log_messages)
}
fn rpc_messages_for_session(
&mut self,
- session_id: SessionId,
+ id: &LogStoreEntryIdentifier<'_>,
) -> Option<&mut VecDeque> {
- self.debug_sessions.iter_mut().find_map(|state| {
- if state.id == session_id {
- Some(&mut state.rpc_messages.messages)
- } else {
- None
- }
- })
+ self.get_debug_adapter_state(id)
+ .map(|state| &mut state.rpc_messages.messages)
}
fn initialization_sequence_for_session(
&mut self,
- session_id: SessionId,
- ) -> Option<&mut Vec> {
- self.debug_sessions.iter_mut().find_map(|state| {
- if state.id == session_id {
- Some(&mut state.rpc_messages.initialization_sequence)
- } else {
- None
- }
- })
+ id: &LogStoreEntryIdentifier<'_>,
+ ) -> Option<&Vec> {
+ self.get_debug_adapter_state(&id)
+ .map(|state| &state.rpc_messages.initialization_sequence)
}
}
@@ -453,10 +488,11 @@ impl Render for DapLogToolbarItemView {
return Empty.into_any_element();
};
- let (menu_rows, current_session_id) = log_view.update(cx, |log_view, cx| {
+ let (menu_rows, current_session_id, project) = log_view.update(cx, |log_view, cx| {
(
log_view.menu_items(cx),
log_view.current_view.map(|(session_id, _)| session_id),
+ log_view.project.downgrade(),
)
});
@@ -484,6 +520,7 @@ impl Render for DapLogToolbarItemView {
.menu(move |mut window, cx| {
let log_view = log_view.clone();
let menu_rows = menu_rows.clone();
+ let project = project.clone();
ContextMenu::build(&mut window, cx, move |mut menu, window, _cx| {
for row in menu_rows.into_iter() {
menu = menu.custom_row(move |_window, _cx| {
@@ -509,8 +546,15 @@ impl Render for DapLogToolbarItemView {
.child(Label::new(ADAPTER_LOGS))
.into_any_element()
},
- window.handler_for(&log_view, move |view, window, cx| {
- view.show_log_messages_for_adapter(row.session_id, window, cx);
+ window.handler_for(&log_view, {
+ let project = project.clone();
+ let id = LogStoreEntryIdentifier {
+ project: Cow::Owned(project),
+ session_id: row.session_id,
+ };
+ move |view, window, cx| {
+ view.show_log_messages_for_adapter(&id, window, cx);
+ }
}),
);
}
@@ -524,8 +568,15 @@ impl Render for DapLogToolbarItemView {
.child(Label::new(RPC_MESSAGES))
.into_any_element()
},
- window.handler_for(&log_view, move |view, window, cx| {
- view.show_rpc_trace_for_server(row.session_id, window, cx);
+ window.handler_for(&log_view, {
+ let project = project.clone();
+ let id = LogStoreEntryIdentifier {
+ project: Cow::Owned(project),
+ session_id: row.session_id,
+ };
+ move |view, window, cx| {
+ view.show_rpc_trace_for_server(&id, window, cx);
+ }
}),
)
.custom_entry(
@@ -536,12 +587,17 @@ impl Render for DapLogToolbarItemView {
.child(Label::new(INITIALIZATION_SEQUENCE))
.into_any_element()
},
- window.handler_for(&log_view, move |view, window, cx| {
- view.show_initialization_sequence_for_server(
- row.session_id,
- window,
- cx,
- );
+ window.handler_for(&log_view, {
+ let project = project.clone();
+ let id = LogStoreEntryIdentifier {
+ project: Cow::Owned(project),
+ session_id: row.session_id,
+ };
+ move |view, window, cx| {
+ view.show_initialization_sequence_for_server(
+ &id, window, cx,
+ );
+ }
}),
);
}
@@ -613,7 +669,9 @@ impl DapLogView {
let events_subscriptions = cx.subscribe(&log_store, |log_view, _, event, cx| match event {
Event::NewLogEntry { id, entry, kind } => {
- if log_view.current_view == Some((*id, *kind)) {
+ if log_view.current_view == Some((id.session_id, *kind))
+ && log_view.project == *id.project
+ {
log_view.editor.update(cx, |editor, cx| {
editor.set_read_only(false);
let last_point = editor.buffer().read(cx).len(cx);
@@ -629,12 +687,18 @@ impl DapLogView {
}
}
});
-
+ let weak_project = project.downgrade();
let state_info = log_store
.read(cx)
- .debug_sessions
- .back()
- .map(|session| (session.id, session.has_adapter_logs));
+ .projects
+ .get(&weak_project)
+ .and_then(|project| {
+ project
+ .debug_sessions
+ .values()
+ .next_back()
+ .map(|session| (session.id, session.has_adapter_logs))
+ });
let mut this = Self {
editor,
@@ -647,10 +711,14 @@ impl DapLogView {
};
if let Some((session_id, have_adapter_logs)) = state_info {
+ let id = LogStoreEntryIdentifier {
+ session_id,
+ project: Cow::Owned(weak_project),
+ };
if have_adapter_logs {
- this.show_log_messages_for_adapter(session_id, window, cx);
+ this.show_log_messages_for_adapter(&id, window, cx);
} else {
- this.show_rpc_trace_for_server(session_id, window, cx);
+ this.show_rpc_trace_for_server(&id, window, cx);
}
}
@@ -690,31 +758,38 @@ impl DapLogView {
fn menu_items(&self, cx: &App) -> Vec {
self.log_store
.read(cx)
- .debug_sessions
- .iter()
- .rev()
- .map(|state| DapMenuItem {
- session_id: state.id,
- adapter_name: state.adapter_name.clone(),
- has_adapter_logs: state.has_adapter_logs,
- selected_entry: self.current_view.map_or(LogKind::Adapter, |(_, kind)| kind),
+ .projects
+ .get(&self.project.downgrade())
+ .map_or_else(Vec::new, |state| {
+ state
+ .debug_sessions
+ .values()
+ .rev()
+ .map(|state| DapMenuItem {
+ session_id: state.id,
+ adapter_name: state.adapter_name.clone(),
+ has_adapter_logs: state.has_adapter_logs,
+ selected_entry: self
+ .current_view
+ .map_or(LogKind::Adapter, |(_, kind)| kind),
+ })
+ .collect::>()
})
- .collect::>()
}
fn show_rpc_trace_for_server(
&mut self,
- session_id: SessionId,
+ id: &LogStoreEntryIdentifier<'_>,
window: &mut Window,
cx: &mut Context,
) {
let rpc_log = self.log_store.update(cx, |log_store, _| {
log_store
- .rpc_messages_for_session(session_id)
+ .rpc_messages_for_session(id)
.map(|state| log_contents(state.iter().cloned()))
});
if let Some(rpc_log) = rpc_log {
- self.current_view = Some((session_id, LogKind::Rpc));
+ self.current_view = Some((id.session_id, LogKind::Rpc));
let (editor, editor_subscriptions) = Self::editor_for_logs(rpc_log, window, cx);
let language = self.project.read(cx).languages().language_for_name("JSON");
editor
@@ -725,8 +800,7 @@ impl DapLogView {
.expect("log buffer should be a singleton")
.update(cx, |_, cx| {
cx.spawn({
- let buffer = cx.entity();
- async move |_, cx| {
+ async move |buffer, cx| {
let language = language.await.ok();
buffer.update(cx, |buffer, cx| {
buffer.set_language(language, cx);
@@ -746,17 +820,17 @@ impl DapLogView {
fn show_log_messages_for_adapter(
&mut self,
- session_id: SessionId,
+ id: &LogStoreEntryIdentifier<'_>,
window: &mut Window,
cx: &mut Context,
) {
let message_log = self.log_store.update(cx, |log_store, _| {
log_store
- .log_messages_for_session(session_id)
+ .log_messages_for_session(id)
.map(|state| log_contents(state.iter().cloned()))
});
if let Some(message_log) = message_log {
- self.current_view = Some((session_id, LogKind::Adapter));
+ self.current_view = Some((id.session_id, LogKind::Adapter));
let (editor, editor_subscriptions) = Self::editor_for_logs(message_log, window, cx);
editor
.read(cx)
@@ -775,17 +849,17 @@ impl DapLogView {
fn show_initialization_sequence_for_server(
&mut self,
- session_id: SessionId,
+ id: &LogStoreEntryIdentifier<'_>,
window: &mut Window,
cx: &mut Context,
) {
let rpc_log = self.log_store.update(cx, |log_store, _| {
log_store
- .initialization_sequence_for_session(session_id)
+ .initialization_sequence_for_session(id)
.map(|state| log_contents(state.iter().cloned()))
});
if let Some(rpc_log) = rpc_log {
- self.current_view = Some((session_id, LogKind::Rpc));
+ self.current_view = Some((id.session_id, LogKind::Rpc));
let (editor, editor_subscriptions) = Self::editor_for_logs(rpc_log, window, cx);
let language = self.project.read(cx).languages().language_for_name("JSON");
editor
@@ -993,9 +1067,9 @@ impl Focusable for DapLogView {
}
}
-pub enum Event {
+enum Event {
NewLogEntry {
- id: SessionId,
+ id: LogStoreEntryIdentifier<'static>,
entry: SharedString,
kind: LogKind,
},
@@ -1008,31 +1082,30 @@ impl EventEmitter for DapLogView {}
#[cfg(any(test, feature = "test-support"))]
impl LogStore {
- pub fn contained_session_ids(&self) -> Vec {
- self.debug_sessions
- .iter()
- .map(|session| session.id)
- .collect()
+ pub fn has_projects(&self) -> bool {
+ !self.projects.is_empty()
}
- pub fn rpc_messages_for_session_id(&self, session_id: SessionId) -> Vec {
- self.debug_sessions
- .iter()
- .find(|adapter_state| adapter_state.id == session_id)
- .expect("This session should exist if a test is calling")
- .rpc_messages
- .messages
- .clone()
- .into()
+ pub fn contained_session_ids(&self, project: &WeakEntity) -> Vec {
+ self.projects.get(project).map_or(vec![], |state| {
+ state.debug_sessions.keys().copied().collect()
+ })
}
- pub fn log_messages_for_session_id(&self, session_id: SessionId) -> Vec {
- self.debug_sessions
- .iter()
- .find(|adapter_state| adapter_state.id == session_id)
- .expect("This session should exist if a test is calling")
- .log_messages
- .clone()
- .into()
+ pub fn rpc_messages_for_session_id(
+ &self,
+ project: &WeakEntity,
+ session_id: SessionId,
+ ) -> Vec {
+ self.projects.get(&project).map_or(vec![], |state| {
+ state
+ .debug_sessions
+ .get(&session_id)
+ .expect("This session should exist if a test is calling")
+ .rpc_messages
+ .messages
+ .clone()
+ .into()
+ })
}
}
diff --git a/crates/debugger_ui/Cargo.toml b/crates/debugger_ui/Cargo.toml
index 91f9acad3c73334980036880143df9c7b410b3b6..ba71e50a0830c7fbab60aa75ba14bb63d58bac07 100644
--- a/crates/debugger_ui/Cargo.toml
+++ b/crates/debugger_ui/Cargo.toml
@@ -28,6 +28,7 @@ test-support = [
[dependencies]
alacritty_terminal.workspace = true
anyhow.workspace = true
+bitflags.workspace = true
client.workspace = true
collections.workspace = true
command_palette_hooks.workspace = true
diff --git a/crates/debugger_ui/src/debugger_panel.rs b/crates/debugger_ui/src/debugger_panel.rs
index b7f3be0426e9c189eb0edf203859c7d2489c75d9..795b4caf9e43a28c8bf115755332fa9976d89d93 100644
--- a/crates/debugger_ui/src/debugger_panel.rs
+++ b/crates/debugger_ui/src/debugger_panel.rs
@@ -100,7 +100,13 @@ impl DebugPanel {
sessions: vec![],
active_session: None,
focus_handle,
- breakpoint_list: BreakpointList::new(None, workspace.weak_handle(), &project, cx),
+ breakpoint_list: BreakpointList::new(
+ None,
+ workspace.weak_handle(),
+ &project,
+ window,
+ cx,
+ ),
project,
workspace: workspace.weak_handle(),
context_menu: None,
@@ -862,7 +868,7 @@ impl DebugPanel {
let threads =
running_state.update(cx, |running_state, cx| {
let session = running_state.session();
- session.read(cx).is_running().then(|| {
+ session.read(cx).is_started().then(|| {
session.update(cx, |session, cx| {
session.threads(cx)
})
@@ -1292,6 +1298,11 @@ impl Render for DebugPanel {
}
v_flex()
+ .when_else(
+ self.position(window, cx) == DockPosition::Bottom,
+ |this| this.max_h(self.size),
+ |this| this.max_w(self.size),
+ )
.size_full()
.key_context("DebugPanel")
.child(h_flex().children(self.top_controls_strip(window, cx)))
@@ -1462,6 +1473,94 @@ impl Render for DebugPanel {
if has_sessions {
this.children(self.active_session.clone())
} else {
+ let docked_to_bottom = self.position(window, cx) == DockPosition::Bottom;
+ let welcome_experience = v_flex()
+ .when_else(
+ docked_to_bottom,
+ |this| this.w_2_3().h_full().pr_8(),
+ |this| this.w_full().h_1_3(),
+ )
+ .items_center()
+ .justify_center()
+ .gap_2()
+ .child(
+ Button::new("spawn-new-session-empty-state", "New Session")
+ .icon(IconName::Plus)
+ .icon_size(IconSize::XSmall)
+ .icon_color(Color::Muted)
+ .icon_position(IconPosition::Start)
+ .on_click(|_, window, cx| {
+ window.dispatch_action(crate::Start.boxed_clone(), cx);
+ }),
+ )
+ .child(
+ Button::new("edit-debug-settings", "Edit debug.json")
+ .icon(IconName::Code)
+ .icon_size(IconSize::XSmall)
+ .color(Color::Muted)
+ .icon_color(Color::Muted)
+ .icon_position(IconPosition::Start)
+ .on_click(|_, window, cx| {
+ window.dispatch_action(
+ zed_actions::OpenProjectDebugTasks.boxed_clone(),
+ cx,
+ );
+ }),
+ )
+ .child(
+ Button::new("open-debugger-docs", "Debugger Docs")
+ .icon(IconName::Book)
+ .color(Color::Muted)
+ .icon_size(IconSize::XSmall)
+ .icon_color(Color::Muted)
+ .icon_position(IconPosition::Start)
+ .on_click(|_, _, cx| cx.open_url("https://zed.dev/docs/debugger")),
+ )
+ .child(
+ Button::new(
+ "spawn-new-session-install-extensions",
+ "Debugger Extensions",
+ )
+ .icon(IconName::Blocks)
+ .color(Color::Muted)
+ .icon_size(IconSize::XSmall)
+ .icon_color(Color::Muted)
+ .icon_position(IconPosition::Start)
+ .on_click(|_, window, cx| {
+ window.dispatch_action(
+ zed_actions::Extensions {
+ category_filter: Some(
+ zed_actions::ExtensionCategoryFilter::DebugAdapters,
+ ),
+ }
+ .boxed_clone(),
+ cx,
+ );
+ }),
+ );
+ let breakpoint_list =
+ v_flex()
+ .group("base-breakpoint-list")
+ .items_start()
+ .when_else(
+ docked_to_bottom,
+ |this| this.min_w_1_3().h_full(),
+ |this| this.w_full().h_2_3(),
+ )
+ .p_1()
+ .child(
+ h_flex()
+ .pl_1()
+ .w_full()
+ .justify_between()
+ .child(Label::new("Breakpoints").size(LabelSize::Small))
+ .child(h_flex().visible_on_hover("base-breakpoint-list").child(
+ self.breakpoint_list.read(cx).render_control_strip(),
+ ))
+ .track_focus(&self.breakpoint_list.focus_handle(cx)),
+ )
+ .child(Divider::horizontal())
+ .child(self.breakpoint_list.clone());
this.child(
v_flex()
.h_full()
@@ -1469,65 +1568,23 @@ impl Render for DebugPanel {
.items_center()
.justify_center()
.child(
- h_flex().size_full()
- .items_start()
-
- .child(v_flex().group("base-breakpoint-list").items_start().min_w_1_3().h_full().p_1()
- .child(h_flex().pl_1().w_full().justify_between()
- .child(Label::new("Breakpoints").size(LabelSize::Small))
- .child(h_flex().visible_on_hover("base-breakpoint-list").child(self.breakpoint_list.read(cx).render_control_strip())))
- .child(Divider::horizontal())
- .child(self.breakpoint_list.clone()))
- .child(Divider::vertical())
- .child(
- v_flex().w_2_3().h_full().items_center().justify_center()
- .gap_2()
- .pr_8()
- .child(
- Button::new("spawn-new-session-empty-state", "New Session")
- .icon(IconName::Plus)
- .icon_size(IconSize::XSmall)
- .icon_color(Color::Muted)
- .icon_position(IconPosition::Start)
- .on_click(|_, window, cx| {
- window.dispatch_action(crate::Start.boxed_clone(), cx);
- })
- )
- .child(
- Button::new("edit-debug-settings", "Edit debug.json")
- .icon(IconName::Code)
- .icon_size(IconSize::XSmall)
- .color(Color::Muted)
- .icon_color(Color::Muted)
- .icon_position(IconPosition::Start)
- .on_click(|_, window, cx| {
- window.dispatch_action(zed_actions::OpenProjectDebugTasks.boxed_clone(), cx);
- })
- )
- .child(
- Button::new("open-debugger-docs", "Debugger Docs")
- .icon(IconName::Book)
- .color(Color::Muted)
- .icon_size(IconSize::XSmall)
- .icon_color(Color::Muted)
- .icon_position(IconPosition::Start)
- .on_click(|_, _, cx| {
- cx.open_url("https://zed.dev/docs/debugger")
- })
- )
- .child(
- Button::new("spawn-new-session-install-extensions", "Debugger Extensions")
- .icon(IconName::Blocks)
- .color(Color::Muted)
- .icon_size(IconSize::XSmall)
- .icon_color(Color::Muted)
- .icon_position(IconPosition::Start)
- .on_click(|_, window, cx| {
- window.dispatch_action(zed_actions::Extensions { category_filter: Some(zed_actions::ExtensionCategoryFilter::DebugAdapters)}.boxed_clone(), cx);
- })
- )
- )
- )
+ div()
+ .when_else(docked_to_bottom, Div::h_flex, Div::v_flex)
+ .size_full()
+ .map(|this| {
+ if docked_to_bottom {
+ this.items_start()
+ .child(breakpoint_list)
+ .child(Divider::vertical())
+ .child(welcome_experience)
+ } else {
+ this.items_end()
+ .child(welcome_experience)
+ .child(Divider::horizontal())
+ .child(breakpoint_list)
+ }
+ }),
+ ),
)
}
})
diff --git a/crates/debugger_ui/src/session/running.rs b/crates/debugger_ui/src/session/running.rs
index 6a3535fe0ebc43eb49066f0e3a81887c10ad51bc..91e49059e92609d2639d043220fa042cc70b708b 100644
--- a/crates/debugger_ui/src/session/running.rs
+++ b/crates/debugger_ui/src/session/running.rs
@@ -697,8 +697,13 @@ impl RunningState {
)
});
- let breakpoint_list =
- BreakpointList::new(Some(session.clone()), workspace.clone(), &project, cx);
+ let breakpoint_list = BreakpointList::new(
+ Some(session.clone()),
+ workspace.clone(),
+ &project,
+ window,
+ cx,
+ );
let _subscriptions = vec![
cx.on_app_quit(move |this, cx| {
@@ -895,7 +900,7 @@ impl RunningState {
let config_is_valid = request_type.is_ok();
-
+ let mut extra_config = Value::Null;
let build_output = if let Some(build) = build {
let (task_template, locator_name) = match build {
BuildTaskDefinition::Template {
@@ -925,6 +930,7 @@ impl RunningState {
};
let locator_name = if let Some(locator_name) = locator_name {
+ extra_config = config.clone();
debug_assert!(!config_is_valid);
Some(locator_name)
} else if !config_is_valid {
@@ -940,6 +946,7 @@ impl RunningState {
});
if let Ok(t) = task {
t.await.and_then(|scenario| {
+ extra_config = scenario.config;
match scenario.build {
Some(BuildTaskDefinition::Template {
locator_name, ..
@@ -1003,13 +1010,13 @@ impl RunningState {
if !exit_status.success() {
anyhow::bail!("Build failed");
}
- Some((task.resolved.clone(), locator_name))
+ Some((task.resolved.clone(), locator_name, extra_config))
} else {
None
};
if config_is_valid {
- } else if let Some((task, locator_name)) = build_output {
+ } else if let Some((task, locator_name, extra_config)) = build_output {
let locator_name =
locator_name.with_context(|| {
format!("Could not find a valid locator for a build task and configure is invalid with error: {}", request_type.err()
@@ -1034,6 +1041,8 @@ impl RunningState {
.with_context(|| anyhow!("{}: is not a valid adapter name", &adapter))?.config_from_zed_format(zed_config)
.await?;
config = scenario.config;
+ util::merge_non_null_json_value_into(extra_config, &mut config);
+
Self::substitute_variables_in_config(&mut config, &task_context);
} else {
let Err(e) = request_type else {
diff --git a/crates/debugger_ui/src/session/running/breakpoint_list.rs b/crates/debugger_ui/src/session/running/breakpoint_list.rs
index 8077b289a7d111cbbd1ed189206904d753ae412c..5576435a0875ae298a7a7f5fb9d509a6a7ea16f1 100644
--- a/crates/debugger_ui/src/session/running/breakpoint_list.rs
+++ b/crates/debugger_ui/src/session/running/breakpoint_list.rs
@@ -5,11 +5,11 @@ use std::{
time::Duration,
};
-use dap::ExceptionBreakpointsFilter;
+use dap::{Capabilities, ExceptionBreakpointsFilter};
use editor::Editor;
use gpui::{
- Action, AppContext, Entity, FocusHandle, Focusable, MouseButton, ScrollStrategy, Stateful,
- Task, UniformListScrollHandle, WeakEntity, uniform_list,
+ Action, AppContext, ClickEvent, Entity, FocusHandle, Focusable, MouseButton, ScrollStrategy,
+ Stateful, Task, UniformListScrollHandle, WeakEntity, actions, uniform_list,
};
use language::Point;
use project::{
@@ -21,16 +21,20 @@ use project::{
worktree_store::WorktreeStore,
};
use ui::{
- AnyElement, App, ButtonCommon, Clickable, Color, Context, Disableable, Div, FluentBuilder as _,
- Icon, IconButton, IconName, IconSize, Indicator, InteractiveElement, IntoElement, Label,
- LabelCommon, LabelSize, ListItem, ParentElement, Render, Scrollbar, ScrollbarState,
- SharedString, StatefulInteractiveElement, Styled, Toggleable, Tooltip, Window, div, h_flex, px,
- v_flex,
+ ActiveTheme, AnyElement, App, ButtonCommon, Clickable, Color, Context, Disableable, Div,
+ Divider, FluentBuilder as _, Icon, IconButton, IconName, IconSize, Indicator,
+ InteractiveElement, IntoElement, Label, LabelCommon, LabelSize, ListItem, ParentElement,
+ Render, RenderOnce, Scrollbar, ScrollbarState, SharedString, StatefulInteractiveElement,
+ Styled, Toggleable, Tooltip, Window, div, h_flex, px, v_flex,
};
use util::ResultExt;
use workspace::Workspace;
use zed_actions::{ToggleEnableBreakpoint, UnsetBreakpoint};
+actions!(
+ debugger,
+ [PreviousBreakpointProperty, NextBreakpointProperty]
+);
#[derive(Clone, Copy, PartialEq)]
pub(crate) enum SelectedBreakpointKind {
Source,
@@ -48,6 +52,8 @@ pub(crate) struct BreakpointList {
focus_handle: FocusHandle,
scroll_handle: UniformListScrollHandle,
selected_ix: Option,
+ input: Entity,
+ strip_mode: Option,
}
impl Focusable for BreakpointList {
@@ -56,11 +62,19 @@ impl Focusable for BreakpointList {
}
}
+#[derive(Clone, Copy, PartialEq)]
+enum ActiveBreakpointStripMode {
+ Log,
+ Condition,
+ HitCondition,
+}
+
impl BreakpointList {
pub(crate) fn new(
session: Option>,
workspace: WeakEntity,
project: &Entity,
+ window: &mut Window,
cx: &mut App,
) -> Entity {
let project = project.read(cx);
@@ -70,7 +84,7 @@ impl BreakpointList {
let scroll_handle = UniformListScrollHandle::new();
let scrollbar_state = ScrollbarState::new(scroll_handle.clone());
- cx.new(|_| Self {
+ cx.new(|cx| Self {
breakpoint_store,
worktree_store,
scrollbar_state,
@@ -82,17 +96,28 @@ impl BreakpointList {
focus_handle,
scroll_handle,
selected_ix: None,
+ input: cx.new(|cx| Editor::single_line(window, cx)),
+ strip_mode: None,
})
}
fn edit_line_breakpoint(
- &mut self,
+ &self,
path: Arc,
row: u32,
action: BreakpointEditAction,
- cx: &mut Context,
+ cx: &mut App,
+ ) {
+ Self::edit_line_breakpoint_inner(&self.breakpoint_store, path, row, action, cx);
+ }
+ fn edit_line_breakpoint_inner(
+ breakpoint_store: &Entity,
+ path: Arc,
+ row: u32,
+ action: BreakpointEditAction,
+ cx: &mut App,
) {
- self.breakpoint_store.update(cx, |breakpoint_store, cx| {
+ breakpoint_store.update(cx, |breakpoint_store, cx| {
if let Some((buffer, breakpoint)) = breakpoint_store.breakpoint_at_row(&path, row, cx) {
breakpoint_store.toggle_breakpoint(buffer, breakpoint, action, cx);
} else {
@@ -148,16 +173,63 @@ impl BreakpointList {
})
}
- fn select_ix(&mut self, ix: Option, cx: &mut Context) {
+ fn set_active_breakpoint_property(
+ &mut self,
+ prop: ActiveBreakpointStripMode,
+ window: &mut Window,
+ cx: &mut App,
+ ) {
+ self.strip_mode = Some(prop);
+ let placeholder = match prop {
+ ActiveBreakpointStripMode::Log => "Set Log Message",
+ ActiveBreakpointStripMode::Condition => "Set Condition",
+ ActiveBreakpointStripMode::HitCondition => "Set Hit Condition",
+ };
+ let mut is_exception_breakpoint = true;
+ let active_value = self.selected_ix.and_then(|ix| {
+ self.breakpoints.get(ix).and_then(|bp| {
+ if let BreakpointEntryKind::LineBreakpoint(bp) = &bp.kind {
+ is_exception_breakpoint = false;
+ match prop {
+ ActiveBreakpointStripMode::Log => bp.breakpoint.message.clone(),
+ ActiveBreakpointStripMode::Condition => bp.breakpoint.condition.clone(),
+ ActiveBreakpointStripMode::HitCondition => {
+ bp.breakpoint.hit_condition.clone()
+ }
+ }
+ } else {
+ None
+ }
+ })
+ });
+
+ self.input.update(cx, |this, cx| {
+ this.set_placeholder_text(placeholder, cx);
+ this.set_read_only(is_exception_breakpoint);
+ this.set_text(active_value.as_deref().unwrap_or(""), window, cx);
+ });
+ }
+
+ fn select_ix(&mut self, ix: Option, window: &mut Window, cx: &mut Context) {
self.selected_ix = ix;
if let Some(ix) = ix {
self.scroll_handle
.scroll_to_item(ix, ScrollStrategy::Center);
}
+ if let Some(mode) = self.strip_mode {
+ self.set_active_breakpoint_property(mode, window, cx);
+ }
+
cx.notify();
}
- fn select_next(&mut self, _: &menu::SelectNext, _window: &mut Window, cx: &mut Context) {
+ fn select_next(&mut self, _: &menu::SelectNext, window: &mut Window, cx: &mut Context) {
+ if self.strip_mode.is_some() {
+ if self.input.focus_handle(cx).contains_focused(window, cx) {
+ cx.propagate();
+ return;
+ }
+ }
let ix = match self.selected_ix {
_ if self.breakpoints.len() == 0 => None,
None => Some(0),
@@ -169,15 +241,21 @@ impl BreakpointList {
}
}
};
- self.select_ix(ix, cx);
+ self.select_ix(ix, window, cx);
}
fn select_previous(
&mut self,
_: &menu::SelectPrevious,
- _window: &mut Window,
+ window: &mut Window,
cx: &mut Context,
) {
+ if self.strip_mode.is_some() {
+ if self.input.focus_handle(cx).contains_focused(window, cx) {
+ cx.propagate();
+ return;
+ }
+ }
let ix = match self.selected_ix {
_ if self.breakpoints.len() == 0 => None,
None => Some(self.breakpoints.len() - 1),
@@ -189,37 +267,105 @@ impl BreakpointList {
}
}
};
- self.select_ix(ix, cx);
+ self.select_ix(ix, window, cx);
}
- fn select_first(
- &mut self,
- _: &menu::SelectFirst,
- _window: &mut Window,
- cx: &mut Context,
- ) {
+ fn select_first(&mut self, _: &menu::SelectFirst, window: &mut Window, cx: &mut Context) {
+ if self.strip_mode.is_some() {
+ if self.input.focus_handle(cx).contains_focused(window, cx) {
+ cx.propagate();
+ return;
+ }
+ }
let ix = if self.breakpoints.len() > 0 {
Some(0)
} else {
None
};
- self.select_ix(ix, cx);
+ self.select_ix(ix, window, cx);
}
- fn select_last(&mut self, _: &menu::SelectLast, _window: &mut Window, cx: &mut Context) {
+ fn select_last(&mut self, _: &menu::SelectLast, window: &mut Window, cx: &mut Context) {
+ if self.strip_mode.is_some() {
+ if self.input.focus_handle(cx).contains_focused(window, cx) {
+ cx.propagate();
+ return;
+ }
+ }
let ix = if self.breakpoints.len() > 0 {
Some(self.breakpoints.len() - 1)
} else {
None
};
- self.select_ix(ix, cx);
+ self.select_ix(ix, window, cx);
}
+ fn dismiss(&mut self, _: &menu::Cancel, window: &mut Window, cx: &mut Context) {
+ if self.input.focus_handle(cx).contains_focused(window, cx) {
+ self.focus_handle.focus(window);
+ } else if self.strip_mode.is_some() {
+ self.strip_mode.take();
+ cx.notify();
+ } else {
+ cx.propagate();
+ }
+ }
fn confirm(&mut self, _: &menu::Confirm, window: &mut Window, cx: &mut Context) {
let Some(entry) = self.selected_ix.and_then(|ix| self.breakpoints.get_mut(ix)) else {
return;
};
+ if let Some(mode) = self.strip_mode {
+ let handle = self.input.focus_handle(cx);
+ if handle.is_focused(window) {
+ // Go back to the main strip. Save the result as well.
+ let text = self.input.read(cx).text(cx);
+
+ match mode {
+ ActiveBreakpointStripMode::Log => match &entry.kind {
+ BreakpointEntryKind::LineBreakpoint(line_breakpoint) => {
+ Self::edit_line_breakpoint_inner(
+ &self.breakpoint_store,
+ line_breakpoint.breakpoint.path.clone(),
+ line_breakpoint.breakpoint.row,
+ BreakpointEditAction::EditLogMessage(Arc::from(text)),
+ cx,
+ );
+ }
+ _ => {}
+ },
+ ActiveBreakpointStripMode::Condition => match &entry.kind {
+ BreakpointEntryKind::LineBreakpoint(line_breakpoint) => {
+ Self::edit_line_breakpoint_inner(
+ &self.breakpoint_store,
+ line_breakpoint.breakpoint.path.clone(),
+ line_breakpoint.breakpoint.row,
+ BreakpointEditAction::EditCondition(Arc::from(text)),
+ cx,
+ );
+ }
+ _ => {}
+ },
+ ActiveBreakpointStripMode::HitCondition => match &entry.kind {
+ BreakpointEntryKind::LineBreakpoint(line_breakpoint) => {
+ Self::edit_line_breakpoint_inner(
+ &self.breakpoint_store,
+ line_breakpoint.breakpoint.path.clone(),
+ line_breakpoint.breakpoint.row,
+ BreakpointEditAction::EditHitCondition(Arc::from(text)),
+ cx,
+ );
+ }
+ _ => {}
+ },
+ }
+ self.focus_handle.focus(window);
+ } else {
+ handle.focus(window);
+ }
+
+ return;
+ }
match &mut entry.kind {
BreakpointEntryKind::LineBreakpoint(line_breakpoint) => {
let path = line_breakpoint.breakpoint.path.clone();
@@ -233,12 +379,18 @@ impl BreakpointList {
fn toggle_enable_breakpoint(
&mut self,
_: &ToggleEnableBreakpoint,
- _window: &mut Window,
+ window: &mut Window,
cx: &mut Context,
) {
let Some(entry) = self.selected_ix.and_then(|ix| self.breakpoints.get_mut(ix)) else {
return;
};
+ if self.strip_mode.is_some() {
+ if self.input.focus_handle(cx).contains_focused(window, cx) {
+ cx.propagate();
+ return;
+ }
+ }
match &mut entry.kind {
BreakpointEntryKind::LineBreakpoint(line_breakpoint) => {
@@ -279,6 +431,50 @@ impl BreakpointList {
cx.notify();
}
+ fn previous_breakpoint_property(
+ &mut self,
+ _: &PreviousBreakpointProperty,
+ window: &mut Window,
+ cx: &mut Context,
+ ) {
+ let next_mode = match self.strip_mode {
+ Some(ActiveBreakpointStripMode::Log) => None,
+ Some(ActiveBreakpointStripMode::Condition) => Some(ActiveBreakpointStripMode::Log),
+ Some(ActiveBreakpointStripMode::HitCondition) => {
+ Some(ActiveBreakpointStripMode::Condition)
+ }
+ None => Some(ActiveBreakpointStripMode::HitCondition),
+ };
+ if let Some(mode) = next_mode {
+ self.set_active_breakpoint_property(mode, window, cx);
+ } else {
+ self.strip_mode.take();
+ }
+
+ cx.notify();
+ }
+ fn next_breakpoint_property(
+ &mut self,
+ _: &NextBreakpointProperty,
+ window: &mut Window,
+ cx: &mut Context,
+ ) {
+ let next_mode = match self.strip_mode {
+ Some(ActiveBreakpointStripMode::Log) => Some(ActiveBreakpointStripMode::Condition),
+ Some(ActiveBreakpointStripMode::Condition) => {
+ Some(ActiveBreakpointStripMode::HitCondition)
+ }
+ Some(ActiveBreakpointStripMode::HitCondition) => None,
+ None => Some(ActiveBreakpointStripMode::Log),
+ };
+ if let Some(mode) = next_mode {
+ self.set_active_breakpoint_property(mode, window, cx);
+ } else {
+ self.strip_mode.take();
+ }
+ cx.notify();
+ }
+
fn hide_scrollbar(&mut self, window: &mut Window, cx: &mut Context) {
const SCROLLBAR_SHOW_INTERVAL: Duration = Duration::from_secs(1);
self.hide_scrollbar_task = Some(cx.spawn_in(window, async move |panel, cx| {
@@ -294,20 +490,31 @@ impl BreakpointList {
}))
}
- fn render_list(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement {
+ fn render_list(&mut self, cx: &mut Context) -> impl IntoElement {
let selected_ix = self.selected_ix;
let focus_handle = self.focus_handle.clone();
+ let supported_breakpoint_properties = self
+ .session
+ .as_ref()
+ .map(|session| SupportedBreakpointProperties::from(session.read(cx).capabilities()))
+ .unwrap_or_else(SupportedBreakpointProperties::empty);
+ let strip_mode = self.strip_mode;
uniform_list(
"breakpoint-list",
self.breakpoints.len(),
- cx.processor(move |this, range: Range, window, cx| {
+ cx.processor(move |this, range: Range, _, _| {
range
.clone()
.zip(&mut this.breakpoints[range])
.map(|(ix, breakpoint)| {
breakpoint
- .render(ix, focus_handle.clone(), window, cx)
- .toggle_state(Some(ix) == selected_ix)
+ .render(
+ strip_mode,
+ supported_breakpoint_properties,
+ ix,
+ Some(ix) == selected_ix,
+ focus_handle.clone(),
+ )
.into_any_element()
})
.collect()
@@ -443,7 +650,6 @@ impl BreakpointList {
impl Render for BreakpointList {
fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl ui::IntoElement {
- // let old_len = self.breakpoints.len();
let breakpoints = self.breakpoint_store.read(cx).all_source_breakpoints(cx);
self.breakpoints.clear();
let weak = cx.weak_entity();
@@ -523,15 +729,46 @@ impl Render for BreakpointList {
.on_action(cx.listener(Self::select_previous))
.on_action(cx.listener(Self::select_first))
.on_action(cx.listener(Self::select_last))
+ .on_action(cx.listener(Self::dismiss))
.on_action(cx.listener(Self::confirm))
.on_action(cx.listener(Self::toggle_enable_breakpoint))
.on_action(cx.listener(Self::unset_breakpoint))
+ .on_action(cx.listener(Self::next_breakpoint_property))
+ .on_action(cx.listener(Self::previous_breakpoint_property))
.size_full()
.m_0p5()
- .child(self.render_list(window, cx))
- .children(self.render_vertical_scrollbar(cx))
+ .child(
+ v_flex()
+ .size_full()
+ .child(self.render_list(cx))
+ .children(self.render_vertical_scrollbar(cx)),
+ )
+ .when_some(self.strip_mode, |this, _| {
+ this.child(Divider::horizontal()).child(
+ h_flex()
+ // .w_full()
+ .m_0p5()
+ .p_0p5()
+ .border_1()
+ .rounded_sm()
+ .when(
+ self.input.focus_handle(cx).contains_focused(window, cx),
+ |this| {
+ let colors = cx.theme().colors();
+ let border = if self.input.read(cx).read_only(cx) {
+ colors.border_disabled
+ } else {
+ colors.border_focused
+ };
+ this.border_color(border)
+ },
+ )
+ .child(self.input.clone()),
+ )
+ })
}
}
+
#[derive(Clone, Debug)]
struct LineBreakpoint {
name: SharedString,
@@ -543,7 +780,10 @@ struct LineBreakpoint {
impl LineBreakpoint {
fn render(
&mut self,
+ props: SupportedBreakpointProperties,
+ strip_mode: Option,
ix: usize,
+ is_selected: bool,
focus_handle: FocusHandle,
weak: WeakEntity,
) -> ListItem {
@@ -594,15 +834,16 @@ impl LineBreakpoint {
})
.child(Indicator::icon(Icon::new(icon_name)).color(Color::Debugger))
.on_mouse_down(MouseButton::Left, move |_, _, _| {});
+
ListItem::new(SharedString::from(format!(
"breakpoint-ui-item-{:?}/{}:{}",
self.dir, self.name, self.line
)))
.on_click({
let weak = weak.clone();
- move |_, _, cx| {
+ move |_, window, cx| {
weak.update(cx, |breakpoint_list, cx| {
- breakpoint_list.select_ix(Some(ix), cx);
+ breakpoint_list.select_ix(Some(ix), window, cx);
})
.ok();
}
@@ -613,39 +854,67 @@ impl LineBreakpoint {
cx.stop_propagation();
})
.child(
- v_flex()
- .py_1()
+ h_flex()
+ .w_full()
+ .mr_4()
+ .py_0p5()
.gap_1()
.min_h(px(26.))
- .justify_center()
+ .justify_between()
.id(SharedString::from(format!(
"breakpoint-ui-on-click-go-to-line-{:?}/{}:{}",
self.dir, self.name, self.line
)))
- .on_click(move |_, window, cx| {
- weak.update(cx, |breakpoint_list, cx| {
- breakpoint_list.select_ix(Some(ix), cx);
- breakpoint_list.go_to_line_breakpoint(path.clone(), row, window, cx);
- })
- .ok();
+ .on_click({
+ let weak = weak.clone();
+ move |_, window, cx| {
+ weak.update(cx, |breakpoint_list, cx| {
+ breakpoint_list.select_ix(Some(ix), window, cx);
+ breakpoint_list.go_to_line_breakpoint(path.clone(), row, window, cx);
+ })
+ .ok();
+ }
})
.cursor_pointer()
.child(
h_flex()
- .gap_1()
+ .gap_0p5()
.child(
Label::new(format!("{}:{}", self.name, self.line))
.size(LabelSize::Small)
.line_height_style(ui::LineHeightStyle::UiLabel),
)
- .children(self.dir.clone().map(|dir| {
- Label::new(dir)
- .color(Color::Muted)
- .size(LabelSize::Small)
- .line_height_style(ui::LineHeightStyle::UiLabel)
+ .children(self.dir.as_ref().and_then(|dir| {
+ let path_without_root = Path::new(dir.as_ref())
+ .components()
+ .skip(1)
+ .collect::();
+ path_without_root.components().next()?;
+ Some(
+ Label::new(path_without_root.to_string_lossy().into_owned())
+ .color(Color::Muted)
+ .size(LabelSize::Small)
+ .line_height_style(ui::LineHeightStyle::UiLabel)
+ .truncate(),
+ )
})),
- ),
+ )
+ .when_some(self.dir.as_ref(), |this, parent_dir| {
+ this.tooltip(Tooltip::text(format!("Worktree parent path: {parent_dir}")))
+ })
+ .child(BreakpointOptionsStrip {
+ props,
+ breakpoint: BreakpointEntry {
+ kind: BreakpointEntryKind::LineBreakpoint(self.clone()),
+ weak: weak,
+ },
+ is_selected,
+ focus_handle,
+ strip_mode,
+ index: ix,
+ }),
)
+ .toggle_state(is_selected)
}
}
#[derive(Clone, Debug)]
@@ -658,7 +927,10 @@ struct ExceptionBreakpoint {
impl ExceptionBreakpoint {
fn render(
&mut self,
+ props: SupportedBreakpointProperties,
+ strip_mode: Option,
ix: usize,
+ is_selected: bool,
focus_handle: FocusHandle,
list: WeakEntity,
) -> ListItem {
@@ -669,15 +941,15 @@ impl ExceptionBreakpoint {
};
let id = SharedString::from(&self.id);
let is_enabled = self.is_enabled;
-
+ let weak = list.clone();
ListItem::new(SharedString::from(format!(
"exception-breakpoint-ui-item-{}",
self.id
)))
.on_click({
let list = list.clone();
- move |_, _, cx| {
- list.update(cx, |list, cx| list.select_ix(Some(ix), cx))
+ move |_, window, cx| {
+ list.update(cx, |list, cx| list.select_ix(Some(ix), window, cx))
.ok();
}
})
@@ -691,18 +963,21 @@ impl ExceptionBreakpoint {
"exception-breakpoint-ui-item-{}-click-handler",
self.id
)))
- .tooltip(move |window, cx| {
- Tooltip::for_action_in(
- if is_enabled {
- "Disable Exception Breakpoint"
- } else {
- "Enable Exception Breakpoint"
- },
- &ToggleEnableBreakpoint,
- &focus_handle,
- window,
- cx,
- )
+ .tooltip({
+ let focus_handle = focus_handle.clone();
+ move |window, cx| {
+ Tooltip::for_action_in(
+ if is_enabled {
+ "Disable Exception Breakpoint"
+ } else {
+ "Enable Exception Breakpoint"
+ },
+ &ToggleEnableBreakpoint,
+ &focus_handle,
+ window,
+ cx,
+ )
+ }
})
.on_click({
let list = list.clone();
@@ -722,21 +997,40 @@ impl ExceptionBreakpoint {
.child(Indicator::icon(Icon::new(IconName::Flame)).color(color)),
)
.child(
- v_flex()
- .py_1()
- .gap_1()
- .min_h(px(26.))
- .justify_center()
- .id(("exception-breakpoint-label", ix))
+ h_flex()
+ .w_full()
+ .mr_4()
+ .py_0p5()
+ .justify_between()
.child(
- Label::new(self.data.label.clone())
- .size(LabelSize::Small)
- .line_height_style(ui::LineHeightStyle::UiLabel),
+ v_flex()
+ .py_1()
+ .gap_1()
+ .min_h(px(26.))
+ .justify_center()
+ .id(("exception-breakpoint-label", ix))
+ .child(
+ Label::new(self.data.label.clone())
+ .size(LabelSize::Small)
+ .line_height_style(ui::LineHeightStyle::UiLabel),
+ )
+ .when_some(self.data.description.clone(), |el, description| {
+ el.tooltip(Tooltip::text(description))
+ }),
)
- .when_some(self.data.description.clone(), |el, description| {
- el.tooltip(Tooltip::text(description))
+ .child(BreakpointOptionsStrip {
+ props,
+ breakpoint: BreakpointEntry {
+ kind: BreakpointEntryKind::ExceptionBreakpoint(self.clone()),
+ weak: weak,
+ },
+ is_selected,
+ focus_handle,
+ strip_mode,
+ index: ix,
}),
)
+ .toggle_state(is_selected)
}
}
#[derive(Clone, Debug)]
@@ -754,18 +1048,267 @@ struct BreakpointEntry {
impl BreakpointEntry {
fn render(
&mut self,
+ strip_mode: Option,
+ props: SupportedBreakpointProperties,
ix: usize,
+ is_selected: bool,
focus_handle: FocusHandle,
- _: &mut Window,
- _: &mut App,
) -> ListItem {
match &mut self.kind {
+ BreakpointEntryKind::LineBreakpoint(line_breakpoint) => line_breakpoint.render(
+ props,
+ strip_mode,
+ ix,
+ is_selected,
+ focus_handle,
+ self.weak.clone(),
+ ),
+ BreakpointEntryKind::ExceptionBreakpoint(exception_breakpoint) => exception_breakpoint
+ .render(
+ props.for_exception_breakpoints(),
+ strip_mode,
+ ix,
+ is_selected,
+ focus_handle,
+ self.weak.clone(),
+ ),
+ }
+ }
+
+ fn id(&self) -> SharedString {
+ match &self.kind {
+ BreakpointEntryKind::LineBreakpoint(line_breakpoint) => format!(
+ "source-breakpoint-control-strip-{:?}:{}",
+ line_breakpoint.breakpoint.path, line_breakpoint.breakpoint.row
+ )
+ .into(),
+ BreakpointEntryKind::ExceptionBreakpoint(exception_breakpoint) => format!(
+ "exception-breakpoint-control-strip--{}",
+ exception_breakpoint.id
+ )
+ .into(),
+ }
+ }
+
+ fn has_log(&self) -> bool {
+ match &self.kind {
BreakpointEntryKind::LineBreakpoint(line_breakpoint) => {
- line_breakpoint.render(ix, focus_handle, self.weak.clone())
+ line_breakpoint.breakpoint.message.is_some()
}
- BreakpointEntryKind::ExceptionBreakpoint(exception_breakpoint) => {
- exception_breakpoint.render(ix, focus_handle, self.weak.clone())
+ _ => false,
+ }
+ }
+
+ fn has_condition(&self) -> bool {
+ match &self.kind {
+ BreakpointEntryKind::LineBreakpoint(line_breakpoint) => {
+ line_breakpoint.breakpoint.condition.is_some()
+ }
+ // We don't support conditions on exception breakpoints
+ BreakpointEntryKind::ExceptionBreakpoint(_) => false,
+ }
+ }
+
+ fn has_hit_condition(&self) -> bool {
+ match &self.kind {
+ BreakpointEntryKind::LineBreakpoint(line_breakpoint) => {
+ line_breakpoint.breakpoint.hit_condition.is_some()
}
+ _ => false,
}
}
}
+bitflags::bitflags! {
+ #[derive(Clone, Copy)]
+ pub struct SupportedBreakpointProperties: u32 {
+ const LOG = 1 << 0;
+ const CONDITION = 1 << 1;
+ const HIT_CONDITION = 1 << 2;
+ // Conditions for exceptions can be set only when exception filters are supported.
+ const EXCEPTION_FILTER_OPTIONS = 1 << 3;
+ }
+}
+
+impl From<&Capabilities> for SupportedBreakpointProperties {
+ fn from(caps: &Capabilities) -> Self {
+ let mut this = Self::empty();
+ for (prop, offset) in [
+ (caps.supports_log_points, Self::LOG),
+ (caps.supports_conditional_breakpoints, Self::CONDITION),
+ (
+ caps.supports_hit_conditional_breakpoints,
+ Self::HIT_CONDITION,
+ ),
+ (
+ caps.supports_exception_options,
+ Self::EXCEPTION_FILTER_OPTIONS,
+ ),
+ ] {
+ if prop.unwrap_or_default() {
+ this.insert(offset);
+ }
+ }
+ this
+ }
+}
+
+impl SupportedBreakpointProperties {
+ fn for_exception_breakpoints(self) -> Self {
+ // TODO: we don't yet support conditions for exception breakpoints at the data layer, hence all props are disabled here.
+ Self::empty()
+ }
+}
+#[derive(IntoElement)]
+struct BreakpointOptionsStrip {
+ props: SupportedBreakpointProperties,
+ breakpoint: BreakpointEntry,
+ is_selected: bool,
+ focus_handle: FocusHandle,
+ strip_mode: Option,
+ index: usize,
+}
+
+impl BreakpointOptionsStrip {
+ fn is_toggled(&self, expected_mode: ActiveBreakpointStripMode) -> bool {
+ self.is_selected && self.strip_mode == Some(expected_mode)
+ }
+ fn on_click_callback(
+ &self,
+ mode: ActiveBreakpointStripMode,
+ ) -> impl for<'a> Fn(&ClickEvent, &mut Window, &'a mut App) + use<> {
+ let list = self.breakpoint.weak.clone();
+ let ix = self.index;
+ move |_, window, cx| {
+ list.update(cx, |this, cx| {
+ if this.strip_mode != Some(mode) {
+ this.set_active_breakpoint_property(mode, window, cx);
+ } else if this.selected_ix == Some(ix) {
+ this.strip_mode.take();
+ } else {
+ cx.propagate();
+ }
+ })
+ .ok();
+ }
+ }
+ fn add_border(
+ &self,
+ kind: ActiveBreakpointStripMode,
+ available: bool,
+ window: &Window,
+ cx: &App,
+ ) -> impl Fn(Div) -> Div {
+ move |this: Div| {
+ // Avoid layout shifts in case there's no colored border
+ let this = this.border_2().rounded_sm();
+ if self.is_selected && self.strip_mode == Some(kind) {
+ let theme = cx.theme().colors();
+ if self.focus_handle.is_focused(window) {
+ this.border_color(theme.border_selected)
+ } else {
+ this.border_color(theme.border_disabled)
+ }
+ } else if !available {
+ this.border_color(cx.theme().colors().border_disabled)
+ } else {
+ this
+ }
+ }
+ }
+}
+impl RenderOnce for BreakpointOptionsStrip {
+ fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
+ let id = self.breakpoint.id();
+ let supports_logs = self.props.contains(SupportedBreakpointProperties::LOG);
+ let supports_condition = self
+ .props
+ .contains(SupportedBreakpointProperties::CONDITION);
+ let supports_hit_condition = self
+ .props
+ .contains(SupportedBreakpointProperties::HIT_CONDITION);
+ let has_logs = self.breakpoint.has_log();
+ let has_condition = self.breakpoint.has_condition();
+ let has_hit_condition = self.breakpoint.has_hit_condition();
+ let style_for_toggle = |mode, is_enabled| {
+ if is_enabled && self.strip_mode == Some(mode) && self.is_selected {
+ ui::ButtonStyle::Filled
+ } else {
+ ui::ButtonStyle::Subtle
+ }
+ };
+ let color_for_toggle = |is_enabled| {
+ if is_enabled {
+ ui::Color::Default
+ } else {
+ ui::Color::Muted
+ }
+ };
+
+ h_flex()
+ .gap_1()
+ .child(
+ div().map(self.add_border(ActiveBreakpointStripMode::Log, supports_logs, window, cx))
+ .child(
+ IconButton::new(
+ SharedString::from(format!("{id}-log-toggle")),
+ IconName::ScrollText,
+ )
+ .icon_size(IconSize::XSmall)
+ .style(style_for_toggle(ActiveBreakpointStripMode::Log, has_logs))
+ .icon_color(color_for_toggle(has_logs))
+ .disabled(!supports_logs)
+ .toggle_state(self.is_toggled(ActiveBreakpointStripMode::Log))
+ .on_click(self.on_click_callback(ActiveBreakpointStripMode::Log)).tooltip(|window, cx| Tooltip::with_meta("Set Log Message", None, "Set log message to display (instead of stopping) when a breakpoint is hit", window, cx))
+ )
+ .when(!has_logs && !self.is_selected, |this| this.invisible()),
+ )
+ .child(
+ div().map(self.add_border(
+ ActiveBreakpointStripMode::Condition,
+ supports_condition,
+ window, cx
+ ))
+ .child(
+ IconButton::new(
+ SharedString::from(format!("{id}-condition-toggle")),
+ IconName::SplitAlt,
+ )
+ .icon_size(IconSize::XSmall)
+ .style(style_for_toggle(
+ ActiveBreakpointStripMode::Condition,
+ has_condition
+ ))
+ .icon_color(color_for_toggle(has_condition))
+ .disabled(!supports_condition)
+ .toggle_state(self.is_toggled(ActiveBreakpointStripMode::Condition))
+ .on_click(self.on_click_callback(ActiveBreakpointStripMode::Condition))
+ .tooltip(|window, cx| Tooltip::with_meta("Set Condition", None, "Set condition to evaluate when a breakpoint is hit. Program execution will stop only when the condition is met", window, cx))
+ )
+ .when(!has_condition && !self.is_selected, |this| this.invisible()),
+ )
+ .child(
+ div().map(self.add_border(
+ ActiveBreakpointStripMode::HitCondition,
+ supports_hit_condition,window, cx
+ ))
+ .child(
+ IconButton::new(
+ SharedString::from(format!("{id}-hit-condition-toggle")),
+ IconName::ArrowDown10,
+ )
+ .icon_size(IconSize::XSmall)
+ .style(style_for_toggle(
+ ActiveBreakpointStripMode::HitCondition,
+ has_hit_condition,
+ ))
+ .icon_color(color_for_toggle(has_hit_condition))
+ .disabled(!supports_hit_condition)
+ .toggle_state(self.is_toggled(ActiveBreakpointStripMode::HitCondition))
+ .on_click(self.on_click_callback(ActiveBreakpointStripMode::HitCondition)).tooltip(|window, cx| Tooltip::with_meta("Set Hit Condition", None, "Set expression that controls how many hits of the breakpoint are ignored.", window, cx))
+ )
+ .when(!has_hit_condition && !self.is_selected, |this| {
+ this.invisible()
+ }),
+ )
+ }
+}
diff --git a/crates/debugger_ui/src/session/running/console.rs b/crates/debugger_ui/src/session/running/console.rs
index 83d2d46547ada9da328cc44443813a87a6f681f1..aaac63640188b2b277d1ff8bfb9b75b114f5554b 100644
--- a/crates/debugger_ui/src/session/running/console.rs
+++ b/crates/debugger_ui/src/session/running/console.rs
@@ -114,7 +114,7 @@ impl Console {
}
fn is_running(&self, cx: &Context) -> bool {
- self.session.read(cx).is_running()
+ self.session.read(cx).is_started()
}
fn handle_stack_frame_list_events(
diff --git a/crates/debugger_ui/src/stack_trace_view.rs b/crates/debugger_ui/src/stack_trace_view.rs
index 675522e99996b276b5f62eeb88297dfe7d592579..aef053df4a1ea930fb09a779e08afecfa08ddde9 100644
--- a/crates/debugger_ui/src/stack_trace_view.rs
+++ b/crates/debugger_ui/src/stack_trace_view.rs
@@ -4,7 +4,7 @@ use collections::HashMap;
use dap::StackFrameId;
use editor::{
Anchor, Bias, DebugStackFrameLine, Editor, EditorEvent, ExcerptId, ExcerptRange, MultiBuffer,
- RowHighlightOptions, ToPoint, scroll::Autoscroll,
+ RowHighlightOptions, SelectionEffects, ToPoint, scroll::Autoscroll,
};
use gpui::{
AnyView, App, AppContext, Entity, EventEmitter, Focusable, IntoElement, Render, SharedString,
@@ -99,10 +99,11 @@ impl StackTraceView {
if frame_anchor.excerpt_id
!= editor.selections.newest_anchor().head().excerpt_id
{
- let auto_scroll =
- Some(Autoscroll::center().for_anchor(frame_anchor));
+ let effects = SelectionEffects::scroll(
+ Autoscroll::center().for_anchor(frame_anchor),
+ );
- editor.change_selections(auto_scroll, window, cx, |selections| {
+ editor.change_selections(effects, window, cx, |selections| {
let selection_id = selections.new_selection_id();
let selection = Selection {
diff --git a/crates/debugger_ui/src/tests/dap_logger.rs b/crates/debugger_ui/src/tests/dap_logger.rs
index 0427a5c4ac41161739295dad194f7d1e94d1dec9..ff2b0f695f6a2e7f0ca65b49938e0129efb04326 100644
--- a/crates/debugger_ui/src/tests/dap_logger.rs
+++ b/crates/debugger_ui/src/tests/dap_logger.rs
@@ -37,15 +37,23 @@ async fn test_dap_logger_captures_all_session_rpc_messages(
.await;
assert!(
- log_store.read_with(cx, |log_store, _| log_store
- .contained_session_ids()
- .is_empty()),
- "log_store shouldn't contain any session IDs before any sessions were created"
+ log_store.read_with(cx, |log_store, _| !log_store.has_projects()),
+ "log_store shouldn't contain any projects before any projects were created"
);
let project = Project::test(fs, [path!("/project").as_ref()], cx).await;
let workspace = init_test_workspace(&project, cx).await;
+ assert!(
+ log_store.read_with(cx, |log_store, _| log_store.has_projects()),
+ "log_store shouldn't contain any projects before any projects were created"
+ );
+ assert!(
+ log_store.read_with(cx, |log_store, _| log_store
+ .contained_session_ids(&project.downgrade())
+ .is_empty()),
+ "log_store shouldn't contain any projects before any projects were created"
+ );
let cx = &mut VisualTestContext::from_window(*workspace, cx);
// Start a debug session
@@ -54,20 +62,22 @@ async fn test_dap_logger_captures_all_session_rpc_messages(
let client = session.update(cx, |session, _| session.adapter_client().unwrap());
assert_eq!(
- log_store.read_with(cx, |log_store, _| log_store.contained_session_ids().len()),
+ log_store.read_with(cx, |log_store, _| log_store
+ .contained_session_ids(&project.downgrade())
+ .len()),
1,
);
assert!(
log_store.read_with(cx, |log_store, _| log_store
- .contained_session_ids()
+ .contained_session_ids(&project.downgrade())
.contains(&session_id)),
"log_store should contain the session IDs of the started session"
);
assert!(
!log_store.read_with(cx, |log_store, _| log_store
- .rpc_messages_for_session_id(session_id)
+ .rpc_messages_for_session_id(&project.downgrade(), session_id)
.is_empty()),
"We should have the initialization sequence in the log store"
);
diff --git a/crates/debugger_ui/src/tests/new_process_modal.rs b/crates/debugger_ui/src/tests/new_process_modal.rs
index 201b149746a918a461701920bf7be4dc85510aa7..eb8c7f8063f23b8097efca4a16c071b1649cb903 100644
--- a/crates/debugger_ui/src/tests/new_process_modal.rs
+++ b/crates/debugger_ui/src/tests/new_process_modal.rs
@@ -267,7 +267,6 @@ async fn test_dap_adapter_config_conversion_and_validation(cx: &mut TestAppConte
"Debugpy",
"PHP",
"JavaScript",
- "Ruby",
"Delve",
"GDB",
"fake-adapter",
diff --git a/crates/diagnostics/src/diagnostic_renderer.rs b/crates/diagnostics/src/diagnostic_renderer.rs
index 9524f97ff1e14599576df549844ee7c164d6d017..77bb249733f612ede3017e1cff592927b40e8d43 100644
--- a/crates/diagnostics/src/diagnostic_renderer.rs
+++ b/crates/diagnostics/src/diagnostic_renderer.rs
@@ -4,7 +4,6 @@ use editor::{
Anchor, Editor, EditorSnapshot, ToOffset,
display_map::{BlockContext, BlockPlacement, BlockProperties, BlockStyle},
hover_popover::diagnostics_markdown_style,
- scroll::Autoscroll,
};
use gpui::{AppContext, Entity, Focusable, WeakEntity};
use language::{BufferId, Diagnostic, DiagnosticEntry};
@@ -311,7 +310,7 @@ impl DiagnosticBlock {
let range = range.start.to_offset(&snapshot)..range.end.to_offset(&snapshot);
editor.unfold_ranges(&[range.start..range.end], true, false, cx);
- editor.change_selections(Some(Autoscroll::fit()), window, cx, |s| {
+ editor.change_selections(Default::default(), window, cx, |s| {
s.select_ranges([range.start..range.start]);
});
window.focus(&editor.focus_handle(cx));
diff --git a/crates/diagnostics/src/diagnostics.rs b/crates/diagnostics/src/diagnostics.rs
index 4f66a5a8839ddd8a3a2405a2b57114b73a1cf9f8..8b49c536245a2509cb73254eca8de6d1be1cfd75 100644
--- a/crates/diagnostics/src/diagnostics.rs
+++ b/crates/diagnostics/src/diagnostics.rs
@@ -12,7 +12,6 @@ use diagnostic_renderer::DiagnosticBlock;
use editor::{
DEFAULT_MULTIBUFFER_CONTEXT, Editor, EditorEvent, ExcerptRange, MultiBuffer, PathKey,
display_map::{BlockPlacement, BlockProperties, BlockStyle, CustomBlockId},
- scroll::Autoscroll,
};
use futures::future::join_all;
use gpui::{
@@ -626,7 +625,7 @@ impl ProjectDiagnosticsEditor {
if let Some(anchor_range) = anchor_ranges.first() {
let range_to_select = anchor_range.start..anchor_range.start;
this.editor.update(cx, |editor, cx| {
- editor.change_selections(Some(Autoscroll::fit()), window, cx, |s| {
+ editor.change_selections(Default::default(), window, cx, |s| {
s.select_anchor_ranges([range_to_select]);
})
});
diff --git a/crates/editor/Cargo.toml b/crates/editor/Cargo.toml
index c42b58729e04edd9138002209d7c8db305c853a0..bea83b1df826a3dac1cf4afe14a0dd7b417b972b 100644
--- a/crates/editor/Cargo.toml
+++ b/crates/editor/Cargo.toml
@@ -61,6 +61,7 @@ parking_lot.workspace = true
pretty_assertions.workspace = true
project.workspace = true
rand.workspace = true
+regex.workspace = true
rpc.workspace = true
schemars.workspace = true
serde.workspace = true
diff --git a/crates/editor/src/display_map.rs b/crates/editor/src/display_map.rs
index fd371e20cbf22585d1dd2640f01104dd16428750..3352d21ef878835987e0227a926dfb61c893a182 100644
--- a/crates/editor/src/display_map.rs
+++ b/crates/editor/src/display_map.rs
@@ -37,7 +37,9 @@ pub use block_map::{
use block_map::{BlockRow, BlockSnapshot};
use collections::{HashMap, HashSet};
pub use crease_map::*;
-pub use fold_map::{ChunkRenderer, ChunkRendererContext, Fold, FoldId, FoldPlaceholder, FoldPoint};
+pub use fold_map::{
+ ChunkRenderer, ChunkRendererContext, ChunkRendererId, Fold, FoldId, FoldPlaceholder, FoldPoint,
+};
use fold_map::{FoldMap, FoldSnapshot};
use gpui::{App, Context, Entity, Font, HighlightStyle, LineLayout, Pixels, UnderlineStyle};
pub use inlay_map::Inlay;
@@ -538,7 +540,7 @@ impl DisplayMap {
pub fn update_fold_widths(
&mut self,
- widths: impl IntoIterator
- ,
+ widths: impl IntoIterator
- ,
cx: &mut Context,
) -> bool {
let snapshot = self.buffer.read(cx).snapshot(cx);
@@ -966,10 +968,22 @@ impl DisplaySnapshot {
.and_then(|id| id.style(&editor_style.syntax));
if let Some(chunk_highlight) = chunk.highlight_style {
+ // For color inlays, blend the color with the editor background
+ let mut processed_highlight = chunk_highlight;
+ if chunk.is_inlay {
+ if let Some(inlay_color) = chunk_highlight.color {
+ // Only blend if the color has transparency (alpha < 1.0)
+ if inlay_color.a < 1.0 {
+ let blended_color = editor_style.background.blend(inlay_color);
+ processed_highlight.color = Some(blended_color);
+ }
+ }
+ }
+
if let Some(highlight_style) = highlight_style.as_mut() {
- highlight_style.highlight(chunk_highlight);
+ highlight_style.highlight(processed_highlight);
} else {
- highlight_style = Some(chunk_highlight);
+ highlight_style = Some(processed_highlight);
}
}
diff --git a/crates/editor/src/display_map/fold_map.rs b/crates/editor/src/display_map/fold_map.rs
index 92456836a9766b1ab6fb5e3d4dfc406dc0bc393b..f37e7063e7228176b0f5455c278f331ed31d6ba0 100644
--- a/crates/editor/src/display_map/fold_map.rs
+++ b/crates/editor/src/display_map/fold_map.rs
@@ -1,3 +1,5 @@
+use crate::{InlayId, display_map::inlay_map::InlayChunk};
+
use super::{
Highlights,
inlay_map::{InlayBufferRows, InlayChunks, InlayEdit, InlayOffset, InlayPoint, InlaySnapshot},
@@ -275,13 +277,16 @@ impl FoldMapWriter<'_> {
pub(crate) fn update_fold_widths(
&mut self,
- new_widths: impl IntoIterator
- ,
+ new_widths: impl IntoIterator
- ,
) -> (FoldSnapshot, Vec) {
let mut edits = Vec::new();
let inlay_snapshot = self.0.snapshot.inlay_snapshot.clone();
let buffer = &inlay_snapshot.buffer;
for (id, new_width) in new_widths {
+ let ChunkRendererId::Fold(id) = id else {
+ continue;
+ };
if let Some(metadata) = self.0.snapshot.fold_metadata_by_id.get(&id).cloned() {
if Some(new_width) != metadata.width {
let buffer_start = metadata.range.start.to_offset(buffer);
@@ -527,7 +532,7 @@ impl FoldMap {
placeholder: Some(TransformPlaceholder {
text: ELLIPSIS,
renderer: ChunkRenderer {
- id: fold.id,
+ id: ChunkRendererId::Fold(fold.id),
render: Arc::new(move |cx| {
(fold.placeholder.render)(
fold_id,
@@ -1060,7 +1065,7 @@ impl sum_tree::Summary for TransformSummary {
}
#[derive(Copy, Clone, Eq, PartialEq, Debug, Default, Ord, PartialOrd, Hash)]
-pub struct FoldId(usize);
+pub struct FoldId(pub(super) usize);
impl From for ElementId {
fn from(val: FoldId) -> Self {
@@ -1265,11 +1270,17 @@ pub struct Chunk<'a> {
pub renderer: Option,
}
+#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
+pub enum ChunkRendererId {
+ Fold(FoldId),
+ Inlay(InlayId),
+}
+
/// A recipe for how the chunk should be presented.
#[derive(Clone)]
pub struct ChunkRenderer {
- /// The id of the fold associated with this chunk.
- pub id: FoldId,
+ /// The id of the renderer associated with this chunk.
+ pub id: ChunkRendererId,
/// Creates a custom element to represent this chunk.
pub render: Arc AnyElement>,
/// If true, the element is constrained to the shaped width of the text.
@@ -1311,7 +1322,7 @@ impl DerefMut for ChunkRendererContext<'_, '_> {
pub struct FoldChunks<'a> {
transform_cursor: Cursor<'a, Transform, (FoldOffset, InlayOffset)>,
inlay_chunks: InlayChunks<'a>,
- inlay_chunk: Option<(InlayOffset, language::Chunk<'a>)>,
+ inlay_chunk: Option<(InlayOffset, InlayChunk<'a>)>,
inlay_offset: InlayOffset,
output_offset: FoldOffset,
max_output_offset: FoldOffset,
@@ -1403,7 +1414,8 @@ impl<'a> Iterator for FoldChunks<'a> {
}
// Otherwise, take a chunk from the buffer's text.
- if let Some((buffer_chunk_start, mut chunk)) = self.inlay_chunk.clone() {
+ if let Some((buffer_chunk_start, mut inlay_chunk)) = self.inlay_chunk.clone() {
+ let chunk = &mut inlay_chunk.chunk;
let buffer_chunk_end = buffer_chunk_start + InlayOffset(chunk.text.len());
let transform_end = self.transform_cursor.end(&()).1;
let chunk_end = buffer_chunk_end.min(transform_end);
@@ -1428,7 +1440,7 @@ impl<'a> Iterator for FoldChunks<'a> {
is_tab: chunk.is_tab,
is_inlay: chunk.is_inlay,
underline: chunk.underline,
- renderer: None,
+ renderer: inlay_chunk.renderer,
});
}
diff --git a/crates/editor/src/display_map/inlay_map.rs b/crates/editor/src/display_map/inlay_map.rs
index 33fc5540d63f20e5108e438f38c3cba4703ad927..49b5ce1d26916de0ec79ab80ec21f1bcf8b335e3 100644
--- a/crates/editor/src/display_map/inlay_map.rs
+++ b/crates/editor/src/display_map/inlay_map.rs
@@ -1,4 +1,4 @@
-use crate::{HighlightStyles, InlayId};
+use crate::{ChunkRenderer, HighlightStyles, InlayId};
use collections::BTreeSet;
use gpui::{Hsla, Rgba};
use language::{Chunk, Edit, Point, TextSummary};
@@ -8,11 +8,13 @@ use multi_buffer::{
use std::{
cmp,
ops::{Add, AddAssign, Range, Sub, SubAssign},
+ sync::Arc,
};
use sum_tree::{Bias, Cursor, SumTree};
use text::{Patch, Rope};
+use ui::{ActiveTheme, IntoElement as _, ParentElement as _, Styled as _, div};
-use super::{Highlights, custom_highlights::CustomHighlightsChunks};
+use super::{Highlights, custom_highlights::CustomHighlightsChunks, fold_map::ChunkRendererId};
/// Decides where the [`Inlay`]s should be displayed.
///
@@ -252,6 +254,13 @@ pub struct InlayChunks<'a> {
snapshot: &'a InlaySnapshot,
}
+#[derive(Clone)]
+pub struct InlayChunk<'a> {
+ pub chunk: Chunk<'a>,
+ /// Whether the inlay should be customly rendered.
+ pub renderer: Option,
+}
+
impl InlayChunks<'_> {
pub fn seek(&mut self, new_range: Range) {
self.transforms.seek(&new_range.start, Bias::Right, &());
@@ -271,7 +280,7 @@ impl InlayChunks<'_> {
}
impl<'a> Iterator for InlayChunks<'a> {
- type Item = Chunk<'a>;
+ type Item = InlayChunk<'a>;
fn next(&mut self) -> Option {
if self.output_offset == self.max_output_offset {
@@ -296,9 +305,12 @@ impl<'a> Iterator for InlayChunks<'a> {
chunk.text = suffix;
self.output_offset.0 += prefix.len();
- Chunk {
- text: prefix,
- ..chunk.clone()
+ InlayChunk {
+ chunk: Chunk {
+ text: prefix,
+ ..chunk.clone()
+ },
+ renderer: None,
}
}
Transform::Inlay(inlay) => {
@@ -313,6 +325,7 @@ impl<'a> Iterator for InlayChunks<'a> {
}
}
+ let mut renderer = None;
let mut highlight_style = match inlay.id {
InlayId::InlineCompletion(_) => {
self.highlight_styles.inline_completion.map(|s| {
@@ -325,14 +338,31 @@ impl<'a> Iterator for InlayChunks<'a> {
}
InlayId::Hint(_) => self.highlight_styles.inlay_hint,
InlayId::DebuggerValue(_) => self.highlight_styles.inlay_hint,
- InlayId::Color(_) => match inlay.color {
- Some(color) => {
- let style = self.highlight_styles.inlay_hint.get_or_insert_default();
- style.color = Some(color);
- Some(*style)
+ InlayId::Color(_) => {
+ if let Some(color) = inlay.color {
+ renderer = Some(ChunkRenderer {
+ id: ChunkRendererId::Inlay(inlay.id),
+ render: Arc::new(move |cx| {
+ div()
+ .relative()
+ .size_3p5()
+ .child(
+ div()
+ .absolute()
+ .right_1()
+ .size_3()
+ .border_1()
+ .border_color(cx.theme().colors().border)
+ .bg(color),
+ )
+ .into_any_element()
+ }),
+ constrain_width: false,
+ measured_width: None,
+ });
}
- None => self.highlight_styles.inlay_hint,
- },
+ self.highlight_styles.inlay_hint
+ }
};
let next_inlay_highlight_endpoint;
let offset_in_inlay = self.output_offset - self.transforms.start().0;
@@ -370,11 +400,14 @@ impl<'a> Iterator for InlayChunks<'a> {
self.output_offset.0 += chunk.len();
- Chunk {
- text: chunk,
- highlight_style,
- is_inlay: true,
- ..Default::default()
+ InlayChunk {
+ chunk: Chunk {
+ text: chunk,
+ highlight_style,
+ is_inlay: true,
+ ..Chunk::default()
+ },
+ renderer,
}
}
};
@@ -1066,7 +1099,7 @@ impl InlaySnapshot {
#[cfg(test)]
pub fn text(&self) -> String {
self.chunks(Default::default()..self.len(), false, Highlights::default())
- .map(|chunk| chunk.text)
+ .map(|chunk| chunk.chunk.text)
.collect()
}
@@ -1704,7 +1737,7 @@ mod tests {
..Highlights::default()
},
)
- .map(|chunk| chunk.text)
+ .map(|chunk| chunk.chunk.text)
.collect::();
assert_eq!(
actual_text,
diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs
index ea30cc6fab94d7a80e8855efd3832b21a945b6c1..fe904ab4ec09faba7ce91fd3600363bde05339a0 100644
--- a/crates/editor/src/editor.rs
+++ b/crates/editor/src/editor.rs
@@ -547,6 +547,7 @@ pub enum SoftWrap {
#[derive(Clone)]
pub struct EditorStyle {
pub background: Hsla,
+ pub border: Hsla,
pub local_player: PlayerColor,
pub text: TextStyle,
pub scrollbar_width: Pixels,
@@ -562,6 +563,7 @@ impl Default for EditorStyle {
fn default() -> Self {
Self {
background: Hsla::default(),
+ border: Hsla::default(),
local_player: PlayerColor::default(),
text: TextStyle::default(),
scrollbar_width: Pixels::default(),
@@ -1143,6 +1145,7 @@ pub struct Editor {
drag_and_drop_selection_enabled: bool,
next_color_inlay_id: usize,
colors: Option,
+ folding_newlines: Task<()>,
}
#[derive(Copy, Clone, Debug, PartialEq, Eq, Default)]
@@ -1215,6 +1218,12 @@ impl GutterDimensions {
}
}
+struct CharacterDimensions {
+ em_width: Pixels,
+ em_advance: Pixels,
+ line_height: Pixels,
+}
+
#[derive(Debug)]
pub struct RemoteSelection {
pub replica_id: ReplicaId,
@@ -1255,8 +1264,21 @@ impl Default for SelectionHistoryMode {
}
#[derive(Debug)]
+/// SelectionEffects controls the side-effects of updating the selection.
+///
+/// The default behaviour does "what you mostly want":
+/// - it pushes to the nav history if the cursor moved by >10 lines
+/// - it re-triggers completion requests
+/// - it scrolls to fit
+///
+/// You might want to modify these behaviours. For example when doing a "jump"
+/// like go to definition, we always want to add to nav history; but when scrolling
+/// in vim mode we never do.
+///
+/// Similarly, you might want to disable scrolling if you don't want the viewport to
+/// move.
pub struct SelectionEffects {
- nav_history: bool,
+ nav_history: Option,
completions: bool,
scroll: Option,
}
@@ -1264,7 +1286,7 @@ pub struct SelectionEffects {
impl Default for SelectionEffects {
fn default() -> Self {
Self {
- nav_history: true,
+ nav_history: None,
completions: true,
scroll: Some(Autoscroll::fit()),
}
@@ -1294,7 +1316,7 @@ impl SelectionEffects {
pub fn nav_history(self, nav_history: bool) -> Self {
Self {
- nav_history,
+ nav_history: Some(nav_history),
..self
}
}
@@ -1825,13 +1847,13 @@ impl Editor {
editor
.refresh_inlay_hints(InlayHintRefreshReason::RefreshRequested, cx);
}
- project::Event::LanguageServerAdded(server_id, ..)
- | project::Event::LanguageServerRemoved(server_id) => {
+ project::Event::LanguageServerAdded(..)
+ | project::Event::LanguageServerRemoved(..) => {
if editor.tasks_update_task.is_none() {
editor.tasks_update_task =
Some(editor.refresh_runnables(window, cx));
}
- editor.update_lsp_data(Some(*server_id), None, window, cx);
+ editor.update_lsp_data(true, None, window, cx);
}
project::Event::SnippetEdit(id, snippet_edits) => {
if let Some(buffer) = editor.buffer.read(cx).buffer(*id) {
@@ -2153,6 +2175,7 @@ impl Editor {
mode,
selection_drag_state: SelectionDragState::None,
drag_and_drop_selection_enabled: EditorSettings::get_global(cx).drag_and_drop_selection,
+ folding_newlines: Task::ready(()),
};
if let Some(breakpoints) = editor.breakpoint_store.as_ref() {
editor
@@ -2270,7 +2293,7 @@ impl Editor {
editor.minimap =
editor.create_minimap(EditorSettings::get_global(cx).minimap, window, cx);
editor.colors = Some(LspColorData::new(cx));
- editor.update_lsp_data(None, None, window, cx);
+ editor.update_lsp_data(false, None, window, cx);
}
editor.report_editor_event("Editor Opened", None, cx);
@@ -2909,11 +2932,12 @@ impl Editor {
let new_cursor_position = newest_selection.head();
let selection_start = newest_selection.start;
- if effects.nav_history {
+ if effects.nav_history.is_none() || effects.nav_history == Some(true) {
self.push_to_nav_history(
*old_cursor_position,
Some(new_cursor_position.to_point(buffer)),
false,
+ effects.nav_history == Some(true),
cx,
);
}
@@ -3155,16 +3179,15 @@ impl Editor {
/// effects of selection change occur at the end of the transaction.
pub fn change_selections(
&mut self,
- effects: impl Into,
+ effects: SelectionEffects,
window: &mut Window,
cx: &mut Context,
change: impl FnOnce(&mut MutableSelectionsCollection<'_>) -> R,
) -> R {
- let effects = effects.into();
if let Some(state) = &mut self.deferred_selection_effects_state {
state.effects.scroll = effects.scroll.or(state.effects.scroll);
state.effects.completions = effects.completions;
- state.effects.nav_history |= effects.nav_history;
+ state.effects.nav_history = effects.nav_history.or(state.effects.nav_history);
let (changed, result) = self.selections.change_with(cx, change);
state.changed |= changed;
return result;
@@ -3440,8 +3463,13 @@ impl Editor {
};
let selections_count = self.selections.count();
+ let effects = if auto_scroll {
+ SelectionEffects::default()
+ } else {
+ SelectionEffects::no_scroll()
+ };
- self.change_selections(auto_scroll.then(Autoscroll::newest), window, cx, |s| {
+ self.change_selections(effects, window, cx, |s| {
if let Some(point_to_delete) = point_to_delete {
s.delete(point_to_delete);
@@ -3479,13 +3507,18 @@ impl Editor {
.buffer_snapshot
.anchor_before(position.to_point(&display_map));
- self.change_selections(Some(Autoscroll::newest()), window, cx, |s| {
- s.clear_disjoint();
- s.set_pending_anchor_range(
- pointer_position..pointer_position,
- SelectMode::Character,
- );
- });
+ self.change_selections(
+ SelectionEffects::scroll(Autoscroll::newest()),
+ window,
+ cx,
+ |s| {
+ s.clear_disjoint();
+ s.set_pending_anchor_range(
+ pointer_position..pointer_position,
+ SelectMode::Character,
+ );
+ },
+ );
};
let tail = self.selections.newest::(cx).tail();
@@ -3600,7 +3633,7 @@ impl Editor {
pending.reversed = false;
}
- self.change_selections(None, window, cx, |s| {
+ self.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
s.set_pending(pending, mode);
});
} else {
@@ -3616,7 +3649,7 @@ impl Editor {
self.columnar_selection_state.take();
if self.selections.pending_anchor().is_some() {
let selections = self.selections.all::(cx);
- self.change_selections(None, window, cx, |s| {
+ self.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
s.select(selections);
s.clear_pending();
});
@@ -3690,7 +3723,7 @@ impl Editor {
_ => selection_ranges,
};
- self.change_selections(None, window, cx, |s| {
+ self.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
s.select_ranges(ranges);
});
cx.notify();
@@ -3730,7 +3763,7 @@ impl Editor {
}
if self.mode.is_full()
- && self.change_selections(Some(Autoscroll::fit()), window, cx, |s| s.try_cancel())
+ && self.change_selections(Default::default(), window, cx, |s| s.try_cancel())
{
return;
}
@@ -3897,8 +3930,10 @@ impl Editor {
bracket_pair_matching_end = Some(pair.clone());
}
}
- if bracket_pair.is_none() && bracket_pair_matching_end.is_some() {
- bracket_pair = Some(bracket_pair_matching_end.unwrap());
+ if let Some(end) = bracket_pair_matching_end
+ && bracket_pair.is_none()
+ {
+ bracket_pair = Some(end);
is_bracket_pair_end = true;
}
}
@@ -4531,9 +4566,7 @@ impl Editor {
})
.collect();
- this.change_selections(Some(Autoscroll::fit()), window, cx, |s| {
- s.select(new_selections)
- });
+ this.change_selections(Default::default(), window, cx, |s| s.select(new_selections));
this.refresh_inline_completion(true, false, window, cx);
});
}
@@ -4562,7 +4595,7 @@ impl Editor {
self.transact(window, cx, |editor, window, cx| {
editor.edit(edits, cx);
- editor.change_selections(Some(Autoscroll::fit()), window, cx, |s| {
+ editor.change_selections(Default::default(), window, cx, |s| {
let mut index = 0;
s.move_cursors_with(|map, _, _| {
let row = rows[index];
@@ -4624,7 +4657,7 @@ impl Editor {
self.transact(window, cx, |editor, window, cx| {
editor.edit(edits, cx);
- editor.change_selections(Some(Autoscroll::fit()), window, cx, |s| {
+ editor.change_selections(Default::default(), window, cx, |s| {
let mut index = 0;
s.move_cursors_with(|map, _, _| {
let row = rows[index];
@@ -4701,7 +4734,7 @@ impl Editor {
anchors
});
- this.change_selections(Some(Autoscroll::fit()), window, cx, |s| {
+ this.change_selections(Default::default(), window, cx, |s| {
s.select_anchors(selection_anchors);
});
@@ -4845,7 +4878,7 @@ impl Editor {
.collect();
drop(buffer);
- self.change_selections(None, window, cx, |selections| {
+ self.change_selections(SelectionEffects::no_scroll(), window, cx, |selections| {
selections.select(new_selections)
});
}
@@ -5072,7 +5105,7 @@ impl Editor {
to_insert,
}) = self.inlay_hint_cache.spawn_hint_refresh(
reason_description,
- self.excerpts_for_inlay_hints_query(required_languages.as_ref(), cx),
+ self.visible_excerpts(required_languages.as_ref(), cx),
invalidate_cache,
ignore_debounce,
cx,
@@ -5090,7 +5123,7 @@ impl Editor {
.collect()
}
- pub fn excerpts_for_inlay_hints_query(
+ pub fn visible_excerpts(
&self,
restrict_to_languages: Option<&HashSet>>,
cx: &mut Context,
@@ -6708,6 +6741,77 @@ impl Editor {
})
}
+ fn refresh_single_line_folds(&mut self, window: &mut Window, cx: &mut Context) {
+ struct NewlineFold;
+ let type_id = std::any::TypeId::of::();
+ if !self.mode.is_single_line() {
+ return;
+ }
+ let snapshot = self.snapshot(window, cx);
+ if snapshot.buffer_snapshot.max_point().row == 0 {
+ return;
+ }
+ let task = cx.background_spawn(async move {
+ let new_newlines = snapshot
+ .buffer_chars_at(0)
+ .filter_map(|(c, i)| {
+ if c == '\n' {
+ Some(
+ snapshot.buffer_snapshot.anchor_after(i)
+ ..snapshot.buffer_snapshot.anchor_before(i + 1),
+ )
+ } else {
+ None
+ }
+ })
+ .collect::>();
+ let existing_newlines = snapshot
+ .folds_in_range(0..snapshot.buffer_snapshot.len())
+ .filter_map(|fold| {
+ if fold.placeholder.type_tag == Some(type_id) {
+ Some(fold.range.start..fold.range.end)
+ } else {
+ None
+ }
+ })
+ .collect::>();
+
+ (new_newlines, existing_newlines)
+ });
+ self.folding_newlines = cx.spawn(async move |this, cx| {
+ let (new_newlines, existing_newlines) = task.await;
+ if new_newlines == existing_newlines {
+ return;
+ }
+ let placeholder = FoldPlaceholder {
+ render: Arc::new(move |_, _, cx| {
+ div()
+ .bg(cx.theme().status().hint_background)
+ .border_b_1()
+ .size_full()
+ .font(ThemeSettings::get_global(cx).buffer_font.clone())
+ .border_color(cx.theme().status().hint)
+ .child("\\n")
+ .into_any()
+ }),
+ constrain_width: false,
+ merge_adjacent: false,
+ type_tag: Some(type_id),
+ };
+ let creases = new_newlines
+ .into_iter()
+ .map(|range| Crease::simple(range, placeholder.clone()))
+ .collect();
+ this.update(cx, |this, cx| {
+ this.display_map.update(cx, |display_map, cx| {
+ display_map.remove_folds_with_type(existing_newlines, type_id, cx);
+ display_map.fold(creases, cx);
+ });
+ })
+ .ok();
+ });
+ }
+
fn refresh_selected_text_highlights(
&mut self,
on_buffer_edit: bool,
@@ -7078,7 +7182,7 @@ impl Editor {
self.unfold_ranges(&[target..target], true, false, cx);
// Note that this is also done in vim's handler of the Tab action.
self.change_selections(
- Some(Autoscroll::newest()),
+ SelectionEffects::scroll(Autoscroll::newest()),
window,
cx,
|selections| {
@@ -7123,7 +7227,7 @@ impl Editor {
buffer.edit(edits.iter().cloned(), None, cx)
});
- self.change_selections(None, window, cx, |s| {
+ self.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
s.select_anchor_ranges([last_edit_end..last_edit_end]);
});
@@ -7170,9 +7274,14 @@ impl Editor {
match &active_inline_completion.completion {
InlineCompletion::Move { target, .. } => {
let target = *target;
- self.change_selections(Some(Autoscroll::newest()), window, cx, |selections| {
- selections.select_anchor_ranges([target..target]);
- });
+ self.change_selections(
+ SelectionEffects::scroll(Autoscroll::newest()),
+ window,
+ cx,
+ |selections| {
+ selections.select_anchor_ranges([target..target]);
+ },
+ );
}
InlineCompletion::Edit { edits, .. } => {
// Find an insertion that starts at the cursor position.
@@ -7773,9 +7882,12 @@ impl Editor {
this.entry("Run to cursor", None, move |window, cx| {
weak_editor
.update(cx, |editor, cx| {
- editor.change_selections(None, window, cx, |s| {
- s.select_ranges([Point::new(row, 0)..Point::new(row, 0)])
- });
+ editor.change_selections(
+ SelectionEffects::no_scroll(),
+ window,
+ cx,
+ |s| s.select_ranges([Point::new(row, 0)..Point::new(row, 0)]),
+ );
})
.ok();
@@ -9316,7 +9428,7 @@ impl Editor {
.collect::>()
});
if let Some(tabstop) = tabstops.first() {
- self.change_selections(Some(Autoscroll::fit()), window, cx, |s| {
+ self.change_selections(Default::default(), window, cx, |s| {
// Reverse order so that the first range is the newest created selection.
// Completions will use it and autoscroll will prioritize it.
s.select_ranges(tabstop.ranges.iter().rev().cloned());
@@ -9434,7 +9546,7 @@ impl Editor {
}
}
if let Some(current_ranges) = snippet.ranges.get(snippet.active_index) {
- self.change_selections(Some(Autoscroll::fit()), window, cx, |s| {
+ self.change_selections(Default::default(), window, cx, |s| {
// Reverse order so that the first range is the newest created selection.
// Completions will use it and autoscroll will prioritize it.
s.select_ranges(current_ranges.iter().rev().cloned())
@@ -9524,9 +9636,7 @@ impl Editor {
}
}
- this.change_selections(Some(Autoscroll::fit()), window, cx, |s| {
- s.select(selections)
- });
+ this.change_selections(Default::default(), window, cx, |s| s.select(selections));
this.insert("", window, cx);
let empty_str: Arc = Arc::from("");
for (buffer, edits) in linked_ranges {
@@ -9562,7 +9672,7 @@ impl Editor {
pub fn delete(&mut self, _: &Delete, window: &mut Window, cx: &mut Context) {
self.hide_mouse_cursor(HideMouseCursorOrigin::TypingAction, cx);
self.transact(window, cx, |this, window, cx| {
- this.change_selections(Some(Autoscroll::fit()), window, cx, |s| {
+ this.change_selections(Default::default(), window, cx, |s| {
s.move_with(|map, selection| {
if selection.is_empty() {
let cursor = movement::right(map, selection.head());
@@ -9705,9 +9815,7 @@ impl Editor {
self.transact(window, cx, |this, window, cx| {
this.buffer.update(cx, |b, cx| b.edit(edits, None, cx));
- this.change_selections(Some(Autoscroll::fit()), window, cx, |s| {
- s.select(selections)
- });
+ this.change_selections(Default::default(), window, cx, |s| s.select(selections));
this.refresh_inline_completion(true, false, window, cx);
});
}
@@ -9740,9 +9848,7 @@ impl Editor {
self.transact(window, cx, |this, window, cx| {
this.buffer.update(cx, |b, cx| b.edit(edits, None, cx));
- this.change_selections(Some(Autoscroll::fit()), window, cx, |s| {
- s.select(selections)
- });
+ this.change_selections(Default::default(), window, cx, |s| s.select(selections));
});
}
@@ -9895,9 +10001,7 @@ impl Editor {
);
});
let selections = this.selections.all::(cx);
- this.change_selections(Some(Autoscroll::fit()), window, cx, |s| {
- s.select(selections)
- });
+ this.change_selections(Default::default(), window, cx, |s| s.select(selections));
});
}
@@ -9922,9 +10026,7 @@ impl Editor {
buffer.autoindent_ranges(selections, cx);
});
let selections = this.selections.all::(cx);
- this.change_selections(Some(Autoscroll::fit()), window, cx, |s| {
- s.select(selections)
- });
+ this.change_selections(Default::default(), window, cx, |s| s.select(selections));
});
}
@@ -10005,7 +10107,7 @@ impl Editor {
})
.collect();
- this.change_selections(Some(Autoscroll::fit()), window, cx, |s| {
+ this.change_selections(Default::default(), window, cx, |s| {
s.select(new_selections);
});
});
@@ -10071,7 +10173,7 @@ impl Editor {
}
}
- this.change_selections(Some(Autoscroll::fit()), window, cx, |s| {
+ this.change_selections(Default::default(), window, cx, |s| {
s.select_anchor_ranges(cursor_positions)
});
});
@@ -10658,7 +10760,7 @@ impl Editor {
})
.collect();
- this.change_selections(Some(Autoscroll::fit()), window, cx, |s| {
+ this.change_selections(Default::default(), window, cx, |s| {
s.select(new_selections);
});
@@ -11009,7 +11111,7 @@ impl Editor {
buffer.edit(edits, None, cx);
});
- this.change_selections(Some(Autoscroll::fit()), window, cx, |s| {
+ this.change_selections(Default::default(), window, cx, |s| {
s.select(new_selections);
});
@@ -11045,7 +11147,7 @@ impl Editor {
this.buffer.update(cx, |buffer, cx| {
buffer.edit(edits, None, cx);
});
- this.change_selections(Some(Autoscroll::fit()), window, cx, |s| {
+ this.change_selections(Default::default(), window, cx, |s| {
s.select_anchor_ranges([last_edit_start..last_edit_end]);
});
});
@@ -11247,7 +11349,7 @@ impl Editor {
}
});
this.fold_creases(refold_creases, true, window, cx);
- this.change_selections(Some(Autoscroll::fit()), window, cx, |s| {
+ this.change_selections(Default::default(), window, cx, |s| {
s.select(new_selections);
})
});
@@ -11348,9 +11450,7 @@ impl Editor {
}
});
this.fold_creases(refold_creases, true, window, cx);
- this.change_selections(Some(Autoscroll::fit()), window, cx, |s| {
- s.select(new_selections)
- });
+ this.change_selections(Default::default(), window, cx, |s| s.select(new_selections));
});
}
@@ -11358,7 +11458,7 @@ impl Editor {
self.hide_mouse_cursor(HideMouseCursorOrigin::TypingAction, cx);
let text_layout_details = &self.text_layout_details(window);
self.transact(window, cx, |this, window, cx| {
- let edits = this.change_selections(Some(Autoscroll::fit()), window, cx, |s| {
+ let edits = this.change_selections(Default::default(), window, cx, |s| {
let mut edits: Vec<(Range, String)> = Default::default();
s.move_with(|display_map, selection| {
if !selection.is_empty() {
@@ -11406,7 +11506,7 @@ impl Editor {
this.buffer
.update(cx, |buffer, cx| buffer.edit(edits, None, cx));
let selections = this.selections.all::(cx);
- this.change_selections(Some(Autoscroll::fit()), window, cx, |s| {
+ this.change_selections(Default::default(), window, cx, |s| {
s.select(selections);
});
});
@@ -11426,42 +11526,106 @@ impl Editor {
let buffer = self.buffer.read(cx).snapshot(cx);
let selections = self.selections.all::(cx);
- // Shrink and split selections to respect paragraph boundaries.
- let ranges = selections.into_iter().flat_map(|selection| {
+ // Split selections to respect paragraph, indent, and comment prefix boundaries.
+ let wrap_ranges = selections.into_iter().flat_map(|selection| {
+ let mut non_blank_rows_iter = (selection.start.row..=selection.end.row)
+ .filter(|row| !buffer.is_line_blank(MultiBufferRow(*row)))
+ .peekable();
+
+ let first_row = if let Some(&row) = non_blank_rows_iter.peek() {
+ row
+ } else {
+ return Vec::new();
+ };
+
let language_settings = buffer.language_settings_at(selection.head(), cx);
let language_scope = buffer.language_scope_at(selection.head());
- let Some(start_row) = (selection.start.row..=selection.end.row)
- .find(|row| !buffer.is_line_blank(MultiBufferRow(*row)))
- else {
- return vec![];
- };
- let Some(end_row) = (selection.start.row..=selection.end.row)
- .rev()
- .find(|row| !buffer.is_line_blank(MultiBufferRow(*row)))
- else {
- return vec![];
- };
+ let indent_and_prefix_for_row =
+ |row: u32| -> (IndentSize, Option, Option) {
+ let indent = buffer.indent_size_for_line(MultiBufferRow(row));
+ let (comment_prefix, rewrap_prefix) =
+ if let Some(language_scope) = &language_scope {
+ let indent_end = Point::new(row, indent.len);
+ let comment_prefix = language_scope
+ .line_comment_prefixes()
+ .iter()
+ .find(|prefix| buffer.contains_str_at(indent_end, prefix))
+ .map(|prefix| prefix.to_string());
+ let line_end = Point::new(row, buffer.line_len(MultiBufferRow(row)));
+ let line_text_after_indent = buffer
+ .text_for_range(indent_end..line_end)
+ .collect::();
+ let rewrap_prefix = language_scope
+ .rewrap_prefixes()
+ .iter()
+ .find_map(|prefix_regex| {
+ prefix_regex.find(&line_text_after_indent).map(|mat| {
+ if mat.start() == 0 {
+ Some(mat.as_str().to_string())
+ } else {
+ None
+ }
+ })
+ })
+ .flatten();
+ (comment_prefix, rewrap_prefix)
+ } else {
+ (None, None)
+ };
+ (indent, comment_prefix, rewrap_prefix)
+ };
- let mut row = start_row;
let mut ranges = Vec::new();
- while let Some(blank_row) =
- (row..end_row).find(|row| buffer.is_line_blank(MultiBufferRow(*row)))
- {
- let next_paragraph_start = (blank_row + 1..=end_row)
- .find(|row| !buffer.is_line_blank(MultiBufferRow(*row)))
- .unwrap();
- ranges.push((
- language_settings.clone(),
- language_scope.clone(),
- Point::new(row, 0)..Point::new(blank_row - 1, 0),
- ));
- row = next_paragraph_start;
+ let from_empty_selection = selection.is_empty();
+
+ let mut current_range_start = first_row;
+ let mut prev_row = first_row;
+ let (
+ mut current_range_indent,
+ mut current_range_comment_prefix,
+ mut current_range_rewrap_prefix,
+ ) = indent_and_prefix_for_row(first_row);
+
+ for row in non_blank_rows_iter.skip(1) {
+ let has_paragraph_break = row > prev_row + 1;
+
+ let (row_indent, row_comment_prefix, row_rewrap_prefix) =
+ indent_and_prefix_for_row(row);
+
+ let has_indent_change = row_indent != current_range_indent;
+ let has_comment_change = row_comment_prefix != current_range_comment_prefix;
+
+ let has_boundary_change = has_comment_change
+ || row_rewrap_prefix.is_some()
+ || (has_indent_change && current_range_comment_prefix.is_some());
+
+ if has_paragraph_break || has_boundary_change {
+ ranges.push((
+ language_settings.clone(),
+ Point::new(current_range_start, 0)
+ ..Point::new(prev_row, buffer.line_len(MultiBufferRow(prev_row))),
+ current_range_indent,
+ current_range_comment_prefix.clone(),
+ current_range_rewrap_prefix.clone(),
+ from_empty_selection,
+ ));
+ current_range_start = row;
+ current_range_indent = row_indent;
+ current_range_comment_prefix = row_comment_prefix;
+ current_range_rewrap_prefix = row_rewrap_prefix;
+ }
+ prev_row = row;
}
+
ranges.push((
language_settings.clone(),
- language_scope.clone(),
- Point::new(row, 0)..Point::new(end_row, 0),
+ Point::new(current_range_start, 0)
+ ..Point::new(prev_row, buffer.line_len(MultiBufferRow(prev_row))),
+ current_range_indent,
+ current_range_comment_prefix,
+ current_range_rewrap_prefix,
+ from_empty_selection,
));
ranges
@@ -11470,9 +11634,17 @@ impl Editor {
let mut edits = Vec::new();
let mut rewrapped_row_ranges = Vec::>::new();
- for (language_settings, language_scope, range) in ranges {
- let mut start_row = range.start.row;
- let mut end_row = range.end.row;
+ for (
+ language_settings,
+ wrap_range,
+ indent_size,
+ comment_prefix,
+ rewrap_prefix,
+ from_empty_selection,
+ ) in wrap_ranges
+ {
+ let mut start_row = wrap_range.start.row;
+ let mut end_row = wrap_range.end.row;
// Skip selections that overlap with a range that has already been rewrapped.
let selection_range = start_row..end_row;
@@ -11485,49 +11657,20 @@ impl Editor {
let tab_size = language_settings.tab_size;
- // Since not all lines in the selection may be at the same indent
- // level, choose the indent size that is the most common between all
- // of the lines.
- //
- // If there is a tie, we use the deepest indent.
- let (indent_size, indent_end) = {
- let mut indent_size_occurrences = HashMap::default();
- let mut rows_by_indent_size = HashMap::>::default();
-
- for row in start_row..=end_row {
- let indent = buffer.indent_size_for_line(MultiBufferRow(row));
- rows_by_indent_size.entry(indent).or_default().push(row);
- *indent_size_occurrences.entry(indent).or_insert(0) += 1;
- }
-
- let indent_size = indent_size_occurrences
- .into_iter()
- .max_by_key(|(indent, count)| (*count, indent.len_with_expanded_tabs(tab_size)))
- .map(|(indent, _)| indent)
- .unwrap_or_default();
- let row = rows_by_indent_size[&indent_size][0];
- let indent_end = Point::new(row, indent_size.len);
-
- (indent_size, indent_end)
- };
-
- let mut line_prefix = indent_size.chars().collect::();
-
+ let indent_prefix = indent_size.chars().collect::();
+ let mut line_prefix = indent_prefix.clone();
let mut inside_comment = false;
- if let Some(comment_prefix) = language_scope.and_then(|language| {
- language
- .line_comment_prefixes()
- .iter()
- .find(|prefix| buffer.contains_str_at(indent_end, prefix))
- .cloned()
- }) {
- line_prefix.push_str(&comment_prefix);
+ if let Some(prefix) = &comment_prefix {
+ line_prefix.push_str(prefix);
inside_comment = true;
}
+ if let Some(prefix) = &rewrap_prefix {
+ line_prefix.push_str(prefix);
+ }
let allow_rewrap_based_on_language = match language_settings.allow_rewrap {
RewrapBehavior::InComments => inside_comment,
- RewrapBehavior::InSelections => !range.is_empty(),
+ RewrapBehavior::InSelections => !wrap_range.is_empty(),
RewrapBehavior::Anywhere => true,
};
@@ -11538,7 +11681,7 @@ impl Editor {
continue;
}
- if range.is_empty() {
+ if from_empty_selection {
'expand_upwards: while start_row > 0 {
let prev_row = start_row - 1;
if buffer.contains_str_at(Point::new(prev_row, 0), &line_prefix)
@@ -11570,12 +11713,18 @@ impl Editor {
let selection_text = buffer.text_for_range(start..end).collect::();
let Some(lines_without_prefixes) = selection_text
.lines()
- .map(|line| {
- line.strip_prefix(&line_prefix)
- .or_else(|| line.trim_start().strip_prefix(&line_prefix.trim_start()))
- .with_context(|| {
- format!("line did not start with prefix {line_prefix:?}: {line:?}")
- })
+ .enumerate()
+ .map(|(ix, line)| {
+ let line_trimmed = line.trim_start();
+ if rewrap_prefix.is_some() && ix > 0 {
+ Ok(line_trimmed)
+ } else {
+ line_trimmed
+ .strip_prefix(&line_prefix.trim_start())
+ .with_context(|| {
+ format!("line did not start with prefix {line_prefix:?}: {line:?}")
+ })
+ }
})
.collect::, _>>()
.log_err()
@@ -11588,8 +11737,16 @@ impl Editor {
.language_settings_at(Point::new(start_row, 0), cx)
.preferred_line_length as usize
});
+
+ let subsequent_lines_prefix = if let Some(rewrap_prefix_str) = &rewrap_prefix {
+ format!("{}{}", indent_prefix, " ".repeat(rewrap_prefix_str.len()))
+ } else {
+ line_prefix.clone()
+ };
+
let wrapped_text = wrap_with_prefix(
line_prefix,
+ subsequent_lines_prefix,
lines_without_prefixes.join("\n"),
wrap_column,
tab_size,
@@ -11662,7 +11819,7 @@ impl Editor {
}
self.transact(window, cx, |this, window, cx| {
- this.change_selections(Some(Autoscroll::fit()), window, cx, |s| {
+ this.change_selections(Default::default(), window, cx, |s| {
s.select(selections);
});
this.insert("", window, cx);
@@ -11678,7 +11835,7 @@ impl Editor {
pub fn kill_ring_cut(&mut self, _: &KillRingCut, window: &mut Window, cx: &mut Context) {
self.hide_mouse_cursor(HideMouseCursorOrigin::TypingAction, cx);
- self.change_selections(None, window, cx, |s| {
+ self.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
s.move_with(|snapshot, sel| {
if sel.is_empty() {
sel.end = DisplayPoint::new(sel.end.row(), snapshot.line_len(sel.end.row()))
@@ -11882,9 +12039,7 @@ impl Editor {
});
let selections = this.selections.all::(cx);
- this.change_selections(Some(Autoscroll::fit()), window, cx, |s| {
- s.select(selections)
- });
+ this.change_selections(Default::default(), window, cx, |s| s.select(selections));
} else {
this.insert(&clipboard_text, window, cx);
}
@@ -11923,7 +12078,7 @@ impl Editor {
if let Some((selections, _)) =
self.selection_history.transaction(transaction_id).cloned()
{
- self.change_selections(None, window, cx, |s| {
+ self.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
s.select_anchors(selections.to_vec());
});
} else {
@@ -11953,7 +12108,7 @@ impl Editor {
if let Some((_, Some(selections))) =
self.selection_history.transaction(transaction_id).cloned()
{
- self.change_selections(None, window, cx, |s| {
+ self.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
s.select_anchors(selections.to_vec());
});
} else {
@@ -11983,7 +12138,7 @@ impl Editor {
pub fn move_left(&mut self, _: &MoveLeft, window: &mut Window, cx: &mut Context) {
self.hide_mouse_cursor(HideMouseCursorOrigin::MovementAction, cx);
- self.change_selections(Some(Autoscroll::fit()), window, cx, |s| {
+ self.change_selections(Default::default(), window, cx, |s| {
s.move_with(|map, selection| {
let cursor = if selection.is_empty() {
movement::left(map, selection.start)
@@ -11997,14 +12152,14 @@ impl Editor {
pub fn select_left(&mut self, _: &SelectLeft, window: &mut Window, cx: &mut Context) {
self.hide_mouse_cursor(HideMouseCursorOrigin::MovementAction, cx);
- self.change_selections(Some(Autoscroll::fit()), window, cx, |s| {
+ self.change_selections(Default::default(), window, cx, |s| {
s.move_heads_with(|map, head, _| (movement::left(map, head), SelectionGoal::None));
})
}
pub fn move_right(&mut self, _: &MoveRight, window: &mut Window, cx: &mut Context) {
self.hide_mouse_cursor(HideMouseCursorOrigin::MovementAction, cx);
- self.change_selections(Some(Autoscroll::fit()), window, cx, |s| {
+ self.change_selections(Default::default(), window, cx, |s| {
s.move_with(|map, selection| {
let cursor = if selection.is_empty() {
movement::right(map, selection.end)
@@ -12018,7 +12173,7 @@ impl Editor {
pub fn select_right(&mut self, _: &SelectRight, window: &mut Window, cx: &mut Context) {
self.hide_mouse_cursor(HideMouseCursorOrigin::MovementAction, cx);
- self.change_selections(Some(Autoscroll::fit()), window, cx, |s| {
+ self.change_selections(Default::default(), window, cx, |s| {
s.move_heads_with(|map, head, _| (movement::right(map, head), SelectionGoal::None));
})
}
@@ -12039,7 +12194,7 @@ impl Editor {
let selection_count = self.selections.count();
let first_selection = self.selections.first_anchor();
- self.change_selections(Some(Autoscroll::fit()), window, cx, |s| {
+ self.change_selections(Default::default(), window, cx, |s| {
s.move_with(|map, selection| {
if !selection.is_empty() {
selection.goal = SelectionGoal::None;
@@ -12080,7 +12235,7 @@ impl Editor {
let text_layout_details = &self.text_layout_details(window);
- self.change_selections(Some(Autoscroll::fit()), window, cx, |s| {
+ self.change_selections(Default::default(), window, cx, |s| {
s.move_with(|map, selection| {
if !selection.is_empty() {
selection.goal = SelectionGoal::None;
@@ -12117,7 +12272,7 @@ impl Editor {
let text_layout_details = &self.text_layout_details(window);
- self.change_selections(Some(Autoscroll::fit()), window, cx, |s| {
+ self.change_selections(Default::default(), window, cx, |s| {
s.move_with(|map, selection| {
if !selection.is_empty() {
selection.goal = SelectionGoal::None;
@@ -12143,7 +12298,7 @@ impl Editor {
) {
self.hide_mouse_cursor(HideMouseCursorOrigin::MovementAction, cx);
let text_layout_details = &self.text_layout_details(window);
- self.change_selections(Some(Autoscroll::fit()), window, cx, |s| {
+ self.change_selections(Default::default(), window, cx, |s| {
s.move_heads_with(|map, head, goal| {
movement::down_by_rows(map, head, action.lines, goal, false, text_layout_details)
})
@@ -12158,7 +12313,7 @@ impl Editor {
) {
self.hide_mouse_cursor(HideMouseCursorOrigin::MovementAction, cx);
let text_layout_details = &self.text_layout_details(window);
- self.change_selections(Some(Autoscroll::fit()), window, cx, |s| {
+ self.change_selections(Default::default(), window, cx, |s| {
s.move_heads_with(|map, head, goal| {
movement::up_by_rows(map, head, action.lines, goal, false, text_layout_details)
})
@@ -12179,7 +12334,7 @@ impl Editor {
let text_layout_details = &self.text_layout_details(window);
- self.change_selections(Some(Autoscroll::fit()), window, cx, |s| {
+ self.change_selections(Default::default(), window, cx, |s| {
s.move_heads_with(|map, head, goal| {
movement::up_by_rows(map, head, row_count, goal, false, text_layout_details)
})
@@ -12217,15 +12372,15 @@ impl Editor {
self.hide_mouse_cursor(HideMouseCursorOrigin::MovementAction, cx);
- let autoscroll = if action.center_cursor {
- Autoscroll::center()
+ let effects = if action.center_cursor {
+ SelectionEffects::scroll(Autoscroll::center())
} else {
- Autoscroll::fit()
+ SelectionEffects::default()
};
let text_layout_details = &self.text_layout_details(window);
- self.change_selections(Some(autoscroll), window, cx, |s| {
+ self.change_selections(effects, window, cx, |s| {
s.move_with(|map, selection| {
if !selection.is_empty() {
selection.goal = SelectionGoal::None;
@@ -12246,7 +12401,7 @@ impl Editor {
pub fn select_up(&mut self, _: &SelectUp, window: &mut Window, cx: &mut Context) {
self.hide_mouse_cursor(HideMouseCursorOrigin::MovementAction, cx);
let text_layout_details = &self.text_layout_details(window);
- self.change_selections(Some(Autoscroll::fit()), window, cx, |s| {
+ self.change_selections(Default::default(), window, cx, |s| {
s.move_heads_with(|map, head, goal| {
movement::up(map, head, goal, false, text_layout_details)
})
@@ -12267,7 +12422,7 @@ impl Editor {
let selection_count = self.selections.count();
let first_selection = self.selections.first_anchor();
- self.change_selections(Some(Autoscroll::fit()), window, cx, |s| {
+ self.change_selections(Default::default(), window, cx, |s| {
s.move_with(|map, selection| {
if !selection.is_empty() {
selection.goal = SelectionGoal::None;
@@ -12303,7 +12458,7 @@ impl Editor {
let text_layout_details = &self.text_layout_details(window);
- self.change_selections(Some(Autoscroll::fit()), window, cx, |s| {
+ self.change_selections(Default::default(), window, cx, |s| {
s.move_heads_with(|map, head, goal| {
movement::down_by_rows(map, head, row_count, goal, false, text_layout_details)
})
@@ -12341,14 +12496,14 @@ impl Editor {
self.hide_mouse_cursor(HideMouseCursorOrigin::MovementAction, cx);
- let autoscroll = if action.center_cursor {
- Autoscroll::center()
+ let effects = if action.center_cursor {
+ SelectionEffects::scroll(Autoscroll::center())
} else {
- Autoscroll::fit()
+ SelectionEffects::default()
};
let text_layout_details = &self.text_layout_details(window);
- self.change_selections(Some(autoscroll), window, cx, |s| {
+ self.change_selections(effects, window, cx, |s| {
s.move_with(|map, selection| {
if !selection.is_empty() {
selection.goal = SelectionGoal::None;
@@ -12369,7 +12524,7 @@ impl Editor {
pub fn select_down(&mut self, _: &SelectDown, window: &mut Window, cx: &mut Context) {
self.hide_mouse_cursor(HideMouseCursorOrigin::MovementAction, cx);
let text_layout_details = &self.text_layout_details(window);
- self.change_selections(Some(Autoscroll::fit()), window, cx, |s| {
+ self.change_selections(Default::default(), window, cx, |s| {
s.move_heads_with(|map, head, goal| {
movement::down(map, head, goal, false, text_layout_details)
})
@@ -12427,7 +12582,7 @@ impl Editor {
cx: &mut Context,
) {
self.hide_mouse_cursor(HideMouseCursorOrigin::MovementAction, cx);
- self.change_selections(Some(Autoscroll::fit()), window, cx, |s| {
+ self.change_selections(Default::default(), window, cx, |s| {
s.move_cursors_with(|map, head, _| {
(
movement::previous_word_start(map, head),
@@ -12444,7 +12599,7 @@ impl Editor {
cx: &mut Context,
) {
self.hide_mouse_cursor(HideMouseCursorOrigin::MovementAction, cx);
- self.change_selections(Some(Autoscroll::fit()), window, cx, |s| {
+ self.change_selections(Default::default(), window, cx, |s| {
s.move_cursors_with(|map, head, _| {
(
movement::previous_subword_start(map, head),
@@ -12461,7 +12616,7 @@ impl Editor {
cx: &mut Context,
) {
self.hide_mouse_cursor(HideMouseCursorOrigin::MovementAction, cx);
- self.change_selections(Some(Autoscroll::fit()), window, cx, |s| {
+ self.change_selections(Default::default(), window, cx, |s| {
s.move_heads_with(|map, head, _| {
(
movement::previous_word_start(map, head),
@@ -12478,7 +12633,7 @@ impl Editor {
cx: &mut Context,
) {
self.hide_mouse_cursor(HideMouseCursorOrigin::MovementAction, cx);
- self.change_selections(Some(Autoscroll::fit()), window, cx, |s| {
+ self.change_selections(Default::default(), window, cx, |s| {
s.move_heads_with(|map, head, _| {
(
movement::previous_subword_start(map, head),
@@ -12497,7 +12652,7 @@ impl Editor {
self.hide_mouse_cursor(HideMouseCursorOrigin::TypingAction, cx);
self.transact(window, cx, |this, window, cx| {
this.select_autoclose_pair(window, cx);
- this.change_selections(Some(Autoscroll::fit()), window, cx, |s| {
+ this.change_selections(Default::default(), window, cx, |s| {
s.move_with(|map, selection| {
if selection.is_empty() {
let cursor = if action.ignore_newlines {
@@ -12522,7 +12677,7 @@ impl Editor {
self.hide_mouse_cursor(HideMouseCursorOrigin::TypingAction, cx);
self.transact(window, cx, |this, window, cx| {
this.select_autoclose_pair(window, cx);
- this.change_selections(Some(Autoscroll::fit()), window, cx, |s| {
+ this.change_selections(Default::default(), window, cx, |s| {
s.move_with(|map, selection| {
if selection.is_empty() {
let cursor = movement::previous_subword_start(map, selection.head());
@@ -12541,7 +12696,7 @@ impl Editor {
cx: &mut Context,
) {
self.hide_mouse_cursor(HideMouseCursorOrigin::MovementAction, cx);
- self.change_selections(Some(Autoscroll::fit()), window, cx, |s| {
+ self.change_selections(Default::default(), window, cx, |s| {
s.move_cursors_with(|map, head, _| {
(movement::next_word_end(map, head), SelectionGoal::None)
});
@@ -12555,7 +12710,7 @@ impl Editor {
cx: &mut Context,
) {
self.hide_mouse_cursor(HideMouseCursorOrigin::MovementAction, cx);
- self.change_selections(Some(Autoscroll::fit()), window, cx, |s| {
+ self.change_selections(Default::default(), window, cx, |s| {
s.move_cursors_with(|map, head, _| {
(movement::next_subword_end(map, head), SelectionGoal::None)
});
@@ -12569,7 +12724,7 @@ impl Editor {
cx: &mut Context,
) {
self.hide_mouse_cursor(HideMouseCursorOrigin::MovementAction, cx);
- self.change_selections(Some(Autoscroll::fit()), window, cx, |s| {
+ self.change_selections(Default::default(), window, cx, |s| {
s.move_heads_with(|map, head, _| {
(movement::next_word_end(map, head), SelectionGoal::None)
});
@@ -12583,7 +12738,7 @@ impl Editor {
cx: &mut Context,
) {
self.hide_mouse_cursor(HideMouseCursorOrigin::MovementAction, cx);
- self.change_selections(Some(Autoscroll::fit()), window, cx, |s| {
+ self.change_selections(Default::default(), window, cx, |s| {
s.move_heads_with(|map, head, _| {
(movement::next_subword_end(map, head), SelectionGoal::None)
});
@@ -12598,7 +12753,7 @@ impl Editor {
) {
self.hide_mouse_cursor(HideMouseCursorOrigin::TypingAction, cx);
self.transact(window, cx, |this, window, cx| {
- this.change_selections(Some(Autoscroll::fit()), window, cx, |s| {
+ this.change_selections(Default::default(), window, cx, |s| {
s.move_with(|map, selection| {
if selection.is_empty() {
let cursor = if action.ignore_newlines {
@@ -12622,7 +12777,7 @@ impl Editor {
) {
self.hide_mouse_cursor(HideMouseCursorOrigin::TypingAction, cx);
self.transact(window, cx, |this, window, cx| {
- this.change_selections(Some(Autoscroll::fit()), window, cx, |s| {
+ this.change_selections(Default::default(), window, cx, |s| {
s.move_with(|map, selection| {
if selection.is_empty() {
let cursor = movement::next_subword_end(map, selection.head());
@@ -12641,7 +12796,7 @@ impl Editor {
cx: &mut Context