Compare commits

...

17 Commits

Author SHA1 Message Date
cameron
6ee1916965 Merge remote-tracking branch 'origin/main' into cnext-cprev 2025-12-16 15:07:51 +00:00
Nereuxofficial
da0960bab6 languages: Correctly calculate ranges in label_for_completion (#44925)
Closes #44825

Release Notes:

- Fixed a case where an incorrect match could be generated in
label_for_completion

---------

Co-authored-by: Kirill Bulatov <mail4score@gmail.com>
2025-12-16 14:51:33 +00:00
Finn Evers
81519ae923 collab: Add copilot name alias to the GET /contributor endpoint (#44958)
Although the copilot bot integration is referred to by
`copilot-swe-agent[bot]`
(https://api.github.com/users/copilot-swe-agent[bot]), GitHub parses the
copilot identity as @\copilot in some cases, e.g.
https://github.com/zed-industries/zed/pull/44915#issuecomment-3657567754.
This causes the CLA check to still fail despite Copilot being added to
the CLA endpoint (and https://api.github.com/users/copilot returning a
404 for that very name..).

This PR fixes this by also considering the name alias of Copilot for the
`contributor` endpoint.

Release Notes:

- N/A
2025-12-16 15:44:30 +01:00
Danilo Leal
5f054e8d9c agent_ui: Create components for the model selector (#44993)
This PR introduces a few components for the model selector pickers.
Given we're still maintaining two flavors of it due to one of them being
wired through ACP and the other through the language model registry,
having one source of truth for the UI should help with maintenance
moving forward, considering that despite the internal differences, they
look and behave the same from the standpoint of the UI.

Release Notes:

- N/A
2025-12-16 11:34:20 -03:00
Danilo Leal
37e4f7e9b5 agent_ui: Remove custom "unavailable editing" tooltip (#44992)
Now that we can use `Tooltip::element`, we don't need a separate
file/component just for this.

Release Notes:

- N/A
2025-12-16 10:50:52 -03:00
Smit Barmase
5f451c89e0 markdown: Fix double borders in Markdown and Markdown Preview tables (#44991)
Improves upon https://github.com/zed-industries/zed/pull/42674

Before:

<img width="520" height="202" alt="image"
src="https://github.com/user-attachments/assets/efb1650b-4c0e-4424-8d9b-90de80c72df2"
/> <img width="157" height="211" alt="image"
src="https://github.com/user-attachments/assets/cf4605f3-88e5-4724-ad2b-1219ed04a945"
/>

After:

<img width="529" height="208" alt="image"
src="https://github.com/user-attachments/assets/382fd523-a3d9-4700-a8df-c339419fc6dc"
/>
<img width="133" height="208" alt="image"
src="https://github.com/user-attachments/assets/f22b72d9-d416-47f9-92af-ea1de6fb5583"
/>



Release Notes:

- Fixed an issue where Markdown tables would sometimes show double
borders.
2025-12-16 19:18:52 +05:30
Daiki Takagi
0362e301f7 acp_thread: Decode file:// mention paths so non-ASCII names render correctly (#44983)
## Summary

This fixes a minor bug I found #44981 

- Fix percent-encoded filenames appearing in agent mentions after
message submission.
- Decode file:// paths in MentionUri::parse using the existing
urlencoding crate (already used elsewhere in the codebase).
- Add tests for non-ASCII file URIs.

## Screenshots

<img width="409" height="116" alt="image"
src="https://github.com/user-attachments/assets/32ef033b-6232-47c5-80c7-d5247d5dae88"
/>
2025-12-16 13:43:23 +00:00
lif
37bd27b2a8 diagnostics: Respect toolbar breadcrumbs setting in diagnostics panel (#44974)
## Summary

The diagnostics panel was ignoring the user's `toolbar.breadcrumbs`
setting and always showing breadcrumbs. This makes both
`BufferDiagnosticsEditor` and `ProjectDiagnosticsEditor` check the
`EditorSettings` to determine whether to display breadcrumbs.

## Changes

- `buffer_diagnostics.rs`: Updated `breadcrumb_location` to check
`EditorSettings::get_global(cx).toolbar.breadcrumbs`
- `diagnostics.rs`: Updated `breadcrumb_location` to check
`EditorSettings::get_global(cx).toolbar.breadcrumbs`

This follows the same pattern used by the regular `Editor` in
`items.rs`.

## Test plan

1. Set `toolbar.breadcrumbs` to `false` in settings.json
2. Open a file with diagnostics
3. Run `diagnostics: deploy current file`
4. Verify that breadcrumbs are hidden in the diagnostics panel

Fixes #43020
2025-12-16 13:05:31 +00:00
Danilo Leal
775548e93c ui: Fix Divider component growing unnecessarily (#44986)
I had previously added `flex_none` to the Divider and that caused it to
grow beyond the container's width in some cases (project panel, agent
panel's restore to check point button, etc.).

Release Notes:

- N/A
2025-12-16 12:55:26 +00:00
Danilo Leal
90d7ccfd5d agent_ui: Search models only by name (#44984)
We were previously matching the search on both model name and provider
ID. In most cases, this would yield an okay result, but if you search
for "Opus", for example, you'd see the Sonnet models in the search
result, which was very confusing. This was because we were matching to
both provider ID and model name. "Sonnet" and "Opus" share the same
provider ID, so they both contain "Anthropic" as a prefix. Then, "Opus"
contains the letter P, as well as Anthropic, thus the match.

Now, we're only matching by model name, which I think most of the time
will yield more accurate results.

Release Notes:

- agent: Improved the model search quality in the model picker.
2025-12-16 12:46:08 +00:00
Smit Barmase
68295ba371 markdown: Fix Markdown table not rendering in hover popover (#44712)
Closes #44306

This PR makes two changes:
- Uses the new `grid_cols_min_content` API. See more here:
https://github.com/zed-industries/zed/pull/44973.
- Changes Markdown table rendering to use a single grid instead of
creating a new grid per row, so column widths stay consistent across
rows.

Release Notes:

- Fixed an issue where Markdown tables wouldn't render in the hover
popover.
2025-12-16 18:11:06 +05:30
Lukas Wirth
5152fd898e agent_ui: Add scroll to most recent user prompt button (#44961)
Release Notes:

- Added a button to the agent thread view that scrolls to the most
recent prompt
2025-12-16 13:35:47 +01:00
Danilo Leal
4e482288cb agent_ui: Add keybinding to cycle through profiles (#44979)
Similar to the mode selector in external agents, it will now be possible
to use `shift-tab` to cycle through profiles.

<img width="500" height="384" alt="Screenshot 2025-12-16 at 9  04@2x"
src="https://github.com/user-attachments/assets/11e8824e-9fad-4aab-9e19-53878096db52"
/>

Release Notes:

- Added the ability to use `shift-tab` to cycle through profiles for the
built-in Zed agent.
2025-12-16 09:15:08 -03:00
Danilo Leal
30deb22ab7 agent_ui: Add the ability to delete a profile through the UI (#44977)
It was only possible to delete profiles through the `settings.json`, but
now you can do it through the UI:

<img width="500" height="1954" alt="Screenshot 2025-12-16 at 8  42@2x"
src="https://github.com/user-attachments/assets/077ecdf5-1e80-4b70-86c9-177cc3741e77"
/>

Release Notes:

- agent: Added the ability to delete a profile through the "Manage
Profiles" modal.
2025-12-16 09:04:07 -03:00
cameron
27c8e84535 add diagnostics multibuffer support 2025-12-10 11:34:40 +00:00
cameron
8aa19a11ac vim :cnext and :cprev support and project search integration 2025-12-09 15:48:39 +00:00
cameron
4e705e5b5d [wip] initial implementation 2025-12-09 13:18:36 +00:00
29 changed files with 830 additions and 251 deletions

2
Cargo.lock generated
View File

@@ -37,6 +37,7 @@ dependencies = [
"terminal",
"ui",
"url",
"urlencoding",
"util",
"uuid",
"watch",
@@ -4809,6 +4810,7 @@ dependencies = [
"log",
"lsp",
"markdown",
"multi_buffer",
"pretty_assertions",
"project",
"rand 0.9.2",

View File

@@ -46,6 +46,7 @@ url.workspace = true
util.workspace = true
uuid.workspace = true
watch.workspace = true
urlencoding.workspace = true
[dev-dependencies]
env_logger.workspace = true

View File

@@ -4,12 +4,14 @@ use file_icons::FileIcons;
use prompt_store::{PromptId, UserPromptId};
use serde::{Deserialize, Serialize};
use std::{
borrow::Cow,
fmt,
ops::RangeInclusive,
path::{Path, PathBuf},
};
use ui::{App, IconName, SharedString};
use url::Url;
use urlencoding::decode;
use util::paths::PathStyle;
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, Hash)]
@@ -74,11 +76,13 @@ impl MentionUri {
let path = url.path();
match url.scheme() {
"file" => {
let path = if path_style.is_windows() {
let normalized = if path_style.is_windows() {
path.trim_start_matches("/")
} else {
path
};
let decoded = decode(normalized).unwrap_or(Cow::Borrowed(normalized));
let path = decoded.as_ref();
if let Some(fragment) = url.fragment() {
let line_range = parse_line_range(fragment)?;
@@ -406,6 +410,19 @@ mod tests {
assert_eq!(parsed.to_uri().to_string(), selection_uri);
}
#[test]
fn test_parse_file_uri_with_non_ascii() {
let file_uri = uri!("file:///path/to/%E6%97%A5%E6%9C%AC%E8%AA%9E.txt");
let parsed = MentionUri::parse(file_uri, PathStyle::local()).unwrap();
match &parsed {
MentionUri::File { abs_path } => {
assert_eq!(abs_path, Path::new(path!("/path/to/日本語.txt")));
}
_ => panic!("Expected File variant"),
}
assert_eq!(parsed.to_uri().to_string(), file_uri);
}
#[test]
fn test_parse_untitled_selection_uri() {
let selection_uri = uri!("zed:///agent/untitled-buffer#L1:10");

View File

@@ -12,14 +12,11 @@ use gpui::{
};
use ordered_float::OrderedFloat;
use picker::{Picker, PickerDelegate};
use ui::{
DocumentationAside, DocumentationEdge, DocumentationSide, IntoElement, KeyBinding, ListItem,
ListItemSpacing, prelude::*,
};
use ui::{DocumentationAside, DocumentationEdge, DocumentationSide, prelude::*};
use util::ResultExt;
use zed_actions::agent::OpenSettings;
use crate::ui::HoldForDefault;
use crate::ui::{HoldForDefault, ModelSelectorFooter, ModelSelectorHeader, ModelSelectorListItem};
pub type AcpModelSelector = Picker<AcpModelPickerDelegate>;
@@ -236,39 +233,19 @@ impl PickerDelegate for AcpModelPickerDelegate {
fn render_match(
&self,
ix: usize,
selected: bool,
is_focused: bool,
_: &mut Window,
cx: &mut Context<Picker<Self>>,
) -> Option<Self::ListItem> {
match self.filtered_entries.get(ix)? {
AcpModelPickerEntry::Separator(title) => Some(
div()
.px_2()
.pb_1()
.when(ix > 1, |this| {
this.mt_1()
.pt_2()
.border_t_1()
.border_color(cx.theme().colors().border_variant)
})
.child(
Label::new(title)
.size(LabelSize::XSmall)
.color(Color::Muted),
)
.into_any_element(),
),
AcpModelPickerEntry::Separator(title) => {
Some(ModelSelectorHeader::new(title, ix > 1).into_any_element())
}
AcpModelPickerEntry::Model(model_info) => {
let is_selected = Some(model_info) == self.selected_model.as_ref();
let default_model = self.agent_server.default_model(cx);
let is_default = default_model.as_ref() == Some(&model_info.id);
let model_icon_color = if is_selected {
Color::Accent
} else {
Color::Muted
};
Some(
div()
.id(("model-picker-menu-child", ix))
@@ -284,30 +261,10 @@ impl PickerDelegate for AcpModelPickerDelegate {
}))
})
.child(
ListItem::new(ix)
.inset(true)
.spacing(ListItemSpacing::Sparse)
.toggle_state(selected)
.child(
h_flex()
.w_full()
.gap_1p5()
.when_some(model_info.icon, |this, icon| {
this.child(
Icon::new(icon)
.color(model_icon_color)
.size(IconSize::Small)
)
})
.child(Label::new(model_info.name.clone()).truncate()),
)
.end_slot(div().pr_3().when(is_selected, |this| {
this.child(
Icon::new(IconName::Check)
.color(Color::Accent)
.size(IconSize::Small),
)
})),
ModelSelectorListItem::new(ix, model_info.name.clone())
.is_focused(is_focused)
.is_selected(is_selected)
.when_some(model_info.icon, |this, icon| this.icon(icon)),
)
.into_any_element()
)
@@ -343,7 +300,7 @@ impl PickerDelegate for AcpModelPickerDelegate {
fn render_footer(
&self,
_window: &mut Window,
cx: &mut Context<Picker<Self>>,
_cx: &mut Context<Picker<Self>>,
) -> Option<AnyElement> {
let focus_handle = self.focus_handle.clone();
@@ -351,26 +308,7 @@ impl PickerDelegate for AcpModelPickerDelegate {
return None;
}
Some(
h_flex()
.w_full()
.p_1p5()
.border_t_1()
.border_color(cx.theme().colors().border_variant)
.child(
Button::new("configure", "Configure")
.full_width()
.style(ButtonStyle::Outlined)
.key_binding(
KeyBinding::for_action_in(&OpenSettings, &focus_handle, cx)
.map(|kb| kb.size(rems_from_px(12.))),
)
.on_click(|_, window, cx| {
window.dispatch_action(OpenSettings.boxed_clone(), cx);
}),
)
.into_any(),
)
Some(ModelSelectorFooter::new(OpenSettings.boxed_clone(), focus_handle).into_any_element())
}
}
@@ -403,9 +341,7 @@ async fn fuzzy_search(
let candidates = model_list
.iter()
.enumerate()
.map(|(ix, model)| {
StringMatchCandidate::new(ix, &format!("{}/{}", model.id, model.name))
})
.map(|(ix, model)| StringMatchCandidate::new(ix, model.name.as_ref()))
.collect::<Vec<_>>();
let mut matches = match_strings(
&candidates,

View File

@@ -63,10 +63,7 @@ use crate::acp::message_editor::{MessageEditor, MessageEditorEvent};
use crate::agent_diff::AgentDiff;
use crate::profile_selector::{ProfileProvider, ProfileSelector};
use crate::ui::{
AgentNotification, AgentNotificationEvent, BurnModeTooltip, UnavailableEditingTooltip,
UsageCallout,
};
use crate::ui::{AgentNotification, AgentNotificationEvent, BurnModeTooltip, UsageCallout};
use crate::{
AgentDiffPane, AgentPanel, AllowAlways, AllowOnce, ContinueThread, ContinueWithBurnMode,
CycleModeSelector, ExpandMessageEditor, Follow, KeepAll, NewThread, OpenAgentDiff, OpenHistory,
@@ -693,7 +690,7 @@ impl AcpThreadView {
this.new_server_version_available = Some(new_version.into());
cx.notify();
})
.log_err();
.ok();
}
}
})
@@ -2091,10 +2088,23 @@ impl AcpThreadView {
.icon_size(IconSize::Small)
.icon_color(Color::Muted)
.style(ButtonStyle::Transparent)
.tooltip(move |_window, cx| {
cx.new(|_| UnavailableEditingTooltip::new(agent_name.clone()))
.into()
})
.tooltip(Tooltip::element({
move |_, _| {
v_flex()
.gap_1()
.child(Label::new("Unavailable Editing")).child(
div().max_w_64().child(
Label::new(format!(
"Editing previous messages is not available for {} yet.",
agent_name.clone()
))
.size(LabelSize::Small)
.color(Color::Muted),
),
)
.into_any_element()
}
}))
)
)
}
@@ -4208,7 +4218,11 @@ impl AcpThreadView {
}
}))
.on_action(cx.listener(|this, _: &CycleModeSelector, window, cx| {
if let Some(mode_selector) = this.mode_selector() {
if let Some(profile_selector) = this.profile_selector.as_ref() {
profile_selector.update(cx, |profile_selector, cx| {
profile_selector.cycle_profile(cx);
});
} else if let Some(mode_selector) = this.mode_selector() {
mode_selector.update(cx, |mode_selector, cx| {
mode_selector.cycle_mode(window, cx);
});
@@ -4859,6 +4873,32 @@ impl AcpThreadView {
cx.notify();
}
fn scroll_to_most_recent_user_prompt(&mut self, cx: &mut Context<Self>) {
let Some(thread) = self.thread() else {
return;
};
let entries = thread.read(cx).entries();
if entries.is_empty() {
return;
}
// Find the most recent user message and scroll it to the top of the viewport.
// (Fallback: if no user message exists, scroll to the bottom.)
if let Some(ix) = entries
.iter()
.rposition(|entry| matches!(entry, AgentThreadEntry::UserMessage(_)))
{
self.list_state.scroll_to(ListOffset {
item_ix: ix,
offset_in_item: px(0.0),
});
cx.notify();
} else {
self.scroll_to_bottom(cx);
}
}
pub fn scroll_to_bottom(&mut self, cx: &mut Context<Self>) {
if let Some(thread) = self.thread() {
let entry_count = thread.read(cx).entries().len();
@@ -5077,6 +5117,16 @@ impl AcpThreadView {
}
}));
let scroll_to_recent_user_prompt =
IconButton::new("scroll_to_recent_user_prompt", IconName::ForwardArrow)
.shape(ui::IconButtonShape::Square)
.icon_size(IconSize::Small)
.icon_color(Color::Ignored)
.tooltip(Tooltip::text("Scroll To Most Recent User Prompt"))
.on_click(cx.listener(move |this, _, _, cx| {
this.scroll_to_most_recent_user_prompt(cx);
}));
let scroll_to_top = IconButton::new("scroll_to_top", IconName::ArrowUp)
.shape(ui::IconButtonShape::Square)
.icon_size(IconSize::Small)
@@ -5153,6 +5203,7 @@ impl AcpThreadView {
container
.child(open_as_markdown)
.child(scroll_to_recent_user_prompt)
.child(scroll_to_top)
.into_any_element()
}
@@ -6785,6 +6836,70 @@ pub(crate) mod tests {
});
}
#[gpui::test]
async fn test_scroll_to_most_recent_user_prompt(cx: &mut TestAppContext) {
init_test(cx);
let connection = StubAgentConnection::new();
// Each user prompt will result in a user message entry plus an agent message entry.
connection.set_next_prompt_updates(vec![acp::SessionUpdate::AgentMessageChunk(
acp::ContentChunk::new("Response 1".into()),
)]);
let (thread_view, cx) =
setup_thread_view(StubAgentServer::new(connection.clone()), cx).await;
let thread = thread_view
.read_with(cx, |view, _| view.thread().cloned())
.unwrap();
thread
.update(cx, |thread, cx| thread.send_raw("Prompt 1", cx))
.await
.unwrap();
cx.run_until_parked();
connection.set_next_prompt_updates(vec![acp::SessionUpdate::AgentMessageChunk(
acp::ContentChunk::new("Response 2".into()),
)]);
thread
.update(cx, |thread, cx| thread.send_raw("Prompt 2", cx))
.await
.unwrap();
cx.run_until_parked();
// Move somewhere else first so we're not trivially already on the last user prompt.
thread_view.update(cx, |view, cx| {
view.scroll_to_top(cx);
});
cx.run_until_parked();
thread_view.update(cx, |view, cx| {
view.scroll_to_most_recent_user_prompt(cx);
let scroll_top = view.list_state.logical_scroll_top();
// Entries layout is: [User1, Assistant1, User2, Assistant2]
assert_eq!(scroll_top.item_ix, 2);
});
}
#[gpui::test]
async fn test_scroll_to_most_recent_user_prompt_falls_back_to_bottom_without_user_messages(
cx: &mut TestAppContext,
) {
init_test(cx);
let (thread_view, cx) = setup_thread_view(StubAgentServer::default_response(), cx).await;
// With no entries, scrolling should be a no-op and must not panic.
thread_view.update(cx, |view, cx| {
view.scroll_to_most_recent_user_prompt(cx);
let scroll_top = view.list_state.logical_scroll_top();
assert_eq!(scroll_top.item_ix, 0);
});
}
#[gpui::test]
async fn test_message_editing_cancel(cx: &mut TestAppContext) {
init_test(cx);

View File

@@ -8,6 +8,7 @@ use editor::Editor;
use fs::Fs;
use gpui::{DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, Subscription, prelude::*};
use language_model::{LanguageModel, LanguageModelRegistry};
use settings::SettingsStore;
use settings::{
LanguageModelProviderSetting, LanguageModelSelection, Settings as _, update_settings_file,
};
@@ -94,6 +95,7 @@ pub struct ViewProfileMode {
configure_default_model: NavigableEntry,
configure_tools: NavigableEntry,
configure_mcps: NavigableEntry,
delete_profile: NavigableEntry,
cancel_item: NavigableEntry,
}
@@ -109,6 +111,7 @@ pub struct ManageProfilesModal {
active_model: Option<Arc<dyn LanguageModel>>,
focus_handle: FocusHandle,
mode: Mode,
_settings_subscription: Subscription,
}
impl ManageProfilesModal {
@@ -148,12 +151,23 @@ impl ManageProfilesModal {
) -> Self {
let focus_handle = cx.focus_handle();
// Keep this modal in sync with settings changes (including profile deletion).
let settings_subscription =
cx.observe_global_in::<SettingsStore>(window, |this, window, cx| {
if matches!(this.mode, Mode::ChooseProfile(_)) {
this.mode = Mode::choose_profile(window, cx);
this.focus_handle(cx).focus(window);
cx.notify();
}
});
Self {
fs,
active_model,
context_server_registry,
focus_handle,
mode: Mode::choose_profile(window, cx),
_settings_subscription: settings_subscription,
}
}
@@ -192,6 +206,7 @@ impl ManageProfilesModal {
configure_default_model: NavigableEntry::focusable(cx),
configure_tools: NavigableEntry::focusable(cx),
configure_mcps: NavigableEntry::focusable(cx),
delete_profile: NavigableEntry::focusable(cx),
cancel_item: NavigableEntry::focusable(cx),
});
self.focus_handle(cx).focus(window);
@@ -369,6 +384,42 @@ impl ManageProfilesModal {
}
}
fn delete_profile(
&mut self,
profile_id: AgentProfileId,
window: &mut Window,
cx: &mut Context<Self>,
) {
if builtin_profiles::is_builtin(&profile_id) {
self.view_profile(profile_id, window, cx);
return;
}
let fs = self.fs.clone();
update_settings_file(fs, cx, move |settings, _cx| {
let Some(agent_settings) = settings.agent.as_mut() else {
return;
};
let Some(profiles) = agent_settings.profiles.as_mut() else {
return;
};
profiles.shift_remove(profile_id.0.as_ref());
if agent_settings
.default_profile
.as_deref()
.is_some_and(|default_profile| default_profile == profile_id.0.as_ref())
{
agent_settings.default_profile = Some(AgentProfileId::default().0);
}
});
self.choose_profile(window, cx);
}
fn cancel(&mut self, window: &mut Window, cx: &mut Context<Self>) {
match &self.mode {
Mode::ChooseProfile { .. } => {
@@ -756,6 +807,40 @@ impl ManageProfilesModal {
}),
),
)
.child(
div()
.id("delete-profile")
.track_focus(&mode.delete_profile.focus_handle)
.on_action({
let profile_id = mode.profile_id.clone();
cx.listener(move |this, _: &menu::Confirm, window, cx| {
this.delete_profile(profile_id.clone(), window, cx);
})
})
.child(
ListItem::new("delete-profile")
.toggle_state(
mode.delete_profile
.focus_handle
.contains_focused(window, cx),
)
.inset(true)
.spacing(ListItemSpacing::Sparse)
.start_slot(
Icon::new(IconName::Trash)
.size(IconSize::Small)
.color(Color::Error),
)
.child(Label::new("Delete Profile").color(Color::Error))
.disabled(builtin_profiles::is_builtin(&mode.profile_id))
.on_click({
let profile_id = mode.profile_id.clone();
cx.listener(move |this, _, window, cx| {
this.delete_profile(profile_id.clone(), window, cx);
})
}),
),
)
.child(ListSeparator)
.child(
div()
@@ -805,6 +890,7 @@ impl ManageProfilesModal {
.entry(mode.configure_default_model)
.entry(mode.configure_tools)
.entry(mode.configure_mcps)
.entry(mode.delete_profile)
.entry(mode.cancel_item)
}
}

View File

@@ -11,9 +11,11 @@ use language_model::{
};
use ordered_float::OrderedFloat;
use picker::{Picker, PickerDelegate};
use ui::{KeyBinding, ListItem, ListItemSpacing, prelude::*};
use ui::prelude::*;
use zed_actions::agent::OpenSettings;
use crate::ui::{ModelSelectorFooter, ModelSelectorHeader, ModelSelectorListItem};
type OnModelChanged = Arc<dyn Fn(Arc<dyn LanguageModel>, &mut App) + 'static>;
type GetActiveModel = Arc<dyn Fn(&App) -> Option<ConfiguredModel> + 'static>;
@@ -459,28 +461,14 @@ impl PickerDelegate for LanguageModelPickerDelegate {
fn render_match(
&self,
ix: usize,
selected: bool,
is_focused: bool,
_: &mut Window,
cx: &mut Context<Picker<Self>>,
) -> Option<Self::ListItem> {
match self.filtered_entries.get(ix)? {
LanguageModelPickerEntry::Separator(title) => Some(
div()
.px_2()
.pb_1()
.when(ix > 1, |this| {
this.mt_1()
.pt_2()
.border_t_1()
.border_color(cx.theme().colors().border_variant)
})
.child(
Label::new(title)
.size(LabelSize::XSmall)
.color(Color::Muted),
)
.into_any_element(),
),
LanguageModelPickerEntry::Separator(title) => {
Some(ModelSelectorHeader::new(title, ix > 1).into_any_element())
}
LanguageModelPickerEntry::Model(model_info) => {
let active_model = (self.get_active_model)(cx);
let active_provider_id = active_model.as_ref().map(|m| m.provider.id());
@@ -489,35 +477,11 @@ impl PickerDelegate for LanguageModelPickerDelegate {
let is_selected = Some(model_info.model.provider_id()) == active_provider_id
&& Some(model_info.model.id()) == active_model_id;
let model_icon_color = if is_selected {
Color::Accent
} else {
Color::Muted
};
Some(
ListItem::new(ix)
.inset(true)
.spacing(ListItemSpacing::Sparse)
.toggle_state(selected)
.child(
h_flex()
.w_full()
.gap_1p5()
.child(
Icon::new(model_info.icon)
.color(model_icon_color)
.size(IconSize::Small),
)
.child(Label::new(model_info.model.name().0).truncate()),
)
.end_slot(div().pr_3().when(is_selected, |this| {
this.child(
Icon::new(IconName::Check)
.color(Color::Accent)
.size(IconSize::Small),
)
}))
ModelSelectorListItem::new(ix, model_info.model.name().0)
.is_focused(is_focused)
.is_selected(is_selected)
.icon(model_info.icon)
.into_any_element(),
)
}
@@ -527,34 +491,15 @@ impl PickerDelegate for LanguageModelPickerDelegate {
fn render_footer(
&self,
_window: &mut Window,
cx: &mut Context<Picker<Self>>,
_cx: &mut Context<Picker<Self>>,
) -> Option<gpui::AnyElement> {
let focus_handle = self.focus_handle.clone();
if !self.popover_styles {
return None;
}
Some(
h_flex()
.w_full()
.p_1p5()
.border_t_1()
.border_color(cx.theme().colors().border_variant)
.child(
Button::new("configure", "Configure")
.full_width()
.style(ButtonStyle::Outlined)
.key_binding(
KeyBinding::for_action_in(&OpenSettings, &focus_handle, cx)
.map(|kb| kb.size(rems_from_px(12.))),
)
.on_click(|_, window, cx| {
window.dispatch_action(OpenSettings.boxed_clone(), cx);
}),
)
.into_any(),
)
let focus_handle = self.focus_handle.clone();
Some(ModelSelectorFooter::new(OpenSettings.boxed_clone(), focus_handle).into_any_element())
}
}

View File

@@ -1,4 +1,4 @@
use crate::{ManageProfiles, ToggleProfileSelector};
use crate::{CycleModeSelector, ManageProfiles, ToggleProfileSelector};
use agent_settings::{
AgentProfile, AgentProfileId, AgentSettings, AvailableProfiles, builtin_profiles,
};
@@ -70,6 +70,29 @@ impl ProfileSelector {
self.picker_handle.clone()
}
pub fn cycle_profile(&mut self, cx: &mut Context<Self>) {
if !self.provider.profiles_supported(cx) {
return;
}
let profiles = AgentProfile::available_profiles(cx);
if profiles.is_empty() {
return;
}
let current_profile_id = self.provider.profile_id(cx);
let current_index = profiles
.keys()
.position(|id| id == &current_profile_id)
.unwrap_or(0);
let next_index = (current_index + 1) % profiles.len();
if let Some((next_profile_id, _)) = profiles.get_index(next_index) {
self.provider.set_profile(next_profile_id.clone(), cx);
}
}
fn ensure_picker(
&mut self,
window: &mut Window,
@@ -163,14 +186,29 @@ impl Render for ProfileSelector {
PickerPopoverMenu::new(
picker,
trigger_button,
move |_window, cx| {
Tooltip::for_action_in(
"Toggle Profile Menu",
&ToggleProfileSelector,
&focus_handle,
cx,
)
},
Tooltip::element({
move |_window, cx| {
let container = || h_flex().gap_1().justify_between();
v_flex()
.gap_1()
.child(
container()
.pb_1()
.border_b_1()
.border_color(cx.theme().colors().border_variant)
.child(Label::new("Cycle Through Profiles"))
.child(KeyBinding::for_action_in(
&CycleModeSelector,
&focus_handle,
cx,
)),
)
.child(container().child(Label::new("Toggle Profile Menu")).child(
KeyBinding::for_action_in(&ToggleProfileSelector, &focus_handle, cx),
))
.into_any()
}
}),
gpui::Corner::BottomRight,
cx,
)

View File

@@ -4,8 +4,8 @@ mod burn_mode_tooltip;
mod claude_code_onboarding_modal;
mod end_trial_upsell;
mod hold_for_default;
mod model_selector_components;
mod onboarding_modal;
mod unavailable_editing_tooltip;
mod usage_callout;
pub use acp_onboarding_modal::*;
@@ -14,6 +14,6 @@ pub use burn_mode_tooltip::*;
pub use claude_code_onboarding_modal::*;
pub use end_trial_upsell::*;
pub use hold_for_default::*;
pub use model_selector_components::*;
pub use onboarding_modal::*;
pub use unavailable_editing_tooltip::*;
pub use usage_callout::*;

View File

@@ -0,0 +1,147 @@
use gpui::{Action, FocusHandle, prelude::*};
use ui::{KeyBinding, ListItem, ListItemSpacing, prelude::*};
#[derive(IntoElement)]
pub struct ModelSelectorHeader {
title: SharedString,
has_border: bool,
}
impl ModelSelectorHeader {
pub fn new(title: impl Into<SharedString>, has_border: bool) -> Self {
Self {
title: title.into(),
has_border,
}
}
}
impl RenderOnce for ModelSelectorHeader {
fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
div()
.px_2()
.pb_1()
.when(self.has_border, |this| {
this.mt_1()
.pt_2()
.border_t_1()
.border_color(cx.theme().colors().border_variant)
})
.child(
Label::new(self.title)
.size(LabelSize::XSmall)
.color(Color::Muted),
)
}
}
#[derive(IntoElement)]
pub struct ModelSelectorListItem {
index: usize,
title: SharedString,
icon: Option<IconName>,
is_selected: bool,
is_focused: bool,
}
impl ModelSelectorListItem {
pub fn new(index: usize, title: impl Into<SharedString>) -> Self {
Self {
index,
title: title.into(),
icon: None,
is_selected: false,
is_focused: false,
}
}
pub fn icon(mut self, icon: IconName) -> Self {
self.icon = Some(icon);
self
}
pub fn is_selected(mut self, is_selected: bool) -> Self {
self.is_selected = is_selected;
self
}
pub fn is_focused(mut self, is_focused: bool) -> Self {
self.is_focused = is_focused;
self
}
}
impl RenderOnce for ModelSelectorListItem {
fn render(self, _window: &mut Window, _cx: &mut App) -> impl IntoElement {
let model_icon_color = if self.is_selected {
Color::Accent
} else {
Color::Muted
};
ListItem::new(self.index)
.inset(true)
.spacing(ListItemSpacing::Sparse)
.toggle_state(self.is_focused)
.child(
h_flex()
.w_full()
.gap_1p5()
.when_some(self.icon, |this, icon| {
this.child(
Icon::new(icon)
.color(model_icon_color)
.size(IconSize::Small),
)
})
.child(Label::new(self.title).truncate()),
)
.end_slot(div().pr_2().when(self.is_selected, |this| {
this.child(
Icon::new(IconName::Check)
.color(Color::Accent)
.size(IconSize::Small),
)
}))
}
}
#[derive(IntoElement)]
pub struct ModelSelectorFooter {
action: Box<dyn Action>,
focus_handle: FocusHandle,
}
impl ModelSelectorFooter {
pub fn new(action: Box<dyn Action>, focus_handle: FocusHandle) -> Self {
Self {
action,
focus_handle,
}
}
}
impl RenderOnce for ModelSelectorFooter {
fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
let action = self.action;
let focus_handle = self.focus_handle;
h_flex()
.w_full()
.p_1p5()
.border_t_1()
.border_color(cx.theme().colors().border_variant)
.child(
Button::new("configure", "Configure")
.full_width()
.style(ButtonStyle::Outlined)
.key_binding(
KeyBinding::for_action_in(action.as_ref(), &focus_handle, cx)
.map(|kb| kb.size(rems_from_px(12.))),
)
.on_click(move |_, window, cx| {
window.dispatch_action(action.boxed_clone(), cx);
}),
)
}
}

View File

@@ -1,29 +0,0 @@
use gpui::{Context, IntoElement, Render, Window};
use ui::{prelude::*, tooltip_container};
pub struct UnavailableEditingTooltip {
agent_name: SharedString,
}
impl UnavailableEditingTooltip {
pub fn new(agent_name: SharedString) -> Self {
Self { agent_name }
}
}
impl Render for UnavailableEditingTooltip {
fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
tooltip_container(cx, |this, _| {
this.child(Label::new("Unavailable Editing")).child(
div().max_w_64().child(
Label::new(format!(
"Editing previous messages is not available for {} yet.",
self.agent_name
))
.size(LabelSize::Small)
.color(Color::Muted),
),
)
})
}
}

View File

@@ -111,6 +111,9 @@ struct CopilotSweAgentBot;
impl CopilotSweAgentBot {
const LOGIN: &'static str = "copilot-swe-agent[bot]";
const USER_ID: i32 = 198982749;
/// The alias of the GitHub copilot user. Although https://api.github.com/users/copilot
/// yields a 404, GitHub still refers to the copilot bot user as @Copilot in some cases.
const NAME_ALIAS: &'static str = "copilot";
/// Returns the `created_at` timestamp for the Dependabot bot user.
fn created_at() -> &'static NaiveDateTime {
@@ -125,7 +128,9 @@ impl CopilotSweAgentBot {
/// Returns whether the given contributor selector corresponds to the Copilot bot user.
fn is_copilot_bot(contributor: &ContributorSelector) -> bool {
match contributor {
ContributorSelector::GitHubLogin { github_login } => github_login == Self::LOGIN,
ContributorSelector::GitHubLogin { github_login } => {
github_login == Self::LOGIN || github_login == Self::NAME_ALIAS
}
ContributorSelector::GitHubUserId { github_user_id } => {
github_user_id == &Self::USER_ID
}

View File

@@ -24,6 +24,7 @@ language.workspace = true
log.workspace = true
lsp.workspace = true
markdown.workspace = true
multi_buffer.workspace = true
project.workspace = true
rand.workspace = true
serde.workspace = true

View File

@@ -6,7 +6,7 @@ use crate::{
use anyhow::Result;
use collections::HashMap;
use editor::{
Editor, EditorEvent, ExcerptRange, MultiBuffer, PathKey,
Editor, EditorEvent, EditorSettings, ExcerptRange, MultiBuffer, PathKey,
display_map::{BlockPlacement, BlockProperties, BlockStyle, CustomBlockId},
multibuffer_context_lines,
};
@@ -701,8 +701,12 @@ impl Item for BufferDiagnosticsEditor {
});
}
fn breadcrumb_location(&self, _: &App) -> ToolbarItemLocation {
ToolbarItemLocation::PrimaryLeft
fn breadcrumb_location(&self, cx: &App) -> ToolbarItemLocation {
if EditorSettings::get_global(cx).toolbar.breadcrumbs {
ToolbarItemLocation::PrimaryLeft
} else {
ToolbarItemLocation::Hidden
}
}
fn breadcrumbs(&self, theme: &theme::Theme, cx: &App) -> Option<Vec<BreadcrumbText>> {

View File

@@ -12,7 +12,7 @@ use buffer_diagnostics::BufferDiagnosticsEditor;
use collections::{BTreeSet, HashMap, HashSet};
use diagnostic_renderer::DiagnosticBlock;
use editor::{
Editor, EditorEvent, ExcerptRange, MultiBuffer, PathKey,
Editor, EditorEvent, EditorSettings, ExcerptRange, MultiBuffer, PathKey,
display_map::{BlockPlacement, BlockProperties, BlockStyle, CustomBlockId},
multibuffer_context_lines,
};
@@ -336,6 +336,33 @@ impl ProjectDiagnosticsEditor {
}
}
fn update_multibuffer_initial_ranges(&mut self, cx: &mut App) {
let snapshot = self.multibuffer.read(cx).snapshot(cx);
let len = self.diagnostics.values().map(|v| v.len()).sum();
let mut ranges: Vec<Range<multi_buffer::Anchor>> = Vec::with_capacity(len);
for (buffer_id, diagnostics) in &self.diagnostics {
let Some(excerpt_id) = snapshot.excerpt_id_for_buffer_id(*buffer_id) else {
#[cfg(debug_assertions)]
panic!("no excerpt_id for buffer_id: {buffer_id}");
#[cfg(not(debug_assertions))]
continue;
};
for diagnostic in diagnostics {
let opt = snapshot.anchor_range_in_excerpt(excerpt_id, diagnostic.range.clone());
if let Some(range) = opt {
ranges.push(range);
};
}
}
self.editor.update(cx, |editor, cx| {
editor.set_initial_multibuffer_matches(ranges);
});
}
fn update_stale_excerpts(&mut self, window: &mut Window, cx: &mut Context<Self>) {
if self.update_excerpts_task.is_some() {
return;
@@ -375,6 +402,10 @@ impl ProjectDiagnosticsEditor {
this.update_excerpts(buffer, retain_excerpts, window, cx)
})?
.await?;
this.update(cx, |this, cx| {
this.update_multibuffer_initial_ranges(cx);
});
}
}
Ok(())
@@ -464,6 +495,7 @@ impl ProjectDiagnosticsEditor {
});
self.update_stale_excerpts(window, cx);
self.update_multibuffer_initial_ranges(cx);
}
fn diagnostics_are_unchanged(
@@ -894,8 +926,12 @@ impl Item for ProjectDiagnosticsEditor {
Some(Box::new(self.editor.clone()))
}
fn breadcrumb_location(&self, _: &App) -> ToolbarItemLocation {
ToolbarItemLocation::PrimaryLeft
fn breadcrumb_location(&self, cx: &App) -> ToolbarItemLocation {
if EditorSettings::get_global(cx).toolbar.breadcrumbs {
ToolbarItemLocation::PrimaryLeft
} else {
ToolbarItemLocation::Hidden
}
}
fn breadcrumbs(&self, theme: &theme::Theme, cx: &App) -> Option<Vec<BreadcrumbText>> {

View File

@@ -717,6 +717,10 @@ actions!(
SelectNextSyntaxNode,
/// Selects the previous syntax node sibling.
SelectPreviousSyntaxNode,
/// Select the next match in the current multibuffer.
SelectNextMultibufferMatch,
/// Select the previous match in the current multibuffer.
SelectPreviousMultibufferMatch,
/// Extends selection left.
SelectLeft,
/// Selects the current line.

View File

@@ -1218,6 +1218,10 @@ pub struct Editor {
accent_data: Option<AccentData>,
fetched_tree_sitter_chunks: HashMap<ExcerptId, HashSet<Range<BufferRow>>>,
use_base_text_line_numbers: bool,
/// Matches used to create the multibuffer (e.g. LSP references, project search matches).
/// Ranges are always sorted by start anchor. Invalid anchors are removed on the next call to
/// [`SelectNextMultibufferMatch`] (or prev.)
initial_multibuffer_matches: Vec<Range<multi_buffer::Anchor>>,
}
#[derive(Debug, PartialEq)]
@@ -2420,6 +2424,7 @@ impl Editor {
accent_data: None,
fetched_tree_sitter_chunks: HashMap::default(),
use_base_text_line_numbers: false,
initial_multibuffer_matches: Vec::default(),
};
if is_minimap {
@@ -16315,6 +16320,103 @@ impl Editor {
}
}
pub fn select_next_multibuffer_match(
&mut self,
_: &SelectNextMultibufferMatch,
window: &mut Window,
cx: &mut Context<Self>,
) {
self.select_multibuffer_match(Bias::Right, window, cx);
}
pub fn select_prev_multibuffer_match(
&mut self,
_: &SelectPreviousMultibufferMatch,
window: &mut Window,
cx: &mut Context<Self>,
) {
self.select_multibuffer_match(Bias::Left, window, cx);
}
pub fn set_initial_multibuffer_matches(&mut self, ranges: Vec<Range<Anchor>>) {
self.initial_multibuffer_matches = ranges;
}
/// Multibuffers can be created with initial "matches" (e.g. LSP references, project search
/// matches, etc.). We populate a list when the multibuffer is created. During editing, matches
/// can be invalidated if the anchors are deleted
fn select_multibuffer_match(
&mut self,
direction: Bias,
window: &mut Window,
cx: &mut Context<Self>,
) {
let len_before = self.initial_multibuffer_matches.len();
let snapshot = self.buffer.read(cx).snapshot(cx);
// todo! doing a linear scan every time might be slow?
self.initial_multibuffer_matches.retain(|range| {
// todo! is this right? what if a match is half-invalidated? can we truncate the match?
range.start.is_valid(&snapshot) && range.end.is_valid(&snapshot)
});
let len_after = self.initial_multibuffer_matches.len();
dbg!(len_before, len_after);
match &self.initial_multibuffer_matches[..] {
[] => return,
// if there is only one match, we select it regardless of direction
[only] => {
let only = only.clone();
self.change_selections(SelectionEffects::default(), window, cx, |selections| {
selections.clear_disjoint();
selections.insert_range(only.start..only.start);
});
return;
}
_ => {}
};
debug_assert!(self.initial_multibuffer_matches.len() >= 2);
dbg!(
self.initial_multibuffer_matches
.iter()
.map(|range| format!(
"{:?}..{:?}",
range.start.to_point(&snapshot),
range.end.to_point(&snapshot)
))
.collect::<Vec<_>>()
);
let display_snapshot = self.display_snapshot(cx);
let selection = self.selections.last::<MultiBufferOffset>(&display_snapshot);
let search = self
.initial_multibuffer_matches
.binary_search_by(|range| range.start.to_offset(&snapshot).cmp(&selection.start));
dbg!(self.initial_multibuffer_matches.len(), search, direction);
let target_index = match search {
Ok(i) => match (i, direction) {
(0, Bias::Left) => self.initial_multibuffer_matches.len() - 1,
(i, Bias::Left) => i - 1,
(i, Bias::Right) => (i + 1) % self.initial_multibuffer_matches.len(),
},
Err(i) => match (i, direction) {
(0, Bias::Left) => self.initial_multibuffer_matches.len() - 1,
(i, Bias::Left) => i - 1,
(i, Bias::Right) => i % self.initial_multibuffer_matches.len(),
},
};
let target = self.initial_multibuffer_matches[target_index].clone();
self.change_selections(SelectionEffects::default(), window, cx, |selections| {
selections.clear_disjoint();
selections.insert_range(target.start..target.start);
});
}
fn refresh_runnables(&mut self, window: &mut Window, cx: &mut Context<Self>) -> Task<()> {
if !EditorSettings::get_global(cx).gutter.runnables {
self.clear_tasks();
@@ -17709,9 +17811,6 @@ impl Editor {
}
};
// TODO(cameron): is this needed?
// the thinking is to avoid "jumping to the current location" (avoid
// polluting "jumplist" in vim terms)
if current_location_index == destination_location_index {
return Ok(());
}
@@ -17966,6 +18065,7 @@ impl Editor {
cx,
);
editor.lookup_key = Some(Box::new(key));
editor.set_initial_multibuffer_matches(ranges.clone());
editor
})
});

View File

@@ -29335,3 +29335,59 @@ async fn test_find_references_single_case(cx: &mut TestAppContext) {
cx.assert_editor_state(after);
}
// #[gpui::test]
// async fn next_multibuffer_match_lsp_references(cx: &mut TestAppContext) {
// init_test(cx, |_| {});
// let mut cx = EditorLspTestContext::new_rust(
// lsp::ServerCapabilities {
// references_provider: Some(lsp::OneOf::Left(true)),
// ..lsp::ServerCapabilities::default()
// },
// cx,
// )
// .await;
// cx.lsp
// .set_request_handler::<lsp::request::References, _, _>(async move |params, _| {
// let mk_loc = |row, start_col, end_col| lsp::Location {
// uri: params.text_document_position.text_document.uri.clone(),
// range: lsp::Range::new(
// lsp::Position::new(row, start_col),
// lsp::Position::new(row, end_col),
// ),
// };
//
// Ok(Some(vec![
// mk_loc(1, 8, 13),
// mk_loc(3, 12, 17),
// mk_loc(5, 12, 17),
// mk_loc(7, 12, 17),
// ]))
// });
//
// let initial_state = indoc!(
// r#"
// fn main() {
// let ˇhello = 123;
//
// let x = hello;
//
// let y = hello;
//
// let z = hello;
// }
// "#
// );
//
// cx.set_state(initial_state);
//
// let navigated = cx
// .update_editor(|editor, window, cx| editor.find_all_references(&action, window, cx))
// .expect("should have spawned a task")
// .await
// .unwrap();
// assert_eq!(navigated, Navigated::Yes);
//
//
//
// }

View File

@@ -385,6 +385,8 @@ impl EditorElement {
register_action(editor, window, Editor::select_smaller_syntax_node);
register_action(editor, window, Editor::select_next_syntax_node);
register_action(editor, window, Editor::select_prev_syntax_node);
register_action(editor, window, Editor::select_next_multibuffer_match);
register_action(editor, window, Editor::select_prev_multibuffer_match);
register_action(editor, window, Editor::unwrap_syntax_node);
register_action(editor, window, Editor::select_enclosing_symbol);
register_action(editor, window, Editor::move_to_enclosing_bracket);

View File

@@ -656,6 +656,7 @@ pub fn hover_markdown_style(window: &Window, cx: &App) -> MarkdownStyle {
.text_base()
.mt(rems(1.))
.mb_0(),
table_columns_min_size: true,
..Default::default()
}
}
@@ -709,6 +710,7 @@ pub fn diagnostics_markdown_style(window: &Window, cx: &App) -> MarkdownStyle {
.font_weight(FontWeight::BOLD)
.text_base()
.mb_0(),
table_columns_min_size: true,
..Default::default()
}
}

View File

@@ -2425,7 +2425,10 @@ impl CodeLabel {
"invalid filter range"
);
runs.iter().for_each(|(range, _)| {
assert!(text.get(range.clone()).is_some(), "invalid run range");
assert!(
text.get(range.clone()).is_some(),
"invalid run range with inputs. Requested range {range:?} in text '{text}'",
);
});
Self {
runs,

View File

@@ -375,16 +375,20 @@ impl LspAdapter for RustLspAdapter {
let start_pos = range.start as usize;
let end_pos = range.end as usize;
label.push_str(&snippet.text[text_pos..end_pos]);
text_pos = end_pos;
label.push_str(&snippet.text[text_pos..start_pos]);
if start_pos == end_pos {
let caret_start = label.len();
label.push('…');
runs.push((caret_start..label.len(), HighlightId::TABSTOP_INSERT_ID));
} else {
runs.push((start_pos..end_pos, HighlightId::TABSTOP_REPLACE_ID));
let label_start = label.len();
label.push_str(&snippet.text[start_pos..end_pos]);
let label_end = label.len();
runs.push((label_start..label_end, HighlightId::TABSTOP_REPLACE_ID));
}
text_pos = end_pos;
}
label.push_str(&snippet.text[text_pos..]);
@@ -1592,6 +1596,44 @@ mod tests {
],
))
);
// Test for correct range calculation with mixed empty and non-empty tabstops.(See https://github.com/zed-industries/zed/issues/44825)
let res = adapter
.label_for_completion(
&lsp::CompletionItem {
kind: Some(lsp::CompletionItemKind::STRUCT),
label: "Particles".to_string(),
insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
range: lsp::Range::default(),
new_text: "Particles { pos_x: $1, pos_y: $2, vel_x: $3, vel_y: $4, acc_x: ${5:()}, acc_y: ${6:()}, mass: $7 }$0".to_string(),
})),
..Default::default()
},
&language,
)
.await
.unwrap();
assert_eq!(
res,
CodeLabel::new(
"Particles { pos_x: …, pos_y: …, vel_x: …, vel_y: …, acc_x: (), acc_y: (), mass: … }".to_string(),
0..9,
vec![
(19..22, HighlightId::TABSTOP_INSERT_ID),
(31..34, HighlightId::TABSTOP_INSERT_ID),
(43..46, HighlightId::TABSTOP_INSERT_ID),
(55..58, HighlightId::TABSTOP_INSERT_ID),
(67..69, HighlightId::TABSTOP_REPLACE_ID),
(78..80, HighlightId::TABSTOP_REPLACE_ID),
(88..91, HighlightId::TABSTOP_INSERT_ID),
(0..9, highlight_type),
(60..65, highlight_field),
(71..76, highlight_field),
],
)
);
}
#[gpui::test]

View File

@@ -70,6 +70,7 @@ pub struct MarkdownStyle {
pub heading_level_styles: Option<HeadingLevelStyles>,
pub height_is_multiple_of_line_height: bool,
pub prevent_mouse_interaction: bool,
pub table_columns_min_size: bool,
}
impl Default for MarkdownStyle {
@@ -91,6 +92,7 @@ impl Default for MarkdownStyle {
heading_level_styles: None,
height_is_multiple_of_line_height: false,
prevent_mouse_interaction: false,
table_columns_min_size: false,
}
}
}
@@ -1061,15 +1063,23 @@ impl Element for MarkdownElement {
}
MarkdownTag::MetadataBlock(_) => {}
MarkdownTag::Table(alignments) => {
builder.table_alignments = alignments.clone();
builder.table.start(alignments.clone());
let column_count = alignments.len();
builder.push_div(
div()
.id(("table", range.start))
.min_w_0()
.grid()
.grid_cols(column_count as u16)
.when(self.style.table_columns_min_size, |this| {
this.grid_cols_min_content(column_count as u16)
})
.when(!self.style.table_columns_min_size, |this| {
this.grid_cols(column_count as u16)
})
.size_full()
.mb_2()
.border_1()
.border(px(1.5))
.border_color(cx.theme().colors().border)
.rounded_sm()
.overflow_hidden(),
@@ -1078,38 +1088,33 @@ impl Element for MarkdownElement {
);
}
MarkdownTag::TableHead => {
let column_count = builder.table_alignments.len();
builder.push_div(
div()
.grid()
.grid_cols(column_count as u16)
.bg(cx.theme().colors().title_bar_background),
range,
markdown_end,
);
builder.table.start_head();
builder.push_text_style(TextStyleRefinement {
font_weight: Some(FontWeight::SEMIBOLD),
..Default::default()
});
}
MarkdownTag::TableRow => {
let column_count = builder.table_alignments.len();
builder.push_div(
div().grid().grid_cols(column_count as u16),
range,
markdown_end,
);
builder.table.start_row();
}
MarkdownTag::TableCell => {
let is_header = builder.table.in_head;
let row_index = builder.table.row_index;
let col_index = builder.table.col_index;
builder.push_div(
div()
.min_w_0()
.border(px(0.5))
.when(col_index > 0, |this| this.border_l_1())
.when(row_index > 0, |this| this.border_t_1())
.border_color(cx.theme().colors().border)
.px_1()
.py_0p5(),
.py_0p5()
.when(is_header, |this| {
this.bg(cx.theme().colors().title_bar_background)
})
.when(!is_header && row_index % 2 == 1, |this| {
this.bg(cx.theme().colors().panel_background)
}),
range,
markdown_end,
);
@@ -1223,17 +1228,18 @@ impl Element for MarkdownElement {
}
MarkdownTagEnd::Table => {
builder.pop_div();
builder.table_alignments.clear();
builder.table.end();
}
MarkdownTagEnd::TableHead => {
builder.pop_div();
builder.pop_text_style();
builder.table.end_head();
}
MarkdownTagEnd::TableRow => {
builder.pop_div();
builder.table.end_row();
}
MarkdownTagEnd::TableCell => {
builder.pop_div();
builder.table.end_cell();
}
_ => log::debug!("unsupported markdown tag end: {:?}", tag),
},
@@ -1488,6 +1494,50 @@ impl ParentElement for AnyDiv {
}
}
#[derive(Default)]
struct TableState {
alignments: Vec<Alignment>,
in_head: bool,
row_index: usize,
col_index: usize,
}
impl TableState {
fn start(&mut self, alignments: Vec<Alignment>) {
self.alignments = alignments;
self.in_head = false;
self.row_index = 0;
self.col_index = 0;
}
fn end(&mut self) {
self.alignments.clear();
self.in_head = false;
self.row_index = 0;
self.col_index = 0;
}
fn start_head(&mut self) {
self.in_head = true;
}
fn end_head(&mut self) {
self.in_head = false;
}
fn start_row(&mut self) {
self.col_index = 0;
}
fn end_row(&mut self) {
self.row_index += 1;
}
fn end_cell(&mut self) {
self.col_index += 1;
}
}
struct MarkdownElementBuilder {
div_stack: Vec<AnyDiv>,
rendered_lines: Vec<RenderedLine>,
@@ -1499,7 +1549,7 @@ struct MarkdownElementBuilder {
text_style_stack: Vec<TextStyleRefinement>,
code_block_stack: Vec<Option<Arc<Language>>>,
list_stack: Vec<ListStackEntry>,
table_alignments: Vec<Alignment>,
table: TableState,
syntax_theme: Arc<SyntaxTheme>,
}
@@ -1535,7 +1585,7 @@ impl MarkdownElementBuilder {
text_style_stack: Vec::new(),
code_block_stack: Vec::new(),
list_stack: Vec::new(),
table_alignments: Vec::new(),
table: TableState::default(),
syntax_theme,
}
}

View File

@@ -9,7 +9,7 @@ use gpui::{
AbsoluteLength, AnyElement, App, AppContext as _, ClipboardItem, Context, Div, Element,
ElementId, Entity, HighlightStyle, Hsla, ImageSource, InteractiveText, IntoElement, Keystroke,
Modifiers, ParentElement, Render, Resource, SharedString, Styled, StyledText, TextStyle,
WeakEntity, Window, div, img, rems,
WeakEntity, Window, div, img, px, rems,
};
use settings::Settings;
use std::{
@@ -521,7 +521,8 @@ fn render_markdown_table(parsed: &ParsedMarkdownTable, cx: &mut RenderContext) -
.children(render_markdown_text(&cell.children, cx))
.px_2()
.py_1()
.border_1()
.when(col_idx > 0, |this| this.border_l_1())
.when(row_idx > 0, |this| this.border_t_1())
.border_color(cx.border_color)
.when(cell.is_header, |this| {
this.bg(cx.title_bar_background_color)
@@ -551,7 +552,8 @@ fn render_markdown_table(parsed: &ParsedMarkdownTable, cx: &mut RenderContext) -
}
let empty_cell = div()
.border_1()
.when(col_idx > 0, |this| this.border_l_1())
.when(row_idx > 0, |this| this.border_t_1())
.border_color(cx.border_color)
.when(row_idx % 2 == 1, |this| this.bg(cx.panel_background_color));
@@ -568,8 +570,10 @@ fn render_markdown_table(parsed: &ParsedMarkdownTable, cx: &mut RenderContext) -
div()
.grid()
.grid_cols(max_column_count as u16)
.border_1()
.border(px(1.5))
.border_color(cx.border_color)
.rounded_sm()
.overflow_hidden()
.children(cells),
)
.into_any()

View File

@@ -6428,6 +6428,14 @@ impl MultiBufferSnapshot {
Some(self.excerpt(excerpt_id)?.buffer_id)
}
pub fn excerpt_id_for_buffer_id(&self, buffer_id: BufferId) -> Option<ExcerptId> {
// todo! is linear OK here?
self.excerpts
.iter()
.find(|ex| ex.buffer_id == buffer_id)
.map(|ex| ex.id)
}
pub fn buffer_for_excerpt(&self, excerpt_id: ExcerptId) -> Option<&BufferSnapshot> {
Some(&self.excerpt(excerpt_id)?.buffer)
}

View File

@@ -6050,9 +6050,9 @@ impl Render for ProjectPanel {
h_flex()
.w_1_2()
.gap_2()
.child(div().flex_1().child(Divider::horizontal()))
.child(Divider::horizontal())
.child(Label::new("or").size(LabelSize::XSmall).color(Color::Muted))
.child(div().flex_1().child(Divider::horizontal())),
.child(Divider::horizontal()),
)
.child(
Button::new("clone_repo", "Clone Repository")

View File

@@ -1498,6 +1498,9 @@ impl ProjectSearchView {
fn entity_changed(&mut self, window: &mut Window, cx: &mut Context<Self>) {
let match_ranges = self.entity.read(cx).match_ranges.clone();
self.results_editor.update(cx, |editor, cx| {
editor.set_initial_multibuffer_matches(match_ranges.clone());
});
if match_ranges.is_empty() {
self.active_match_index = None;

View File

@@ -146,13 +146,11 @@ impl RenderOnce for Divider {
let base = match self.direction {
DividerDirection::Horizontal => div()
.min_w_0()
.flex_none()
.h_px()
.w_full()
.when(self.inset, |this| this.mx_1p5()),
DividerDirection::Vertical => div()
.min_w_0()
.flex_none()
.w_px()
.h_full()
.when(self.inset, |this| this.my_1p5()),

View File

@@ -1529,11 +1529,14 @@ fn generate_commands(_: &App) -> Vec<VimCommand> {
VimCommand::str(("cl", "ist"), "diagnostics::Deploy"),
VimCommand::new(("cc", ""), editor::actions::Hover),
VimCommand::new(("ll", ""), editor::actions::Hover),
VimCommand::new(("cn", "ext"), editor::actions::GoToDiagnostic::default())
.range(wrap_count),
VimCommand::new(
("cn", "ext"),
editor::actions::SelectNextMultibufferMatch::default(),
)
.range(wrap_count),
VimCommand::new(
("cp", "revious"),
editor::actions::GoToPreviousDiagnostic::default(),
editor::actions::SelectPreviousMultibufferMatch::default(),
)
.range(wrap_count),
VimCommand::new(