Merge branch 'main' into find-path-tool

This commit is contained in:
Ben Brandt 2025-08-07 17:48:43 +02:00
commit afb9554a28
No known key found for this signature in database
GPG key ID: D4618C5D3B500571
30 changed files with 1113 additions and 570 deletions

8
Cargo.lock generated
View file

@ -7506,9 +7506,9 @@ dependencies = [
[[package]] [[package]]
name = "grid" name = "grid"
version = "0.17.0" version = "0.18.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "71b01d27060ad58be4663b9e4ac9e2d4806918e8876af8912afbddd1a91d5eaa" checksum = "12101ecc8225ea6d675bc70263074eab6169079621c2186fe0c66590b2df9681"
[[package]] [[package]]
name = "group" name = "group"
@ -16219,9 +16219,9 @@ dependencies = [
[[package]] [[package]]
name = "taffy" name = "taffy"
version = "0.8.3" version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7aaef0ac998e6527d6d0d5582f7e43953bb17221ac75bb8eb2fcc2db3396db1c" checksum = "a13e5d13f79d558b5d353a98072ca8ca0e99da429467804de959aa8c83c9a004"
dependencies = [ dependencies = [
"arrayvec", "arrayvec",
"grid", "grid",

View file

@ -1,6 +1,6 @@
# syntax = docker/dockerfile:1.2 # syntax = docker/dockerfile:1.2
FROM rust:1.88-bookworm as builder FROM rust:1.89-bookworm as builder
WORKDIR app WORKDIR app
COPY . . COPY . .

View file

@ -33,6 +33,10 @@ pub trait Template: Sized {
} }
} }
#[expect(
dead_code,
reason = "Marked as unused by Rust 1.89 and left as is as of 07 Aug 2025 to let AI team address it."
)]
#[derive(Serialize)] #[derive(Serialize)]
pub struct GlobTemplate { pub struct GlobTemplate {
pub project_roots: String, pub project_roots: String,

View file

@ -1630,15 +1630,15 @@ fn notify_rejoined_projects(
} }
// Stream this worktree's diagnostics. // Stream this worktree's diagnostics.
for summary in worktree.diagnostic_summaries { let mut worktree_diagnostics = worktree.diagnostic_summaries.into_iter();
session.peer.send( if let Some(summary) = worktree_diagnostics.next() {
session.connection_id, let message = proto::UpdateDiagnosticSummary {
proto::UpdateDiagnosticSummary { project_id: project.id.to_proto(),
project_id: project.id.to_proto(), worktree_id: worktree.id,
worktree_id: worktree.id, summary: Some(summary),
summary: Some(summary), more_summaries: worktree_diagnostics.collect(),
}, };
)?; session.peer.send(session.connection_id, message)?;
} }
for settings_file in worktree.settings_files { for settings_file in worktree.settings_files {
@ -2060,15 +2060,15 @@ async fn join_project(
} }
// Stream this worktree's diagnostics. // Stream this worktree's diagnostics.
for summary in worktree.diagnostic_summaries { let mut worktree_diagnostics = worktree.diagnostic_summaries.into_iter();
session.peer.send( if let Some(summary) = worktree_diagnostics.next() {
session.connection_id, let message = proto::UpdateDiagnosticSummary {
proto::UpdateDiagnosticSummary { project_id: project.id.to_proto(),
project_id: project_id.to_proto(), worktree_id: worktree.id,
worktree_id: worktree.id, summary: Some(summary),
summary: Some(summary), more_summaries: worktree_diagnostics.collect(),
}, };
)?; session.peer.send(session.connection_id, message)?;
} }
for settings_file in worktree.settings_files { for settings_file in worktree.settings_files {

View file

@ -58,11 +58,19 @@ impl EditPredictionProvider for CopilotCompletionProvider {
} }
fn show_completions_in_menu() -> bool { fn show_completions_in_menu() -> bool {
true
}
fn show_tab_accept_marker() -> bool {
true
}
fn supports_jump_to_edit() -> bool {
false false
} }
fn is_refreshing(&self) -> bool { fn is_refreshing(&self) -> bool {
self.pending_refresh.is_some() self.pending_refresh.is_some() && self.completions.is_empty()
} }
fn is_enabled( fn is_enabled(
@ -343,8 +351,8 @@ mod tests {
executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT); executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
cx.update_editor(|editor, window, cx| { cx.update_editor(|editor, window, cx| {
assert!(editor.context_menu_visible()); assert!(editor.context_menu_visible());
assert!(!editor.has_active_edit_prediction()); assert!(editor.has_active_edit_prediction());
// Since we have both, the copilot suggestion is not shown inline // Since we have both, the copilot suggestion is existing but does not show up as ghost text
assert_eq!(editor.text(cx), "one.\ntwo\nthree\n"); assert_eq!(editor.text(cx), "one.\ntwo\nthree\n");
assert_eq!(editor.display_text(cx), "one.\ntwo\nthree\n"); assert_eq!(editor.display_text(cx), "one.\ntwo\nthree\n");
@ -934,8 +942,9 @@ mod tests {
executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT); executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
cx.update_editor(|editor, _, cx| { cx.update_editor(|editor, _, cx| {
assert!(editor.context_menu_visible()); assert!(editor.context_menu_visible());
assert!(!editor.has_active_edit_prediction(),); assert!(editor.has_active_edit_prediction());
assert_eq!(editor.text(cx), "one\ntwo.\nthree\n"); assert_eq!(editor.text(cx), "one\ntwo.\nthree\n");
assert_eq!(editor.display_text(cx), "one\ntwo.\nthree\n");
}); });
} }
@ -1077,8 +1086,6 @@ mod tests {
vec![complete_from_marker.clone(), replace_range_marker.clone()], vec![complete_from_marker.clone(), replace_range_marker.clone()],
); );
let complete_from_position =
cx.to_lsp(marked_ranges.remove(&complete_from_marker).unwrap()[0].start);
let replace_range = let replace_range =
cx.to_lsp_range(marked_ranges.remove(&replace_range_marker).unwrap()[0].clone()); cx.to_lsp_range(marked_ranges.remove(&replace_range_marker).unwrap()[0].clone());
@ -1087,10 +1094,6 @@ mod tests {
let completions = completions.clone(); let completions = completions.clone();
async move { async move {
assert_eq!(params.text_document_position.text_document.uri, url.clone()); assert_eq!(params.text_document_position.text_document.uri, url.clone());
assert_eq!(
params.text_document_position.position,
complete_from_position
);
Ok(Some(lsp::CompletionResponse::Array( Ok(Some(lsp::CompletionResponse::Array(
completions completions
.iter() .iter()

View file

@ -177,9 +177,9 @@ impl ProjectDiagnosticsEditor {
} }
project::Event::DiagnosticsUpdated { project::Event::DiagnosticsUpdated {
language_server_id, language_server_id,
path, paths,
} => { } => {
this.paths_to_update.insert(path.clone()); this.paths_to_update.extend(paths.clone());
let project = project.clone(); let project = project.clone();
this.diagnostic_summary_update = cx.spawn(async move |this, cx| { this.diagnostic_summary_update = cx.spawn(async move |this, cx| {
cx.background_executor() cx.background_executor()
@ -193,9 +193,9 @@ impl ProjectDiagnosticsEditor {
cx.emit(EditorEvent::TitleChanged); cx.emit(EditorEvent::TitleChanged);
if this.editor.focus_handle(cx).contains_focused(window, cx) || this.focus_handle.contains_focused(window, cx) { if this.editor.focus_handle(cx).contains_focused(window, cx) || this.focus_handle.contains_focused(window, cx) {
log::debug!("diagnostics updated for server {language_server_id}, path {path:?}. recording change"); log::debug!("diagnostics updated for server {language_server_id}, paths {paths:?}. recording change");
} else { } else {
log::debug!("diagnostics updated for server {language_server_id}, path {path:?}. updating excerpts"); log::debug!("diagnostics updated for server {language_server_id}, paths {paths:?}. updating excerpts");
this.update_stale_excerpts(window, cx); this.update_stale_excerpts(window, cx);
} }
} }

View file

@ -61,6 +61,10 @@ pub trait EditPredictionProvider: 'static + Sized {
fn show_tab_accept_marker() -> bool { fn show_tab_accept_marker() -> bool {
false false
} }
fn supports_jump_to_edit() -> bool {
true
}
fn data_collection_state(&self, _cx: &App) -> DataCollectionState { fn data_collection_state(&self, _cx: &App) -> DataCollectionState {
DataCollectionState::Unsupported DataCollectionState::Unsupported
} }
@ -116,6 +120,7 @@ pub trait EditPredictionProviderHandle {
) -> bool; ) -> bool;
fn show_completions_in_menu(&self) -> bool; fn show_completions_in_menu(&self) -> bool;
fn show_tab_accept_marker(&self) -> bool; fn show_tab_accept_marker(&self) -> bool;
fn supports_jump_to_edit(&self) -> bool;
fn data_collection_state(&self, cx: &App) -> DataCollectionState; fn data_collection_state(&self, cx: &App) -> DataCollectionState;
fn usage(&self, cx: &App) -> Option<EditPredictionUsage>; fn usage(&self, cx: &App) -> Option<EditPredictionUsage>;
fn toggle_data_collection(&self, cx: &mut App); fn toggle_data_collection(&self, cx: &mut App);
@ -166,6 +171,10 @@ where
T::show_tab_accept_marker() T::show_tab_accept_marker()
} }
fn supports_jump_to_edit(&self) -> bool {
T::supports_jump_to_edit()
}
fn data_collection_state(&self, cx: &App) -> DataCollectionState { fn data_collection_state(&self, cx: &App) -> DataCollectionState {
self.read(cx).data_collection_state(cx) self.read(cx).data_collection_state(cx)
} }

View file

@ -491,7 +491,12 @@ impl EditPredictionButton {
let subtle_mode = matches!(current_mode, EditPredictionsMode::Subtle); let subtle_mode = matches!(current_mode, EditPredictionsMode::Subtle);
let eager_mode = matches!(current_mode, EditPredictionsMode::Eager); let eager_mode = matches!(current_mode, EditPredictionsMode::Eager);
if matches!(provider, EditPredictionProvider::Zed) { if matches!(
provider,
EditPredictionProvider::Zed
| EditPredictionProvider::Copilot
| EditPredictionProvider::Supermaven
) {
menu = menu menu = menu
.separator() .separator()
.header("Display Modes") .header("Display Modes")

View file

@ -228,6 +228,49 @@ async fn test_edit_prediction_invalidation_range(cx: &mut gpui::TestAppContext)
}); });
} }
#[gpui::test]
async fn test_edit_prediction_jump_disabled_for_non_zed_providers(cx: &mut gpui::TestAppContext) {
init_test(cx, |_| {});
let mut cx = EditorTestContext::new(cx).await;
let provider = cx.new(|_| FakeNonZedEditPredictionProvider::default());
assign_editor_completion_provider_non_zed(provider.clone(), &mut cx);
// Cursor is 2+ lines above the proposed edit
cx.set_state(indoc! {"
line 0
line ˇ1
line 2
line 3
line
"});
propose_edits_non_zed(
&provider,
vec![(Point::new(4, 3)..Point::new(4, 3), " 4")],
&mut cx,
);
cx.update_editor(|editor, window, cx| editor.update_visible_edit_prediction(window, cx));
// For non-Zed providers, there should be no move completion (jump functionality disabled)
cx.editor(|editor, _, _| {
if let Some(completion_state) = &editor.active_edit_prediction {
// Should be an Edit prediction, not a Move prediction
match &completion_state.completion {
EditPrediction::Edit { .. } => {
// This is expected for non-Zed providers
}
EditPrediction::Move { .. } => {
panic!(
"Non-Zed providers should not show Move predictions (jump functionality)"
);
}
}
}
});
}
fn assert_editor_active_edit_completion( fn assert_editor_active_edit_completion(
cx: &mut EditorTestContext, cx: &mut EditorTestContext,
assert: impl FnOnce(MultiBufferSnapshot, &Vec<(Range<Anchor>, String)>), assert: impl FnOnce(MultiBufferSnapshot, &Vec<(Range<Anchor>, String)>),
@ -301,6 +344,37 @@ fn assign_editor_completion_provider(
}) })
} }
fn propose_edits_non_zed<T: ToOffset>(
provider: &Entity<FakeNonZedEditPredictionProvider>,
edits: Vec<(Range<T>, &str)>,
cx: &mut EditorTestContext,
) {
let snapshot = cx.buffer_snapshot();
let edits = edits.into_iter().map(|(range, text)| {
let range = snapshot.anchor_after(range.start)..snapshot.anchor_before(range.end);
(range, text.into())
});
cx.update(|_, cx| {
provider.update(cx, |provider, _| {
provider.set_edit_prediction(Some(edit_prediction::EditPrediction {
id: None,
edits: edits.collect(),
edit_preview: None,
}))
})
});
}
fn assign_editor_completion_provider_non_zed(
provider: Entity<FakeNonZedEditPredictionProvider>,
cx: &mut EditorTestContext,
) {
cx.update_editor(|editor, window, cx| {
editor.set_edit_prediction_provider(Some(provider), window, cx);
})
}
#[derive(Default, Clone)] #[derive(Default, Clone)]
pub struct FakeEditPredictionProvider { pub struct FakeEditPredictionProvider {
pub completion: Option<edit_prediction::EditPrediction>, pub completion: Option<edit_prediction::EditPrediction>,
@ -325,6 +399,84 @@ impl EditPredictionProvider for FakeEditPredictionProvider {
false false
} }
fn supports_jump_to_edit() -> bool {
true
}
fn is_enabled(
&self,
_buffer: &gpui::Entity<language::Buffer>,
_cursor_position: language::Anchor,
_cx: &gpui::App,
) -> bool {
true
}
fn is_refreshing(&self) -> bool {
false
}
fn refresh(
&mut self,
_project: Option<Entity<Project>>,
_buffer: gpui::Entity<language::Buffer>,
_cursor_position: language::Anchor,
_debounce: bool,
_cx: &mut gpui::Context<Self>,
) {
}
fn cycle(
&mut self,
_buffer: gpui::Entity<language::Buffer>,
_cursor_position: language::Anchor,
_direction: edit_prediction::Direction,
_cx: &mut gpui::Context<Self>,
) {
}
fn accept(&mut self, _cx: &mut gpui::Context<Self>) {}
fn discard(&mut self, _cx: &mut gpui::Context<Self>) {}
fn suggest<'a>(
&mut self,
_buffer: &gpui::Entity<language::Buffer>,
_cursor_position: language::Anchor,
_cx: &mut gpui::Context<Self>,
) -> Option<edit_prediction::EditPrediction> {
self.completion.clone()
}
}
#[derive(Default, Clone)]
pub struct FakeNonZedEditPredictionProvider {
pub completion: Option<edit_prediction::EditPrediction>,
}
impl FakeNonZedEditPredictionProvider {
pub fn set_edit_prediction(&mut self, completion: Option<edit_prediction::EditPrediction>) {
self.completion = completion;
}
}
impl EditPredictionProvider for FakeNonZedEditPredictionProvider {
fn name() -> &'static str {
"fake-non-zed-provider"
}
fn display_name() -> &'static str {
"Fake Non-Zed Provider"
}
fn show_completions_in_menu() -> bool {
false
}
fn supports_jump_to_edit() -> bool {
false
}
fn is_enabled( fn is_enabled(
&self, &self,
_buffer: &gpui::Entity<language::Buffer>, _buffer: &gpui::Entity<language::Buffer>,

View file

@ -7760,8 +7760,14 @@ impl Editor {
} else { } else {
None None
}; };
let is_move = let supports_jump = self
move_invalidation_row_range.is_some() || self.edit_predictions_hidden_for_vim_mode; .edit_prediction_provider
.as_ref()
.map(|provider| provider.provider.supports_jump_to_edit())
.unwrap_or(true);
let is_move = supports_jump
&& (move_invalidation_row_range.is_some() || self.edit_predictions_hidden_for_vim_mode);
let completion = if is_move { let completion = if is_move {
invalidation_row_range = invalidation_row_range =
move_invalidation_row_range.unwrap_or(edit_start_row..edit_end_row); move_invalidation_row_range.unwrap_or(edit_start_row..edit_end_row);
@ -8799,8 +8805,12 @@ impl Editor {
return None; return None;
} }
let highlighted_edits = let highlighted_edits = if let Some(edit_preview) = edit_preview.as_ref() {
crate::edit_prediction_edit_text(&snapshot, edits, edit_preview.as_ref()?, false, cx); crate::edit_prediction_edit_text(&snapshot, edits, edit_preview, false, cx)
} else {
// Fallback for providers without edit_preview
crate::edit_prediction_fallback_text(edits, cx)
};
let styled_text = highlighted_edits.to_styled_text(&style.text); let styled_text = highlighted_edits.to_styled_text(&style.text);
let line_count = highlighted_edits.text.lines().count(); let line_count = highlighted_edits.text.lines().count();
@ -9068,6 +9078,18 @@ impl Editor {
let editor_bg_color = cx.theme().colors().editor_background; let editor_bg_color = cx.theme().colors().editor_background;
editor_bg_color.blend(accent_color.opacity(0.6)) editor_bg_color.blend(accent_color.opacity(0.6))
} }
fn get_prediction_provider_icon_name(
provider: &Option<RegisteredEditPredictionProvider>,
) -> IconName {
match provider {
Some(provider) => match provider.provider.name() {
"copilot" => IconName::Copilot,
"supermaven" => IconName::Supermaven,
_ => IconName::ZedPredict,
},
None => IconName::ZedPredict,
}
}
fn render_edit_prediction_cursor_popover( fn render_edit_prediction_cursor_popover(
&self, &self,
@ -9080,6 +9102,7 @@ impl Editor {
cx: &mut Context<Editor>, cx: &mut Context<Editor>,
) -> Option<AnyElement> { ) -> Option<AnyElement> {
let provider = self.edit_prediction_provider.as_ref()?; let provider = self.edit_prediction_provider.as_ref()?;
let provider_icon = Self::get_prediction_provider_icon_name(&self.edit_prediction_provider);
if provider.provider.needs_terms_acceptance(cx) { if provider.provider.needs_terms_acceptance(cx) {
return Some( return Some(
@ -9106,7 +9129,7 @@ impl Editor {
h_flex() h_flex()
.flex_1() .flex_1()
.gap_2() .gap_2()
.child(Icon::new(IconName::ZedPredict)) .child(Icon::new(provider_icon))
.child(Label::new("Accept Terms of Service")) .child(Label::new("Accept Terms of Service"))
.child(div().w_full()) .child(div().w_full())
.child( .child(
@ -9122,12 +9145,8 @@ impl Editor {
let is_refreshing = provider.provider.is_refreshing(cx); let is_refreshing = provider.provider.is_refreshing(cx);
fn pending_completion_container() -> Div { fn pending_completion_container(icon: IconName) -> Div {
h_flex() h_flex().h_full().flex_1().gap_2().child(Icon::new(icon))
.h_full()
.flex_1()
.gap_2()
.child(Icon::new(IconName::ZedPredict))
} }
let completion = match &self.active_edit_prediction { let completion = match &self.active_edit_prediction {
@ -9157,7 +9176,7 @@ impl Editor {
Icon::new(IconName::ZedPredictUp) Icon::new(IconName::ZedPredictUp)
} }
} }
EditPrediction::Edit { .. } => Icon::new(IconName::ZedPredict), EditPrediction::Edit { .. } => Icon::new(provider_icon),
})) }))
.child( .child(
h_flex() h_flex()
@ -9224,15 +9243,15 @@ impl Editor {
cx, cx,
)?, )?,
None => { None => pending_completion_container(provider_icon)
pending_completion_container().child(Label::new("...").size(LabelSize::Small)) .child(Label::new("...").size(LabelSize::Small)),
}
}, },
None => pending_completion_container().child(Label::new("No Prediction")), None => pending_completion_container(provider_icon)
.child(Label::new("...").size(LabelSize::Small)),
}; };
let completion = if is_refreshing { let completion = if is_refreshing || self.active_edit_prediction.is_none() {
completion completion
.with_animation( .with_animation(
"loading-completion", "loading-completion",
@ -9332,23 +9351,35 @@ impl Editor {
.child(Icon::new(arrow).color(Color::Muted).size(IconSize::Small)) .child(Icon::new(arrow).color(Color::Muted).size(IconSize::Small))
} }
let supports_jump = self
.edit_prediction_provider
.as_ref()
.map(|provider| provider.provider.supports_jump_to_edit())
.unwrap_or(true);
match &completion.completion { match &completion.completion {
EditPrediction::Move { EditPrediction::Move {
target, snapshot, .. target, snapshot, ..
} => Some( } => {
h_flex() if !supports_jump {
.px_2() return None;
.gap_2() }
.flex_1()
.child( Some(
if target.text_anchor.to_point(&snapshot).row > cursor_point.row { h_flex()
Icon::new(IconName::ZedPredictDown) .px_2()
} else { .gap_2()
Icon::new(IconName::ZedPredictUp) .flex_1()
}, .child(
) if target.text_anchor.to_point(&snapshot).row > cursor_point.row {
.child(Label::new("Jump to Edit")), Icon::new(IconName::ZedPredictDown)
), } else {
Icon::new(IconName::ZedPredictUp)
},
)
.child(Label::new("Jump to Edit")),
)
}
EditPrediction::Edit { EditPrediction::Edit {
edits, edits,
@ -9358,14 +9389,13 @@ impl Editor {
} => { } => {
let first_edit_row = edits.first()?.0.start.text_anchor.to_point(&snapshot).row; let first_edit_row = edits.first()?.0.start.text_anchor.to_point(&snapshot).row;
let (highlighted_edits, has_more_lines) = crate::edit_prediction_edit_text( let (highlighted_edits, has_more_lines) =
&snapshot, if let Some(edit_preview) = edit_preview.as_ref() {
&edits, crate::edit_prediction_edit_text(&snapshot, &edits, edit_preview, true, cx)
edit_preview.as_ref()?, .first_line_preview()
true, } else {
cx, crate::edit_prediction_fallback_text(&edits, cx).first_line_preview()
) };
.first_line_preview();
let styled_text = gpui::StyledText::new(highlighted_edits.text) let styled_text = gpui::StyledText::new(highlighted_edits.text)
.with_default_highlights(&style.text, highlighted_edits.highlights); .with_default_highlights(&style.text, highlighted_edits.highlights);
@ -9376,11 +9406,13 @@ impl Editor {
.child(styled_text) .child(styled_text)
.when(has_more_lines, |parent| parent.child("")); .when(has_more_lines, |parent| parent.child(""));
let left = if first_edit_row != cursor_point.row { let left = if supports_jump && first_edit_row != cursor_point.row {
render_relative_row_jump("", cursor_point.row, first_edit_row) render_relative_row_jump("", cursor_point.row, first_edit_row)
.into_any_element() .into_any_element()
} else { } else {
Icon::new(IconName::ZedPredict).into_any_element() let icon_name =
Editor::get_prediction_provider_icon_name(&self.edit_prediction_provider);
Icon::new(icon_name).into_any_element()
}; };
Some( Some(
@ -23270,6 +23302,33 @@ fn edit_prediction_edit_text(
edit_preview.highlight_edits(current_snapshot, &edits, include_deletions, cx) edit_preview.highlight_edits(current_snapshot, &edits, include_deletions, cx)
} }
fn edit_prediction_fallback_text(edits: &[(Range<Anchor>, String)], cx: &App) -> HighlightedText {
// Fallback for providers that don't provide edit_preview (like Copilot/Supermaven)
// Just show the raw edit text with basic styling
let mut text = String::new();
let mut highlights = Vec::new();
let insertion_highlight_style = HighlightStyle {
color: Some(cx.theme().colors().text),
..Default::default()
};
for (_, edit_text) in edits {
let start_offset = text.len();
text.push_str(edit_text);
let end_offset = text.len();
if start_offset < end_offset {
highlights.push((start_offset..end_offset, insertion_highlight_style));
}
}
HighlightedText {
text: text.into(),
highlights,
}
}
pub fn diagnostic_style(severity: lsp::DiagnosticSeverity, colors: &StatusColors) -> Hsla { pub fn diagnostic_style(severity: lsp::DiagnosticSeverity, colors: &StatusColors) -> Hsla {
match severity { match severity {
lsp::DiagnosticSeverity::ERROR => colors.error, lsp::DiagnosticSeverity::ERROR => colors.error,

View file

@ -3682,6 +3682,7 @@ impl EditorElement {
.id("path header block") .id("path header block")
.size_full() .size_full()
.justify_between() .justify_between()
.overflow_hidden()
.child( .child(
h_flex() h_flex()
.gap_2() .gap_2()

View file

@ -402,11 +402,11 @@ impl GitRepository for FakeGitRepository {
&self, &self,
_paths: Vec<RepoPath>, _paths: Vec<RepoPath>,
_env: Arc<HashMap<String, String>>, _env: Arc<HashMap<String, String>>,
) -> BoxFuture<Result<()>> { ) -> BoxFuture<'_, Result<()>> {
unimplemented!() unimplemented!()
} }
fn stash_pop(&self, _env: Arc<HashMap<String, String>>) -> BoxFuture<Result<()>> { fn stash_pop(&self, _env: Arc<HashMap<String, String>>) -> BoxFuture<'_, Result<()>> {
unimplemented!() unimplemented!()
} }

View file

@ -1,6 +1,9 @@
use notify::EventKind; use notify::EventKind;
use parking_lot::Mutex; use parking_lot::Mutex;
use std::sync::{Arc, OnceLock}; use std::{
collections::HashMap,
sync::{Arc, OnceLock},
};
use util::{ResultExt, paths::SanitizedPath}; use util::{ResultExt, paths::SanitizedPath};
use crate::{PathEvent, PathEventKind, Watcher}; use crate::{PathEvent, PathEventKind, Watcher};
@ -8,6 +11,7 @@ use crate::{PathEvent, PathEventKind, Watcher};
pub struct FsWatcher { pub struct FsWatcher {
tx: smol::channel::Sender<()>, tx: smol::channel::Sender<()>,
pending_path_events: Arc<Mutex<Vec<PathEvent>>>, pending_path_events: Arc<Mutex<Vec<PathEvent>>>,
registrations: Mutex<HashMap<Arc<std::path::Path>, WatcherRegistrationId>>,
} }
impl FsWatcher { impl FsWatcher {
@ -18,10 +22,24 @@ impl FsWatcher {
Self { Self {
tx, tx,
pending_path_events, pending_path_events,
registrations: Default::default(),
} }
} }
} }
impl Drop for FsWatcher {
fn drop(&mut self) {
let mut registrations = self.registrations.lock();
let registrations = registrations.drain();
let _ = global(|g| {
for (_, registration) in registrations {
g.remove(registration);
}
});
}
}
impl Watcher for FsWatcher { impl Watcher for FsWatcher {
fn add(&self, path: &std::path::Path) -> anyhow::Result<()> { fn add(&self, path: &std::path::Path) -> anyhow::Result<()> {
let root_path = SanitizedPath::from(path); let root_path = SanitizedPath::from(path);
@ -29,75 +47,136 @@ impl Watcher for FsWatcher {
let tx = self.tx.clone(); let tx = self.tx.clone();
let pending_paths = self.pending_path_events.clone(); let pending_paths = self.pending_path_events.clone();
use notify::Watcher; let path: Arc<std::path::Path> = path.into();
global({ if self.registrations.lock().contains_key(&path) {
return Ok(());
}
let registration_id = global({
let path = path.clone();
|g| { |g| {
g.add(move |event: &notify::Event| { g.add(
let kind = match event.kind { path,
EventKind::Create(_) => Some(PathEventKind::Created), notify::RecursiveMode::NonRecursive,
EventKind::Modify(_) => Some(PathEventKind::Changed), move |event: &notify::Event| {
EventKind::Remove(_) => Some(PathEventKind::Removed), let kind = match event.kind {
_ => None, EventKind::Create(_) => Some(PathEventKind::Created),
}; EventKind::Modify(_) => Some(PathEventKind::Changed),
let mut path_events = event EventKind::Remove(_) => Some(PathEventKind::Removed),
.paths _ => None,
.iter() };
.filter_map(|event_path| { let mut path_events = event
let event_path = SanitizedPath::from(event_path); .paths
event_path.starts_with(&root_path).then(|| PathEvent { .iter()
path: event_path.as_path().to_path_buf(), .filter_map(|event_path| {
kind, let event_path = SanitizedPath::from(event_path);
event_path.starts_with(&root_path).then(|| PathEvent {
path: event_path.as_path().to_path_buf(),
kind,
})
}) })
}) .collect::<Vec<_>>();
.collect::<Vec<_>>();
if !path_events.is_empty() { if !path_events.is_empty() {
path_events.sort(); path_events.sort();
let mut pending_paths = pending_paths.lock(); let mut pending_paths = pending_paths.lock();
if pending_paths.is_empty() { if pending_paths.is_empty() {
tx.try_send(()).ok(); tx.try_send(()).ok();
}
util::extend_sorted(
&mut *pending_paths,
path_events,
usize::MAX,
|a, b| a.path.cmp(&b.path),
);
} }
util::extend_sorted( },
&mut *pending_paths, )
path_events,
usize::MAX,
|a, b| a.path.cmp(&b.path),
);
}
})
} }
})?;
global(|g| {
g.watcher
.lock()
.watch(path, notify::RecursiveMode::NonRecursive)
})??; })??;
self.registrations.lock().insert(path, registration_id);
Ok(()) Ok(())
} }
fn remove(&self, path: &std::path::Path) -> anyhow::Result<()> { fn remove(&self, path: &std::path::Path) -> anyhow::Result<()> {
use notify::Watcher; let Some(registration) = self.registrations.lock().remove(path) else {
Ok(global(|w| w.watcher.lock().unwatch(path))??) return Ok(());
};
global(|w| w.remove(registration))
} }
} }
pub struct GlobalWatcher { #[derive(Default, Debug, Copy, Clone, PartialEq, Eq, Hash)]
pub struct WatcherRegistrationId(u32);
struct WatcherRegistrationState {
callback: Box<dyn Fn(&notify::Event) + Send + Sync>,
path: Arc<std::path::Path>,
}
struct WatcherState {
// two mutexes because calling watcher.add triggers an watcher.event, which needs watchers. // two mutexes because calling watcher.add triggers an watcher.event, which needs watchers.
#[cfg(target_os = "linux")] #[cfg(target_os = "linux")]
pub(super) watcher: Mutex<notify::INotifyWatcher>, watcher: notify::INotifyWatcher,
#[cfg(target_os = "freebsd")] #[cfg(target_os = "freebsd")]
pub(super) watcher: Mutex<notify::KqueueWatcher>, watcher: notify::KqueueWatcher,
#[cfg(target_os = "windows")] #[cfg(target_os = "windows")]
pub(super) watcher: Mutex<notify::ReadDirectoryChangesWatcher>, watcher: notify::ReadDirectoryChangesWatcher,
pub(super) watchers: Mutex<Vec<Box<dyn Fn(&notify::Event) + Send + Sync>>>,
watchers: HashMap<WatcherRegistrationId, WatcherRegistrationState>,
path_registrations: HashMap<Arc<std::path::Path>, u32>,
last_registration: WatcherRegistrationId,
}
pub struct GlobalWatcher {
state: Mutex<WatcherState>,
} }
impl GlobalWatcher { impl GlobalWatcher {
pub(super) fn add(&self, cb: impl Fn(&notify::Event) + Send + Sync + 'static) { #[must_use]
self.watchers.lock().push(Box::new(cb)) fn add(
&self,
path: Arc<std::path::Path>,
mode: notify::RecursiveMode,
cb: impl Fn(&notify::Event) + Send + Sync + 'static,
) -> anyhow::Result<WatcherRegistrationId> {
use notify::Watcher;
let mut state = self.state.lock();
state.watcher.watch(&path, mode)?;
let id = state.last_registration;
state.last_registration = WatcherRegistrationId(id.0 + 1);
let registration_state = WatcherRegistrationState {
callback: Box::new(cb),
path: path.clone(),
};
state.watchers.insert(id, registration_state);
*state.path_registrations.entry(path.clone()).or_insert(0) += 1;
Ok(id)
}
pub fn remove(&self, id: WatcherRegistrationId) {
use notify::Watcher;
let mut state = self.state.lock();
let Some(registration_state) = state.watchers.remove(&id) else {
return;
};
let Some(count) = state.path_registrations.get_mut(&registration_state.path) else {
return;
};
*count -= 1;
if *count == 0 {
state.watcher.unwatch(&registration_state.path).log_err();
state.path_registrations.remove(&registration_state.path);
}
} }
} }
@ -114,8 +193,10 @@ fn handle_event(event: Result<notify::Event, notify::Error>) {
return; return;
}; };
global::<()>(move |watcher| { global::<()>(move |watcher| {
for f in watcher.watchers.lock().iter() { let state = watcher.state.lock();
f(&event) for registration in state.watchers.values() {
let callback = &registration.callback;
callback(&event);
} }
}) })
.log_err(); .log_err();
@ -124,8 +205,12 @@ fn handle_event(event: Result<notify::Event, notify::Error>) {
pub fn global<T>(f: impl FnOnce(&GlobalWatcher) -> T) -> anyhow::Result<T> { pub fn global<T>(f: impl FnOnce(&GlobalWatcher) -> T) -> anyhow::Result<T> {
let result = FS_WATCHER_INSTANCE.get_or_init(|| { let result = FS_WATCHER_INSTANCE.get_or_init(|| {
notify::recommended_watcher(handle_event).map(|file_watcher| GlobalWatcher { notify::recommended_watcher(handle_event).map(|file_watcher| GlobalWatcher {
watcher: Mutex::new(file_watcher), state: Mutex::new(WatcherState {
watchers: Default::default(), watcher: file_watcher,
watchers: Default::default(),
path_registrations: Default::default(),
last_registration: Default::default(),
}),
}) })
}); });
match result { match result {

View file

@ -399,9 +399,9 @@ pub trait GitRepository: Send + Sync {
&self, &self,
paths: Vec<RepoPath>, paths: Vec<RepoPath>,
env: Arc<HashMap<String, String>>, env: Arc<HashMap<String, String>>,
) -> BoxFuture<Result<()>>; ) -> BoxFuture<'_, Result<()>>;
fn stash_pop(&self, env: Arc<HashMap<String, String>>) -> BoxFuture<Result<()>>; fn stash_pop(&self, env: Arc<HashMap<String, String>>) -> BoxFuture<'_, Result<()>>;
fn push( fn push(
&self, &self,
@ -1203,7 +1203,7 @@ impl GitRepository for RealGitRepository {
&self, &self,
paths: Vec<RepoPath>, paths: Vec<RepoPath>,
env: Arc<HashMap<String, String>>, env: Arc<HashMap<String, String>>,
) -> BoxFuture<Result<()>> { ) -> BoxFuture<'_, Result<()>> {
let working_directory = self.working_directory(); let working_directory = self.working_directory();
self.executor self.executor
.spawn(async move { .spawn(async move {
@ -1227,7 +1227,7 @@ impl GitRepository for RealGitRepository {
.boxed() .boxed()
} }
fn stash_pop(&self, env: Arc<HashMap<String, String>>) -> BoxFuture<Result<()>> { fn stash_pop(&self, env: Arc<HashMap<String, String>>) -> BoxFuture<'_, Result<()>> {
let working_directory = self.working_directory(); let working_directory = self.working_directory();
self.executor self.executor
.spawn(async move { .spawn(async move {

View file

@ -121,7 +121,7 @@ smallvec.workspace = true
smol.workspace = true smol.workspace = true
strum.workspace = true strum.workspace = true
sum_tree.workspace = true sum_tree.workspace = true
taffy = "=0.8.3" taffy = "=0.9.0"
thiserror.workspace = true thiserror.workspace = true
util.workspace = true util.workspace = true
uuid.workspace = true uuid.workspace = true

View file

@ -461,6 +461,8 @@ fn skip_whitespace(source: &str) -> &str {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use core::slice;
use super::*; use super::*;
use crate as gpui; use crate as gpui;
use KeyBindingContextPredicate::*; use KeyBindingContextPredicate::*;
@ -674,11 +676,11 @@ mod tests {
assert!(predicate.eval(&contexts)); assert!(predicate.eval(&contexts));
assert!(!predicate.eval(&[])); assert!(!predicate.eval(&[]));
assert!(!predicate.eval(&[child_context.clone()])); assert!(!predicate.eval(slice::from_ref(&child_context)));
assert!(!predicate.eval(&[parent_context])); assert!(!predicate.eval(&[parent_context]));
let zany_predicate = KeyBindingContextPredicate::parse("child > child").unwrap(); let zany_predicate = KeyBindingContextPredicate::parse("child > child").unwrap();
assert!(!zany_predicate.eval(&[child_context.clone()])); assert!(!zany_predicate.eval(slice::from_ref(&child_context)));
assert!(zany_predicate.eval(&[child_context.clone(), child_context.clone()])); assert!(zany_predicate.eval(&[child_context.clone(), child_context.clone()]));
} }
@ -690,13 +692,13 @@ mod tests {
let parent_context = KeyContext::try_from("parent").unwrap(); let parent_context = KeyContext::try_from("parent").unwrap();
let child_context = KeyContext::try_from("child").unwrap(); let child_context = KeyContext::try_from("child").unwrap();
assert!(not_predicate.eval(&[workspace_context.clone()])); assert!(not_predicate.eval(slice::from_ref(&workspace_context)));
assert!(!not_predicate.eval(&[editor_context.clone()])); assert!(!not_predicate.eval(slice::from_ref(&editor_context)));
assert!(!not_predicate.eval(&[editor_context.clone(), workspace_context.clone()])); assert!(!not_predicate.eval(&[editor_context.clone(), workspace_context.clone()]));
assert!(!not_predicate.eval(&[workspace_context.clone(), editor_context.clone()])); assert!(!not_predicate.eval(&[workspace_context.clone(), editor_context.clone()]));
let complex_not = KeyBindingContextPredicate::parse("!editor && workspace").unwrap(); let complex_not = KeyBindingContextPredicate::parse("!editor && workspace").unwrap();
assert!(complex_not.eval(&[workspace_context.clone()])); assert!(complex_not.eval(slice::from_ref(&workspace_context)));
assert!(!complex_not.eval(&[editor_context.clone(), workspace_context.clone()])); assert!(!complex_not.eval(&[editor_context.clone(), workspace_context.clone()]));
let not_mode_predicate = KeyBindingContextPredicate::parse("!(mode == full)").unwrap(); let not_mode_predicate = KeyBindingContextPredicate::parse("!(mode == full)").unwrap();
@ -709,18 +711,18 @@ mod tests {
assert!(not_mode_predicate.eval(&[other_mode_context])); assert!(not_mode_predicate.eval(&[other_mode_context]));
let not_descendant = KeyBindingContextPredicate::parse("!(parent > child)").unwrap(); let not_descendant = KeyBindingContextPredicate::parse("!(parent > child)").unwrap();
assert!(not_descendant.eval(&[parent_context.clone()])); assert!(not_descendant.eval(slice::from_ref(&parent_context)));
assert!(not_descendant.eval(&[child_context.clone()])); assert!(not_descendant.eval(slice::from_ref(&child_context)));
assert!(!not_descendant.eval(&[parent_context.clone(), child_context.clone()])); assert!(!not_descendant.eval(&[parent_context.clone(), child_context.clone()]));
let not_descendant = KeyBindingContextPredicate::parse("parent > !child").unwrap(); let not_descendant = KeyBindingContextPredicate::parse("parent > !child").unwrap();
assert!(!not_descendant.eval(&[parent_context.clone()])); assert!(!not_descendant.eval(slice::from_ref(&parent_context)));
assert!(!not_descendant.eval(&[child_context.clone()])); assert!(!not_descendant.eval(slice::from_ref(&child_context)));
assert!(!not_descendant.eval(&[parent_context.clone(), child_context.clone()])); assert!(!not_descendant.eval(&[parent_context.clone(), child_context.clone()]));
let double_not = KeyBindingContextPredicate::parse("!!editor").unwrap(); let double_not = KeyBindingContextPredicate::parse("!!editor").unwrap();
assert!(double_not.eval(&[editor_context.clone()])); assert!(double_not.eval(slice::from_ref(&editor_context)));
assert!(!double_not.eval(&[workspace_context.clone()])); assert!(!double_not.eval(slice::from_ref(&workspace_context)));
// Test complex descendant cases // Test complex descendant cases
let workspace_context = KeyContext::try_from("Workspace").unwrap(); let workspace_context = KeyContext::try_from("Workspace").unwrap();
@ -754,9 +756,9 @@ mod tests {
// !Workspace - shouldn't match when Workspace is in the context // !Workspace - shouldn't match when Workspace is in the context
let not_workspace = KeyBindingContextPredicate::parse("!Workspace").unwrap(); let not_workspace = KeyBindingContextPredicate::parse("!Workspace").unwrap();
assert!(!not_workspace.eval(&[workspace_context.clone()])); assert!(!not_workspace.eval(slice::from_ref(&workspace_context)));
assert!(not_workspace.eval(&[pane_context.clone()])); assert!(not_workspace.eval(slice::from_ref(&pane_context)));
assert!(not_workspace.eval(&[editor_context.clone()])); assert!(not_workspace.eval(slice::from_ref(&editor_context)));
assert!(!not_workspace.eval(&workspace_pane_editor)); assert!(!not_workspace.eval(&workspace_pane_editor));
} }
} }

View file

@ -1,28 +1,6 @@
use std::ops::Deref; use std::ops::Deref;
use windows::Win32::{Foundation::HANDLE, UI::WindowsAndMessaging::HCURSOR}; use windows::Win32::UI::WindowsAndMessaging::HCURSOR;
#[derive(Debug, Clone, Copy)]
pub(crate) struct SafeHandle {
raw: HANDLE,
}
unsafe impl Send for SafeHandle {}
unsafe impl Sync for SafeHandle {}
impl From<HANDLE> for SafeHandle {
fn from(value: HANDLE) -> Self {
SafeHandle { raw: value }
}
}
impl Deref for SafeHandle {
type Target = HANDLE;
fn deref(&self) -> &Self::Target {
&self.raw
}
}
#[derive(Debug, Clone, Copy)] #[derive(Debug, Clone, Copy)]
pub(crate) struct SafeCursor { pub(crate) struct SafeCursor {

View file

@ -140,6 +140,20 @@ impl FormatTrigger {
} }
} }
#[derive(Debug)]
pub struct DocumentDiagnosticsUpdate<'a, D> {
pub diagnostics: D,
pub result_id: Option<String>,
pub server_id: LanguageServerId,
pub disk_based_sources: Cow<'a, [String]>,
}
pub struct DocumentDiagnostics {
diagnostics: Vec<DiagnosticEntry<Unclipped<PointUtf16>>>,
document_abs_path: PathBuf,
version: Option<i32>,
}
pub struct LocalLspStore { pub struct LocalLspStore {
weak: WeakEntity<LspStore>, weak: WeakEntity<LspStore>,
worktree_store: Entity<WorktreeStore>, worktree_store: Entity<WorktreeStore>,
@ -503,12 +517,16 @@ impl LocalLspStore {
adapter.process_diagnostics(&mut params, server_id, buffer); adapter.process_diagnostics(&mut params, server_id, buffer);
} }
this.merge_diagnostics( this.merge_lsp_diagnostics(
server_id,
params,
None,
DiagnosticSourceKind::Pushed, DiagnosticSourceKind::Pushed,
&adapter.disk_based_diagnostic_sources, vec![DocumentDiagnosticsUpdate {
server_id,
diagnostics: params,
result_id: None,
disk_based_sources: Cow::Borrowed(
&adapter.disk_based_diagnostic_sources,
),
}],
|_, diagnostic, cx| match diagnostic.source_kind { |_, diagnostic, cx| match diagnostic.source_kind {
DiagnosticSourceKind::Other | DiagnosticSourceKind::Pushed => { DiagnosticSourceKind::Other | DiagnosticSourceKind::Pushed => {
adapter.retain_old_diagnostic(diagnostic, cx) adapter.retain_old_diagnostic(diagnostic, cx)
@ -3610,8 +3628,8 @@ pub enum LspStoreEvent {
RefreshInlayHints, RefreshInlayHints,
RefreshCodeLens, RefreshCodeLens,
DiagnosticsUpdated { DiagnosticsUpdated {
language_server_id: LanguageServerId, server_id: LanguageServerId,
path: ProjectPath, paths: Vec<ProjectPath>,
}, },
DiskBasedDiagnosticsStarted { DiskBasedDiagnosticsStarted {
language_server_id: LanguageServerId, language_server_id: LanguageServerId,
@ -4440,17 +4458,24 @@ impl LspStore {
pub(crate) fn send_diagnostic_summaries(&self, worktree: &mut Worktree) { pub(crate) fn send_diagnostic_summaries(&self, worktree: &mut Worktree) {
if let Some((client, downstream_project_id)) = self.downstream_client.clone() { if let Some((client, downstream_project_id)) = self.downstream_client.clone() {
if let Some(summaries) = self.diagnostic_summaries.get(&worktree.id()) { if let Some(diangostic_summaries) = self.diagnostic_summaries.get(&worktree.id()) {
for (path, summaries) in summaries { let mut summaries =
for (&server_id, summary) in summaries { diangostic_summaries
client .into_iter()
.send(proto::UpdateDiagnosticSummary { .flat_map(|(path, summaries)| {
project_id: downstream_project_id, summaries
worktree_id: worktree.id().to_proto(), .into_iter()
summary: Some(summary.to_proto(server_id, path)), .map(|(server_id, summary)| summary.to_proto(*server_id, path))
}) });
.log_err(); if let Some(summary) = summaries.next() {
} client
.send(proto::UpdateDiagnosticSummary {
project_id: downstream_project_id,
worktree_id: worktree.id().to_proto(),
summary: Some(summary),
more_summaries: summaries.collect(),
})
.log_err();
} }
} }
} }
@ -6564,7 +6589,7 @@ impl LspStore {
&mut self, &mut self,
buffer: Entity<Buffer>, buffer: Entity<Buffer>,
cx: &mut Context<Self>, cx: &mut Context<Self>,
) -> Task<Result<Vec<LspPullDiagnostics>>> { ) -> Task<Result<Option<Vec<LspPullDiagnostics>>>> {
let buffer_id = buffer.read(cx).remote_id(); let buffer_id = buffer.read(cx).remote_id();
if let Some((client, upstream_project_id)) = self.upstream_client() { if let Some((client, upstream_project_id)) = self.upstream_client() {
@ -6575,7 +6600,7 @@ impl LspStore {
}, },
cx, cx,
) { ) {
return Task::ready(Ok(Vec::new())); return Task::ready(Ok(None));
} }
let request_task = client.request(proto::MultiLspQuery { let request_task = client.request(proto::MultiLspQuery {
buffer_id: buffer_id.to_proto(), buffer_id: buffer_id.to_proto(),
@ -6593,7 +6618,7 @@ impl LspStore {
)), )),
}); });
cx.background_spawn(async move { cx.background_spawn(async move {
Ok(request_task let _proto_responses = request_task
.await? .await?
.responses .responses
.into_iter() .into_iter()
@ -6606,8 +6631,11 @@ impl LspStore {
None None
} }
}) })
.flat_map(GetDocumentDiagnostics::diagnostics_from_proto) .collect::<Vec<_>>();
.collect()) // Proto requests cause the diagnostics to be pulled from language server(s) on the local side
// and then, buffer state updated with the diagnostics received, which will be later propagated to the client.
// Do not attempt to further process the dummy responses here.
Ok(None)
}) })
} else { } else {
let server_ids = buffer.update(cx, |buffer, cx| { let server_ids = buffer.update(cx, |buffer, cx| {
@ -6635,7 +6663,7 @@ impl LspStore {
for diagnostics in join_all(pull_diagnostics).await { for diagnostics in join_all(pull_diagnostics).await {
responses.extend(diagnostics?); responses.extend(diagnostics?);
} }
Ok(responses) Ok(Some(responses))
}) })
} }
} }
@ -6701,75 +6729,93 @@ impl LspStore {
buffer: Entity<Buffer>, buffer: Entity<Buffer>,
cx: &mut Context<Self>, cx: &mut Context<Self>,
) -> Task<anyhow::Result<()>> { ) -> Task<anyhow::Result<()>> {
let buffer_id = buffer.read(cx).remote_id();
let diagnostics = self.pull_diagnostics(buffer, cx); let diagnostics = self.pull_diagnostics(buffer, cx);
cx.spawn(async move |lsp_store, cx| { cx.spawn(async move |lsp_store, cx| {
let diagnostics = diagnostics.await.context("pulling diagnostics")?; let Some(diagnostics) = diagnostics.await.context("pulling diagnostics")? else {
return Ok(());
};
lsp_store.update(cx, |lsp_store, cx| { lsp_store.update(cx, |lsp_store, cx| {
if lsp_store.as_local().is_none() { if lsp_store.as_local().is_none() {
return; return;
} }
for diagnostics_set in diagnostics { let mut unchanged_buffers = HashSet::default();
let LspPullDiagnostics::Response { let mut changed_buffers = HashSet::default();
server_id, let server_diagnostics_updates = diagnostics
uri, .into_iter()
diagnostics, .filter_map(|diagnostics_set| match diagnostics_set {
} = diagnostics_set LspPullDiagnostics::Response {
else { server_id,
continue; uri,
};
let adapter = lsp_store.language_server_adapter_for_id(server_id);
let disk_based_sources = adapter
.as_ref()
.map(|adapter| adapter.disk_based_diagnostic_sources.as_slice())
.unwrap_or(&[]);
match diagnostics {
PulledDiagnostics::Unchanged { result_id } => {
lsp_store
.merge_diagnostics(
server_id,
lsp::PublishDiagnosticsParams {
uri: uri.clone(),
diagnostics: Vec::new(),
version: None,
},
Some(result_id),
DiagnosticSourceKind::Pulled,
disk_based_sources,
|_, _, _| true,
cx,
)
.log_err();
}
PulledDiagnostics::Changed {
diagnostics, diagnostics,
result_id, } => Some((server_id, uri, diagnostics)),
} => { LspPullDiagnostics::Default => None,
lsp_store })
.merge_diagnostics( .fold(
HashMap::default(),
|mut acc, (server_id, uri, diagnostics)| {
let (result_id, diagnostics) = match diagnostics {
PulledDiagnostics::Unchanged { result_id } => {
unchanged_buffers.insert(uri.clone());
(Some(result_id), Vec::new())
}
PulledDiagnostics::Changed {
result_id,
diagnostics,
} => {
changed_buffers.insert(uri.clone());
(result_id, diagnostics)
}
};
let disk_based_sources = Cow::Owned(
lsp_store
.language_server_adapter_for_id(server_id)
.as_ref()
.map(|adapter| adapter.disk_based_diagnostic_sources.as_slice())
.unwrap_or(&[])
.to_vec(),
);
acc.entry(server_id).or_insert_with(Vec::new).push(
DocumentDiagnosticsUpdate {
server_id, server_id,
lsp::PublishDiagnosticsParams { diagnostics: lsp::PublishDiagnosticsParams {
uri: uri.clone(), uri,
diagnostics, diagnostics,
version: None, version: None,
}, },
result_id, result_id,
DiagnosticSourceKind::Pulled,
disk_based_sources, disk_based_sources,
|buffer, old_diagnostic, _| match old_diagnostic.source_kind { },
DiagnosticSourceKind::Pulled => { );
buffer.remote_id() != buffer_id acc
} },
DiagnosticSourceKind::Other );
| DiagnosticSourceKind::Pushed => true,
}, for diagnostic_updates in server_diagnostics_updates.into_values() {
cx, lsp_store
) .merge_lsp_diagnostics(
.log_err(); DiagnosticSourceKind::Pulled,
} diagnostic_updates,
} |buffer, old_diagnostic, cx| {
File::from_dyn(buffer.file())
.and_then(|file| {
let abs_path = file.as_local()?.abs_path(cx);
lsp::Url::from_file_path(abs_path).ok()
})
.is_none_or(|buffer_uri| {
unchanged_buffers.contains(&buffer_uri)
|| match old_diagnostic.source_kind {
DiagnosticSourceKind::Pulled => {
!changed_buffers.contains(&buffer_uri)
}
DiagnosticSourceKind::Other
| DiagnosticSourceKind::Pushed => true,
}
})
},
cx,
)
.log_err();
} }
}) })
}) })
@ -7791,88 +7837,135 @@ impl LspStore {
cx: &mut Context<Self>, cx: &mut Context<Self>,
) -> anyhow::Result<()> { ) -> anyhow::Result<()> {
self.merge_diagnostic_entries( self.merge_diagnostic_entries(
server_id, vec![DocumentDiagnosticsUpdate {
abs_path, diagnostics: DocumentDiagnostics {
result_id, diagnostics,
version, document_abs_path: abs_path,
diagnostics, version,
},
result_id,
server_id,
disk_based_sources: Cow::Borrowed(&[]),
}],
|_, _, _| false, |_, _, _| false,
cx, cx,
)?; )?;
Ok(()) Ok(())
} }
pub fn merge_diagnostic_entries( pub fn merge_diagnostic_entries<'a>(
&mut self, &mut self,
server_id: LanguageServerId, diagnostic_updates: Vec<DocumentDiagnosticsUpdate<'a, DocumentDiagnostics>>,
abs_path: PathBuf, merge: impl Fn(&Buffer, &Diagnostic, &App) -> bool + Clone,
result_id: Option<String>,
version: Option<i32>,
mut diagnostics: Vec<DiagnosticEntry<Unclipped<PointUtf16>>>,
filter: impl Fn(&Buffer, &Diagnostic, &App) -> bool + Clone,
cx: &mut Context<Self>, cx: &mut Context<Self>,
) -> anyhow::Result<()> { ) -> anyhow::Result<()> {
let Some((worktree, relative_path)) = let mut diagnostics_summary = None::<proto::UpdateDiagnosticSummary>;
self.worktree_store.read(cx).find_worktree(&abs_path, cx) let mut updated_diagnostics_paths = HashMap::default();
else { for mut update in diagnostic_updates {
log::warn!("skipping diagnostics update, no worktree found for path {abs_path:?}"); let abs_path = &update.diagnostics.document_abs_path;
return Ok(()); let server_id = update.server_id;
}; let Some((worktree, relative_path)) =
self.worktree_store.read(cx).find_worktree(abs_path, cx)
else {
log::warn!("skipping diagnostics update, no worktree found for path {abs_path:?}");
return Ok(());
};
let project_path = ProjectPath { let worktree_id = worktree.read(cx).id();
worktree_id: worktree.read(cx).id(), let project_path = ProjectPath {
path: relative_path.into(), worktree_id,
}; path: relative_path.into(),
};
if let Some(buffer_handle) = self.buffer_store.read(cx).get_by_path(&project_path) { if let Some(buffer_handle) = self.buffer_store.read(cx).get_by_path(&project_path) {
let snapshot = buffer_handle.read(cx).snapshot(); let snapshot = buffer_handle.read(cx).snapshot();
let buffer = buffer_handle.read(cx); let buffer = buffer_handle.read(cx);
let reused_diagnostics = buffer let reused_diagnostics = buffer
.get_diagnostics(server_id) .get_diagnostics(server_id)
.into_iter() .into_iter()
.flat_map(|diag| { .flat_map(|diag| {
diag.iter() diag.iter()
.filter(|v| filter(buffer, &v.diagnostic, cx)) .filter(|v| merge(buffer, &v.diagnostic, cx))
.map(|v| { .map(|v| {
let start = Unclipped(v.range.start.to_point_utf16(&snapshot)); let start = Unclipped(v.range.start.to_point_utf16(&snapshot));
let end = Unclipped(v.range.end.to_point_utf16(&snapshot)); let end = Unclipped(v.range.end.to_point_utf16(&snapshot));
DiagnosticEntry { DiagnosticEntry {
range: start..end, range: start..end,
diagnostic: v.diagnostic.clone(), diagnostic: v.diagnostic.clone(),
} }
}) })
}) })
.collect::<Vec<_>>(); .collect::<Vec<_>>();
self.as_local_mut() self.as_local_mut()
.context("cannot merge diagnostics on a remote LspStore")? .context("cannot merge diagnostics on a remote LspStore")?
.update_buffer_diagnostics( .update_buffer_diagnostics(
&buffer_handle, &buffer_handle,
server_id,
update.result_id,
update.diagnostics.version,
update.diagnostics.diagnostics.clone(),
reused_diagnostics.clone(),
cx,
)?;
update.diagnostics.diagnostics.extend(reused_diagnostics);
}
let updated = worktree.update(cx, |worktree, cx| {
self.update_worktree_diagnostics(
worktree.id(),
server_id, server_id,
result_id, project_path.path.clone(),
version, update.diagnostics.diagnostics,
diagnostics.clone(),
reused_diagnostics.clone(),
cx, cx,
)?; )
})?;
diagnostics.extend(reused_diagnostics); match updated {
ControlFlow::Continue(new_summary) => {
if let Some((project_id, new_summary)) = new_summary {
match &mut diagnostics_summary {
Some(diagnostics_summary) => {
diagnostics_summary
.more_summaries
.push(proto::DiagnosticSummary {
path: project_path.path.as_ref().to_proto(),
language_server_id: server_id.0 as u64,
error_count: new_summary.error_count,
warning_count: new_summary.warning_count,
})
}
None => {
diagnostics_summary = Some(proto::UpdateDiagnosticSummary {
project_id: project_id,
worktree_id: worktree_id.to_proto(),
summary: Some(proto::DiagnosticSummary {
path: project_path.path.as_ref().to_proto(),
language_server_id: server_id.0 as u64,
error_count: new_summary.error_count,
warning_count: new_summary.warning_count,
}),
more_summaries: Vec::new(),
})
}
}
}
updated_diagnostics_paths
.entry(server_id)
.or_insert_with(Vec::new)
.push(project_path);
}
ControlFlow::Break(()) => {}
}
} }
let updated = worktree.update(cx, |worktree, cx| { if let Some((diagnostics_summary, (downstream_client, _))) =
self.update_worktree_diagnostics( diagnostics_summary.zip(self.downstream_client.as_ref())
worktree.id(), {
server_id, downstream_client.send(diagnostics_summary).log_err();
project_path.path.clone(), }
diagnostics, for (server_id, paths) in updated_diagnostics_paths {
cx, cx.emit(LspStoreEvent::DiagnosticsUpdated { server_id, paths });
)
})?;
if updated {
cx.emit(LspStoreEvent::DiagnosticsUpdated {
language_server_id: server_id,
path: project_path,
})
} }
Ok(()) Ok(())
} }
@ -7881,10 +7974,10 @@ impl LspStore {
&mut self, &mut self,
worktree_id: WorktreeId, worktree_id: WorktreeId,
server_id: LanguageServerId, server_id: LanguageServerId,
worktree_path: Arc<Path>, path_in_worktree: Arc<Path>,
diagnostics: Vec<DiagnosticEntry<Unclipped<PointUtf16>>>, diagnostics: Vec<DiagnosticEntry<Unclipped<PointUtf16>>>,
_: &mut Context<Worktree>, _: &mut Context<Worktree>,
) -> Result<bool> { ) -> Result<ControlFlow<(), Option<(u64, proto::DiagnosticSummary)>>> {
let local = match &mut self.mode { let local = match &mut self.mode {
LspStoreMode::Local(local_lsp_store) => local_lsp_store, LspStoreMode::Local(local_lsp_store) => local_lsp_store,
_ => anyhow::bail!("update_worktree_diagnostics called on remote"), _ => anyhow::bail!("update_worktree_diagnostics called on remote"),
@ -7892,7 +7985,9 @@ impl LspStore {
let summaries_for_tree = self.diagnostic_summaries.entry(worktree_id).or_default(); let summaries_for_tree = self.diagnostic_summaries.entry(worktree_id).or_default();
let diagnostics_for_tree = local.diagnostics.entry(worktree_id).or_default(); let diagnostics_for_tree = local.diagnostics.entry(worktree_id).or_default();
let summaries_by_server_id = summaries_for_tree.entry(worktree_path.clone()).or_default(); let summaries_by_server_id = summaries_for_tree
.entry(path_in_worktree.clone())
.or_default();
let old_summary = summaries_by_server_id let old_summary = summaries_by_server_id
.remove(&server_id) .remove(&server_id)
@ -7900,18 +7995,19 @@ impl LspStore {
let new_summary = DiagnosticSummary::new(&diagnostics); let new_summary = DiagnosticSummary::new(&diagnostics);
if new_summary.is_empty() { if new_summary.is_empty() {
if let Some(diagnostics_by_server_id) = diagnostics_for_tree.get_mut(&worktree_path) { if let Some(diagnostics_by_server_id) = diagnostics_for_tree.get_mut(&path_in_worktree)
{
if let Ok(ix) = diagnostics_by_server_id.binary_search_by_key(&server_id, |e| e.0) { if let Ok(ix) = diagnostics_by_server_id.binary_search_by_key(&server_id, |e| e.0) {
diagnostics_by_server_id.remove(ix); diagnostics_by_server_id.remove(ix);
} }
if diagnostics_by_server_id.is_empty() { if diagnostics_by_server_id.is_empty() {
diagnostics_for_tree.remove(&worktree_path); diagnostics_for_tree.remove(&path_in_worktree);
} }
} }
} else { } else {
summaries_by_server_id.insert(server_id, new_summary); summaries_by_server_id.insert(server_id, new_summary);
let diagnostics_by_server_id = diagnostics_for_tree let diagnostics_by_server_id = diagnostics_for_tree
.entry(worktree_path.clone()) .entry(path_in_worktree.clone())
.or_default(); .or_default();
match diagnostics_by_server_id.binary_search_by_key(&server_id, |e| e.0) { match diagnostics_by_server_id.binary_search_by_key(&server_id, |e| e.0) {
Ok(ix) => { Ok(ix) => {
@ -7924,23 +8020,22 @@ impl LspStore {
} }
if !old_summary.is_empty() || !new_summary.is_empty() { if !old_summary.is_empty() || !new_summary.is_empty() {
if let Some((downstream_client, project_id)) = &self.downstream_client { if let Some((_, project_id)) = &self.downstream_client {
downstream_client Ok(ControlFlow::Continue(Some((
.send(proto::UpdateDiagnosticSummary { *project_id,
project_id: *project_id, proto::DiagnosticSummary {
worktree_id: worktree_id.to_proto(), path: path_in_worktree.to_proto(),
summary: Some(proto::DiagnosticSummary { language_server_id: server_id.0 as u64,
path: worktree_path.to_proto(), error_count: new_summary.error_count as u32,
language_server_id: server_id.0 as u64, warning_count: new_summary.warning_count as u32,
error_count: new_summary.error_count as u32, },
warning_count: new_summary.warning_count as u32, ))))
}), } else {
}) Ok(ControlFlow::Continue(None))
.log_err();
} }
} else {
Ok(ControlFlow::Break(()))
} }
Ok(!old_summary.is_empty() || !new_summary.is_empty())
} }
pub fn open_buffer_for_symbol( pub fn open_buffer_for_symbol(
@ -8793,23 +8888,30 @@ impl LspStore {
envelope: TypedEnvelope<proto::UpdateDiagnosticSummary>, envelope: TypedEnvelope<proto::UpdateDiagnosticSummary>,
mut cx: AsyncApp, mut cx: AsyncApp,
) -> Result<()> { ) -> Result<()> {
this.update(&mut cx, |this, cx| { this.update(&mut cx, |lsp_store, cx| {
let worktree_id = WorktreeId::from_proto(envelope.payload.worktree_id); let worktree_id = WorktreeId::from_proto(envelope.payload.worktree_id);
if let Some(message) = envelope.payload.summary { let mut updated_diagnostics_paths = HashMap::default();
let mut diagnostics_summary = None::<proto::UpdateDiagnosticSummary>;
for message_summary in envelope
.payload
.summary
.into_iter()
.chain(envelope.payload.more_summaries)
{
let project_path = ProjectPath { let project_path = ProjectPath {
worktree_id, worktree_id,
path: Arc::<Path>::from_proto(message.path), path: Arc::<Path>::from_proto(message_summary.path),
}; };
let path = project_path.path.clone(); let path = project_path.path.clone();
let server_id = LanguageServerId(message.language_server_id as usize); let server_id = LanguageServerId(message_summary.language_server_id as usize);
let summary = DiagnosticSummary { let summary = DiagnosticSummary {
error_count: message.error_count as usize, error_count: message_summary.error_count as usize,
warning_count: message.warning_count as usize, warning_count: message_summary.warning_count as usize,
}; };
if summary.is_empty() { if summary.is_empty() {
if let Some(worktree_summaries) = if let Some(worktree_summaries) =
this.diagnostic_summaries.get_mut(&worktree_id) lsp_store.diagnostic_summaries.get_mut(&worktree_id)
{ {
if let Some(summaries) = worktree_summaries.get_mut(&path) { if let Some(summaries) = worktree_summaries.get_mut(&path) {
summaries.remove(&server_id); summaries.remove(&server_id);
@ -8819,31 +8921,55 @@ impl LspStore {
} }
} }
} else { } else {
this.diagnostic_summaries lsp_store
.diagnostic_summaries
.entry(worktree_id) .entry(worktree_id)
.or_default() .or_default()
.entry(path) .entry(path)
.or_default() .or_default()
.insert(server_id, summary); .insert(server_id, summary);
} }
if let Some((downstream_client, project_id)) = &this.downstream_client {
downstream_client if let Some((_, project_id)) = &lsp_store.downstream_client {
.send(proto::UpdateDiagnosticSummary { match &mut diagnostics_summary {
project_id: *project_id, Some(diagnostics_summary) => {
worktree_id: worktree_id.to_proto(), diagnostics_summary
summary: Some(proto::DiagnosticSummary { .more_summaries
path: project_path.path.as_ref().to_proto(), .push(proto::DiagnosticSummary {
language_server_id: server_id.0 as u64, path: project_path.path.as_ref().to_proto(),
error_count: summary.error_count as u32, language_server_id: server_id.0 as u64,
warning_count: summary.warning_count as u32, error_count: summary.error_count as u32,
}), warning_count: summary.warning_count as u32,
}) })
.log_err(); }
None => {
diagnostics_summary = Some(proto::UpdateDiagnosticSummary {
project_id: *project_id,
worktree_id: worktree_id.to_proto(),
summary: Some(proto::DiagnosticSummary {
path: project_path.path.as_ref().to_proto(),
language_server_id: server_id.0 as u64,
error_count: summary.error_count as u32,
warning_count: summary.warning_count as u32,
}),
more_summaries: Vec::new(),
})
}
}
} }
cx.emit(LspStoreEvent::DiagnosticsUpdated { updated_diagnostics_paths
language_server_id: LanguageServerId(message.language_server_id as usize), .entry(server_id)
path: project_path, .or_insert_with(Vec::new)
}); .push(project_path);
}
if let Some((diagnostics_summary, (downstream_client, _))) =
diagnostics_summary.zip(lsp_store.downstream_client.as_ref())
{
downstream_client.send(diagnostics_summary).log_err();
}
for (server_id, paths) in updated_diagnostics_paths {
cx.emit(LspStoreEvent::DiagnosticsUpdated { server_id, paths });
} }
Ok(()) Ok(())
})? })?
@ -10361,6 +10487,7 @@ impl LspStore {
error_count: 0, error_count: 0,
warning_count: 0, warning_count: 0,
}), }),
more_summaries: Vec::new(),
}) })
.log_err(); .log_err();
} }
@ -10649,52 +10776,80 @@ impl LspStore {
) )
} }
#[cfg(any(test, feature = "test-support"))]
pub fn update_diagnostics( pub fn update_diagnostics(
&mut self, &mut self,
language_server_id: LanguageServerId, server_id: LanguageServerId,
params: lsp::PublishDiagnosticsParams, diagnostics: lsp::PublishDiagnosticsParams,
result_id: Option<String>, result_id: Option<String>,
source_kind: DiagnosticSourceKind, source_kind: DiagnosticSourceKind,
disk_based_sources: &[String], disk_based_sources: &[String],
cx: &mut Context<Self>, cx: &mut Context<Self>,
) -> Result<()> { ) -> Result<()> {
self.merge_diagnostics( self.merge_lsp_diagnostics(
language_server_id,
params,
result_id,
source_kind, source_kind,
disk_based_sources, vec![DocumentDiagnosticsUpdate {
diagnostics,
result_id,
server_id,
disk_based_sources: Cow::Borrowed(disk_based_sources),
}],
|_, _, _| false, |_, _, _| false,
cx, cx,
) )
} }
pub fn merge_diagnostics( pub fn merge_lsp_diagnostics(
&mut self, &mut self,
language_server_id: LanguageServerId,
mut params: lsp::PublishDiagnosticsParams,
result_id: Option<String>,
source_kind: DiagnosticSourceKind, source_kind: DiagnosticSourceKind,
disk_based_sources: &[String], lsp_diagnostics: Vec<DocumentDiagnosticsUpdate<lsp::PublishDiagnosticsParams>>,
filter: impl Fn(&Buffer, &Diagnostic, &App) -> bool + Clone, merge: impl Fn(&Buffer, &Diagnostic, &App) -> bool + Clone,
cx: &mut Context<Self>, cx: &mut Context<Self>,
) -> Result<()> { ) -> Result<()> {
anyhow::ensure!(self.mode.is_local(), "called update_diagnostics on remote"); anyhow::ensure!(self.mode.is_local(), "called update_diagnostics on remote");
let abs_path = params let updates = lsp_diagnostics
.uri .into_iter()
.to_file_path() .filter_map(|update| {
.map_err(|()| anyhow!("URI is not a file"))?; let abs_path = update.diagnostics.uri.to_file_path().ok()?;
Some(DocumentDiagnosticsUpdate {
diagnostics: self.lsp_to_document_diagnostics(
abs_path,
source_kind,
update.server_id,
update.diagnostics,
&update.disk_based_sources,
),
result_id: update.result_id,
server_id: update.server_id,
disk_based_sources: update.disk_based_sources,
})
})
.collect();
self.merge_diagnostic_entries(updates, merge, cx)?;
Ok(())
}
fn lsp_to_document_diagnostics(
&mut self,
document_abs_path: PathBuf,
source_kind: DiagnosticSourceKind,
server_id: LanguageServerId,
mut lsp_diagnostics: lsp::PublishDiagnosticsParams,
disk_based_sources: &[String],
) -> DocumentDiagnostics {
let mut diagnostics = Vec::default(); let mut diagnostics = Vec::default();
let mut primary_diagnostic_group_ids = HashMap::default(); let mut primary_diagnostic_group_ids = HashMap::default();
let mut sources_by_group_id = HashMap::default(); let mut sources_by_group_id = HashMap::default();
let mut supporting_diagnostics = HashMap::default(); let mut supporting_diagnostics = HashMap::default();
let adapter = self.language_server_adapter_for_id(language_server_id); let adapter = self.language_server_adapter_for_id(server_id);
// Ensure that primary diagnostics are always the most severe // Ensure that primary diagnostics are always the most severe
params.diagnostics.sort_by_key(|item| item.severity); lsp_diagnostics
.diagnostics
.sort_by_key(|item| item.severity);
for diagnostic in &params.diagnostics { for diagnostic in &lsp_diagnostics.diagnostics {
let source = diagnostic.source.as_ref(); let source = diagnostic.source.as_ref();
let range = range_from_lsp(diagnostic.range); let range = range_from_lsp(diagnostic.range);
let is_supporting = diagnostic let is_supporting = diagnostic
@ -10716,7 +10871,7 @@ impl LspStore {
.map_or(false, |tags| tags.contains(&DiagnosticTag::UNNECESSARY)); .map_or(false, |tags| tags.contains(&DiagnosticTag::UNNECESSARY));
let underline = self let underline = self
.language_server_adapter_for_id(language_server_id) .language_server_adapter_for_id(server_id)
.map_or(true, |adapter| adapter.underline_diagnostic(diagnostic)); .map_or(true, |adapter| adapter.underline_diagnostic(diagnostic));
if is_supporting { if is_supporting {
@ -10758,7 +10913,7 @@ impl LspStore {
}); });
if let Some(infos) = &diagnostic.related_information { if let Some(infos) = &diagnostic.related_information {
for info in infos { for info in infos {
if info.location.uri == params.uri && !info.message.is_empty() { if info.location.uri == lsp_diagnostics.uri && !info.message.is_empty() {
let range = range_from_lsp(info.location.range); let range = range_from_lsp(info.location.range);
diagnostics.push(DiagnosticEntry { diagnostics.push(DiagnosticEntry {
range, range,
@ -10806,16 +10961,11 @@ impl LspStore {
} }
} }
self.merge_diagnostic_entries( DocumentDiagnostics {
language_server_id,
abs_path,
result_id,
params.version,
diagnostics, diagnostics,
filter, document_abs_path,
cx, version: lsp_diagnostics.version,
)?; }
Ok(())
} }
fn insert_newly_running_language_server( fn insert_newly_running_language_server(
@ -11571,67 +11721,84 @@ impl LspStore {
) { ) {
let workspace_diagnostics = let workspace_diagnostics =
GetDocumentDiagnostics::deserialize_workspace_diagnostics_report(report, server_id); GetDocumentDiagnostics::deserialize_workspace_diagnostics_report(report, server_id);
for workspace_diagnostics in workspace_diagnostics { let mut unchanged_buffers = HashSet::default();
let LspPullDiagnostics::Response { let mut changed_buffers = HashSet::default();
server_id, let workspace_diagnostics_updates = workspace_diagnostics
uri, .into_iter()
diagnostics, .filter_map(
} = workspace_diagnostics.diagnostics |workspace_diagnostics| match workspace_diagnostics.diagnostics {
else { LspPullDiagnostics::Response {
continue;
};
let adapter = self.language_server_adapter_for_id(server_id);
let disk_based_sources = adapter
.as_ref()
.map(|adapter| adapter.disk_based_diagnostic_sources.as_slice())
.unwrap_or(&[]);
match diagnostics {
PulledDiagnostics::Unchanged { result_id } => {
self.merge_diagnostics(
server_id, server_id,
lsp::PublishDiagnosticsParams { uri,
uri: uri.clone(), diagnostics,
diagnostics: Vec::new(), } => Some((server_id, uri, diagnostics, workspace_diagnostics.version)),
version: None, LspPullDiagnostics::Default => None,
}, },
Some(result_id), )
DiagnosticSourceKind::Pulled, .fold(
disk_based_sources, HashMap::default(),
|_, _, _| true, |mut acc, (server_id, uri, diagnostics, version)| {
cx, let (result_id, diagnostics) = match diagnostics {
) PulledDiagnostics::Unchanged { result_id } => {
.log_err(); unchanged_buffers.insert(uri.clone());
} (Some(result_id), Vec::new())
PulledDiagnostics::Changed { }
diagnostics, PulledDiagnostics::Changed {
result_id, result_id,
} => {
self.merge_diagnostics(
server_id,
lsp::PublishDiagnosticsParams {
uri: uri.clone(),
diagnostics, diagnostics,
version: workspace_diagnostics.version, } => {
}, changed_buffers.insert(uri.clone());
result_id, (result_id, diagnostics)
DiagnosticSourceKind::Pulled, }
disk_based_sources, };
|buffer, old_diagnostic, cx| match old_diagnostic.source_kind { let disk_based_sources = Cow::Owned(
DiagnosticSourceKind::Pulled => { self.language_server_adapter_for_id(server_id)
let buffer_url = File::from_dyn(buffer.file()) .as_ref()
.map(|f| f.abs_path(cx)) .map(|adapter| adapter.disk_based_diagnostic_sources.as_slice())
.and_then(|abs_path| file_path_to_lsp_url(&abs_path).ok()); .unwrap_or(&[])
buffer_url.is_none_or(|buffer_url| buffer_url != uri) .to_vec(),
} );
DiagnosticSourceKind::Other | DiagnosticSourceKind::Pushed => true, acc.entry(server_id)
}, .or_insert_with(Vec::new)
cx, .push(DocumentDiagnosticsUpdate {
) server_id,
.log_err(); diagnostics: lsp::PublishDiagnosticsParams {
} uri,
} diagnostics,
version,
},
result_id,
disk_based_sources,
});
acc
},
);
for diagnostic_updates in workspace_diagnostics_updates.into_values() {
self.merge_lsp_diagnostics(
DiagnosticSourceKind::Pulled,
diagnostic_updates,
|buffer, old_diagnostic, cx| {
File::from_dyn(buffer.file())
.and_then(|file| {
let abs_path = file.as_local()?.abs_path(cx);
lsp::Url::from_file_path(abs_path).ok()
})
.is_none_or(|buffer_uri| {
unchanged_buffers.contains(&buffer_uri)
|| match old_diagnostic.source_kind {
DiagnosticSourceKind::Pulled => {
!changed_buffers.contains(&buffer_uri)
}
DiagnosticSourceKind::Other | DiagnosticSourceKind::Pushed => {
true
}
}
})
},
cx,
)
.log_err();
} }
} }
} }

View file

@ -1,4 +1,4 @@
use std::sync::Arc; use std::{borrow::Cow, sync::Arc};
use ::serde::{Deserialize, Serialize}; use ::serde::{Deserialize, Serialize};
use gpui::WeakEntity; use gpui::WeakEntity;
@ -6,7 +6,7 @@ use language::{CachedLspAdapter, Diagnostic, DiagnosticSourceKind};
use lsp::{LanguageServer, LanguageServerName}; use lsp::{LanguageServer, LanguageServerName};
use util::ResultExt as _; use util::ResultExt as _;
use crate::LspStore; use crate::{LspStore, lsp_store::DocumentDiagnosticsUpdate};
pub const CLANGD_SERVER_NAME: LanguageServerName = LanguageServerName::new_static("clangd"); pub const CLANGD_SERVER_NAME: LanguageServerName = LanguageServerName::new_static("clangd");
const INACTIVE_REGION_MESSAGE: &str = "inactive region"; const INACTIVE_REGION_MESSAGE: &str = "inactive region";
@ -81,12 +81,16 @@ pub fn register_notifications(
version: params.text_document.version, version: params.text_document.version,
diagnostics, diagnostics,
}; };
this.merge_diagnostics( this.merge_lsp_diagnostics(
server_id,
mapped_diagnostics,
None,
DiagnosticSourceKind::Pushed, DiagnosticSourceKind::Pushed,
&adapter.disk_based_diagnostic_sources, vec![DocumentDiagnosticsUpdate {
server_id,
diagnostics: mapped_diagnostics,
result_id: None,
disk_based_sources: Cow::Borrowed(
&adapter.disk_based_diagnostic_sources,
),
}],
|_, diag, _| !is_inactive_region(diag), |_, diag, _| !is_inactive_region(diag),
cx, cx,
) )

View file

@ -74,9 +74,9 @@ use gpui::{
Task, WeakEntity, Window, Task, WeakEntity, Window,
}; };
use language::{ use language::{
Buffer, BufferEvent, Capability, CodeLabel, CursorShape, DiagnosticSourceKind, Language, Buffer, BufferEvent, Capability, CodeLabel, CursorShape, Language, LanguageName,
LanguageName, LanguageRegistry, PointUtf16, ToOffset, ToPointUtf16, Toolchain, ToolchainList, LanguageRegistry, PointUtf16, ToOffset, ToPointUtf16, Toolchain, ToolchainList, Transaction,
Transaction, Unclipped, language_settings::InlayHintKind, proto::split_operations, Unclipped, language_settings::InlayHintKind, proto::split_operations,
}; };
use lsp::{ use lsp::{
CodeActionKind, CompletionContext, CompletionItemKind, DocumentHighlightKind, InsertTextMode, CodeActionKind, CompletionContext, CompletionItemKind, DocumentHighlightKind, InsertTextMode,
@ -305,7 +305,7 @@ pub enum Event {
language_server_id: LanguageServerId, language_server_id: LanguageServerId,
}, },
DiagnosticsUpdated { DiagnosticsUpdated {
path: ProjectPath, paths: Vec<ProjectPath>,
language_server_id: LanguageServerId, language_server_id: LanguageServerId,
}, },
RemoteIdChanged(Option<u64>), RemoteIdChanged(Option<u64>),
@ -2895,18 +2895,17 @@ impl Project {
cx: &mut Context<Self>, cx: &mut Context<Self>,
) { ) {
match event { match event {
LspStoreEvent::DiagnosticsUpdated { LspStoreEvent::DiagnosticsUpdated { server_id, paths } => {
language_server_id, cx.emit(Event::DiagnosticsUpdated {
path, paths: paths.clone(),
} => cx.emit(Event::DiagnosticsUpdated { language_server_id: *server_id,
path: path.clone(), })
language_server_id: *language_server_id, }
}), LspStoreEvent::LanguageServerAdded(server_id, name, worktree_id) => cx.emit(
LspStoreEvent::LanguageServerAdded(language_server_id, name, worktree_id) => cx.emit( Event::LanguageServerAdded(*server_id, name.clone(), *worktree_id),
Event::LanguageServerAdded(*language_server_id, name.clone(), *worktree_id),
), ),
LspStoreEvent::LanguageServerRemoved(language_server_id) => { LspStoreEvent::LanguageServerRemoved(server_id) => {
cx.emit(Event::LanguageServerRemoved(*language_server_id)) cx.emit(Event::LanguageServerRemoved(*server_id))
} }
LspStoreEvent::LanguageServerLog(server_id, log_type, string) => cx.emit( LspStoreEvent::LanguageServerLog(server_id, log_type, string) => cx.emit(
Event::LanguageServerLog(*server_id, log_type.clone(), string.clone()), Event::LanguageServerLog(*server_id, log_type.clone(), string.clone()),
@ -3829,27 +3828,6 @@ impl Project {
}) })
} }
pub fn update_diagnostics(
&mut self,
language_server_id: LanguageServerId,
source_kind: DiagnosticSourceKind,
result_id: Option<String>,
params: lsp::PublishDiagnosticsParams,
disk_based_sources: &[String],
cx: &mut Context<Self>,
) -> Result<(), anyhow::Error> {
self.lsp_store.update(cx, |lsp_store, cx| {
lsp_store.update_diagnostics(
language_server_id,
params,
result_id,
source_kind,
disk_based_sources,
cx,
)
})
}
pub fn search(&mut self, query: SearchQuery, cx: &mut Context<Self>) -> Receiver<SearchResult> { pub fn search(&mut self, query: SearchQuery, cx: &mut Context<Self>) -> Receiver<SearchResult> {
let (result_tx, result_rx) = smol::channel::unbounded(); let (result_tx, result_rx) = smol::channel::unbounded();

View file

@ -20,8 +20,8 @@ use gpui::{App, BackgroundExecutor, SemanticVersion, UpdateGlobal};
use http_client::Url; use http_client::Url;
use itertools::Itertools; use itertools::Itertools;
use language::{ use language::{
Diagnostic, DiagnosticEntry, DiagnosticSet, DiskState, FakeLspAdapter, LanguageConfig, Diagnostic, DiagnosticEntry, DiagnosticSet, DiagnosticSourceKind, DiskState, FakeLspAdapter,
LanguageMatcher, LanguageName, LineEnding, OffsetRangeExt, Point, ToPoint, LanguageConfig, LanguageMatcher, LanguageName, LineEnding, OffsetRangeExt, Point, ToPoint,
language_settings::{AllLanguageSettings, LanguageSettingsContent, language_settings}, language_settings::{AllLanguageSettings, LanguageSettingsContent, language_settings},
tree_sitter_rust, tree_sitter_typescript, tree_sitter_rust, tree_sitter_typescript,
}; };
@ -1619,7 +1619,7 @@ async fn test_disk_based_diagnostics_progress(cx: &mut gpui::TestAppContext) {
events.next().await.unwrap(), events.next().await.unwrap(),
Event::DiagnosticsUpdated { Event::DiagnosticsUpdated {
language_server_id: LanguageServerId(0), language_server_id: LanguageServerId(0),
path: (worktree_id, Path::new("a.rs")).into() paths: vec![(worktree_id, Path::new("a.rs")).into()],
} }
); );
@ -1667,7 +1667,7 @@ async fn test_disk_based_diagnostics_progress(cx: &mut gpui::TestAppContext) {
events.next().await.unwrap(), events.next().await.unwrap(),
Event::DiagnosticsUpdated { Event::DiagnosticsUpdated {
language_server_id: LanguageServerId(0), language_server_id: LanguageServerId(0),
path: (worktree_id, Path::new("a.rs")).into() paths: vec![(worktree_id, Path::new("a.rs")).into()],
} }
); );

View file

@ -525,6 +525,7 @@ message UpdateDiagnosticSummary {
uint64 project_id = 1; uint64 project_id = 1;
uint64 worktree_id = 2; uint64 worktree_id = 2;
DiagnosticSummary summary = 3; DiagnosticSummary summary = 3;
repeated DiagnosticSummary more_summaries = 4;
} }
message DiagnosticSummary { message DiagnosticSummary {

View file

@ -2415,6 +2415,7 @@ impl Render for KeybindingEditorModal {
.header( .header(
ModalHeader::new().child( ModalHeader::new().child(
v_flex() v_flex()
.w_full()
.pb_1p5() .pb_1p5()
.mb_1() .mb_1()
.gap_0p5() .gap_0p5()
@ -2438,17 +2439,55 @@ impl Render for KeybindingEditorModal {
.section( .section(
Section::new().child( Section::new().child(
v_flex() v_flex()
.gap_2() .gap_2p5()
.child( .child(
v_flex() v_flex()
.child(Label::new("Edit Keystroke"))
.gap_1() .gap_1()
.child(self.keybind_editor.clone()), .child(Label::new("Edit Keystroke"))
.child(self.keybind_editor.clone())
.child(h_flex().gap_px().when(
matching_bindings_count > 0,
|this| {
let label = format!(
"There {} {} {} with the same keystrokes.",
if matching_bindings_count == 1 {
"is"
} else {
"are"
},
matching_bindings_count,
if matching_bindings_count == 1 {
"binding"
} else {
"bindings"
}
);
this.child(
Label::new(label)
.size(LabelSize::Small)
.color(Color::Muted),
)
.child(
Button::new("show_matching", "View")
.label_size(LabelSize::Small)
.icon(IconName::ArrowUpRight)
.icon_color(Color::Muted)
.icon_size(IconSize::XSmall)
.on_click(cx.listener(
|this, _, window, cx| {
this.show_matching_bindings(
window, cx,
);
},
)),
)
},
)),
) )
.when_some(self.action_arguments_editor.clone(), |this, editor| { .when_some(self.action_arguments_editor.clone(), |this, editor| {
this.child( this.child(
v_flex() v_flex()
.mt_1p5()
.gap_1() .gap_1()
.child(Label::new("Edit Arguments")) .child(Label::new("Edit Arguments"))
.child(editor), .child(editor),
@ -2459,50 +2498,25 @@ impl Render for KeybindingEditorModal {
this.child( this.child(
Banner::new() Banner::new()
.severity(error.severity) .severity(error.severity)
// For some reason, the div overflows its container to the .child(Label::new(error.content.clone())),
//right. The padding accounts for that.
.child(
div()
.size_full()
.pr_2()
.child(Label::new(error.content.clone())),
),
) )
}), }),
), ),
) )
.footer( .footer(
ModalFooter::new() ModalFooter::new().end_slot(
.start_slot( h_flex()
div().when(matching_bindings_count > 0, |this| { .gap_1()
this.child( .child(
Button::new("show_matching", format!( Button::new("cancel", "Cancel")
"There {} {} {} with the same keystrokes. Click to view", .on_click(cx.listener(|_, _, _, cx| cx.emit(DismissEvent))),
if matching_bindings_count == 1 { "is" } else { "are" }, )
matching_bindings_count, .child(Button::new("save-btn", "Save").on_click(cx.listener(
if matching_bindings_count == 1 { "binding" } else { "bindings" } |this, _event, _window, cx| {
)) this.save_or_display_error(cx);
.style(ButtonStyle::Transparent) },
.color(Color::Accent) ))),
.on_click(cx.listener(|this, _, window, cx| { ),
this.show_matching_bindings(window, cx);
}))
)
})
)
.end_slot(
h_flex()
.gap_1()
.child(
Button::new("cancel", "Cancel")
.on_click(cx.listener(|_, _, _, cx| cx.emit(DismissEvent))),
)
.child(Button::new("save-btn", "Save").on_click(cx.listener(
|this, _event, _window, cx| {
this.save_or_display_error(cx);
},
))),
),
), ),
) )
} }

View file

@ -529,7 +529,7 @@ impl Render for KeystrokeInput {
.w_full() .w_full()
.flex_1() .flex_1()
.justify_between() .justify_between()
.rounded_lg() .rounded_sm()
.overflow_hidden() .overflow_hidden()
.map(|this| { .map(|this| {
if is_recording { if is_recording {

View file

@ -234,16 +234,14 @@ fn find_relevant_completion<'a>(
} }
let original_cursor_offset = buffer.clip_offset(state.prefix_offset, text::Bias::Left); let original_cursor_offset = buffer.clip_offset(state.prefix_offset, text::Bias::Left);
let text_inserted_since_completion_request = let text_inserted_since_completion_request: String = buffer
buffer.text_for_range(original_cursor_offset..current_cursor_offset); .text_for_range(original_cursor_offset..current_cursor_offset)
let mut trimmed_completion = state_completion; .collect();
for chunk in text_inserted_since_completion_request { let trimmed_completion =
if let Some(suffix) = trimmed_completion.strip_prefix(chunk) { match state_completion.strip_prefix(&text_inserted_since_completion_request) {
trimmed_completion = suffix; Some(suffix) => suffix,
} else { None => continue 'completions,
continue 'completions; };
}
}
if best_completion.map_or(false, |best| best.len() > trimmed_completion.len()) { if best_completion.map_or(false, |best| best.len() > trimmed_completion.len()) {
continue; continue;
@ -439,3 +437,77 @@ pub struct SupermavenCompletion {
pub id: SupermavenCompletionStateId, pub id: SupermavenCompletionStateId,
pub updates: watch::Receiver<()>, pub updates: watch::Receiver<()>,
} }
#[cfg(test)]
mod tests {
use super::*;
use collections::BTreeMap;
use gpui::TestAppContext;
use language::Buffer;
#[gpui::test]
async fn test_find_relevant_completion_no_first_letter_skip(cx: &mut TestAppContext) {
let buffer = cx.new(|cx| Buffer::local("hello world", cx));
let buffer_snapshot = buffer.read_with(cx, |buffer, _| buffer.snapshot());
let mut states = BTreeMap::new();
let state_id = SupermavenCompletionStateId(1);
let (updates_tx, _) = watch::channel();
states.insert(
state_id,
SupermavenCompletionState {
buffer_id: buffer.entity_id(),
prefix_anchor: buffer_snapshot.anchor_before(0), // Start of buffer
prefix_offset: 0,
text: "hello".to_string(),
dedent: String::new(),
updates_tx,
},
);
let cursor_position = buffer_snapshot.anchor_after(1);
let result = find_relevant_completion(
&states,
buffer.entity_id(),
&buffer_snapshot,
cursor_position,
);
assert_eq!(result, Some("ello"));
}
#[gpui::test]
async fn test_find_relevant_completion_with_multiple_chars(cx: &mut TestAppContext) {
let buffer = cx.new(|cx| Buffer::local("hello world", cx));
let buffer_snapshot = buffer.read_with(cx, |buffer, _| buffer.snapshot());
let mut states = BTreeMap::new();
let state_id = SupermavenCompletionStateId(1);
let (updates_tx, _) = watch::channel();
states.insert(
state_id,
SupermavenCompletionState {
buffer_id: buffer.entity_id(),
prefix_anchor: buffer_snapshot.anchor_before(0), // Start of buffer
prefix_offset: 0,
text: "hello".to_string(),
dedent: String::new(),
updates_tx,
},
);
let cursor_position = buffer_snapshot.anchor_after(3);
let result = find_relevant_completion(
&states,
buffer.entity_id(),
&buffer_snapshot,
cursor_position,
);
assert_eq!(result, Some("lo"));
}
}

View file

@ -108,6 +108,14 @@ impl EditPredictionProvider for SupermavenCompletionProvider {
} }
fn show_completions_in_menu() -> bool { fn show_completions_in_menu() -> bool {
true
}
fn show_tab_accept_marker() -> bool {
true
}
fn supports_jump_to_edit() -> bool {
false false
} }
@ -116,7 +124,7 @@ impl EditPredictionProvider for SupermavenCompletionProvider {
} }
fn is_refreshing(&self) -> bool { fn is_refreshing(&self) -> bool {
self.pending_refresh.is_some() self.pending_refresh.is_some() && self.completion_id.is_none()
} }
fn refresh( fn refresh(
@ -197,6 +205,7 @@ impl EditPredictionProvider for SupermavenCompletionProvider {
let mut point = cursor_position.to_point(&snapshot); let mut point = cursor_position.to_point(&snapshot);
point.column = snapshot.line_len(point.row); point.column = snapshot.line_len(point.row);
let range = cursor_position..snapshot.anchor_after(point); let range = cursor_position..snapshot.anchor_after(point);
Some(completion_from_diff( Some(completion_from_diff(
snapshot, snapshot,
completion_text, completion_text,

View file

@ -136,7 +136,7 @@ impl BatchedTextRun {
.shape_line( .shape_line(
self.text.clone().into(), self.text.clone().into(),
self.font_size.to_pixels(window.rem_size()), self.font_size.to_pixels(window.rem_size()),
&[self.style.clone()], std::slice::from_ref(&self.style),
Some(dimensions.cell_width), Some(dimensions.cell_width),
) )
.paint(pos, dimensions.line_height, window, cx); .paint(pos, dimensions.line_height, window, cx);

View file

@ -168,7 +168,7 @@ impl Render for SingleLineInput {
.py_1p5() .py_1p5()
.flex_grow() .flex_grow()
.text_color(style.text_color) .text_color(style.text_color)
.rounded_lg() .rounded_sm()
.bg(style.background_color) .bg(style.background_color)
.border_1() .border_1()
.border_color(style.border_color) .border_color(style.border_color)

18
flake.lock generated
View file

@ -2,11 +2,11 @@
"nodes": { "nodes": {
"crane": { "crane": {
"locked": { "locked": {
"lastModified": 1750266157, "lastModified": 1754269165,
"narHash": "sha256-tL42YoNg9y30u7zAqtoGDNdTyXTi8EALDeCB13FtbQA=", "narHash": "sha256-0tcS8FHd4QjbCVoxN9jI+PjHgA4vc/IjkUSp+N3zy0U=",
"owner": "ipetkov", "owner": "ipetkov",
"repo": "crane", "repo": "crane",
"rev": "e37c943371b73ed87faf33f7583860f81f1d5a48", "rev": "444e81206df3f7d92780680e45858e31d2f07a08",
"type": "github" "type": "github"
}, },
"original": { "original": {
@ -33,10 +33,10 @@
"nixpkgs": { "nixpkgs": {
"locked": { "locked": {
"lastModified": 315532800, "lastModified": 315532800,
"narHash": "sha256-j+zO+IHQ7VwEam0pjPExdbLT2rVioyVS3iq4bLO3GEc=", "narHash": "sha256-5VYevX3GccubYeccRGAXvCPA1ktrGmIX1IFC0icX07g=",
"rev": "61c0f513911459945e2cb8bf333dc849f1b976ff", "rev": "a683adc19ff5228af548c6539dbc3440509bfed3",
"type": "tarball", "type": "tarball",
"url": "https://releases.nixos.org/nixpkgs/nixpkgs-25.11pre821324.61c0f5139114/nixexprs.tar.xz" "url": "https://releases.nixos.org/nixpkgs/nixpkgs-25.11pre840248.a683adc19ff5/nixexprs.tar.xz"
}, },
"original": { "original": {
"type": "tarball", "type": "tarball",
@ -58,11 +58,11 @@
] ]
}, },
"locked": { "locked": {
"lastModified": 1750964660, "lastModified": 1754575663,
"narHash": "sha256-YQ6EyFetjH1uy5JhdhRdPe6cuNXlYpMAQePFfZj4W7M=", "narHash": "sha256-afOx8AG0KYtw7mlt6s6ahBBy7eEHZwws3iCRoiuRQS4=",
"owner": "oxalica", "owner": "oxalica",
"repo": "rust-overlay", "repo": "rust-overlay",
"rev": "04f0fcfb1a50c63529805a798b4b5c21610ff390", "rev": "6db0fb0e9cec2e9729dc52bf4898e6c135bb8a0f",
"type": "github" "type": "github"
}, },
"original": { "original": {

View file

@ -1,5 +1,5 @@
[toolchain] [toolchain]
channel = "1.88" channel = "1.89"
profile = "minimal" profile = "minimal"
components = [ "rustfmt", "clippy" ] components = [ "rustfmt", "clippy" ]
targets = [ targets = [