Merge branch 'main' into find-path-tool
This commit is contained in:
commit
afb9554a28
30 changed files with 1113 additions and 570 deletions
8
Cargo.lock
generated
8
Cargo.lock
generated
|
@ -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",
|
||||||
|
|
|
@ -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 . .
|
||||||
|
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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")
|
||||||
|
|
|
@ -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>,
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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!()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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: ¬ify::Event| {
|
g.add(
|
||||||
let kind = match event.kind {
|
path,
|
||||||
EventKind::Create(_) => Some(PathEventKind::Created),
|
notify::RecursiveMode::NonRecursive,
|
||||||
EventKind::Modify(_) => Some(PathEventKind::Changed),
|
move |event: ¬ify::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(¬ify::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(¬ify::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(¬ify::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(¬ify::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(®istration_state.path) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
*count -= 1;
|
||||||
|
if *count == 0 {
|
||||||
|
state.watcher.unwatch(®istration_state.path).log_err();
|
||||||
|
state.path_registrations.remove(®istration_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 = ®istration.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 {
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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 ¶ms.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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,
|
||||||
)
|
)
|
||||||
|
|
|
@ -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();
|
||||||
|
|
||||||
|
|
|
@ -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()],
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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);
|
|
||||||
},
|
|
||||||
))),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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
18
flake.lock
generated
|
@ -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": {
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
[toolchain]
|
[toolchain]
|
||||||
channel = "1.88"
|
channel = "1.89"
|
||||||
profile = "minimal"
|
profile = "minimal"
|
||||||
components = [ "rustfmt", "clippy" ]
|
components = [ "rustfmt", "clippy" ]
|
||||||
targets = [
|
targets = [
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue