From 8dc91973249c2ebb761ed5fb42fb08ddd4adc6e4 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Mon, 3 Jan 2022 16:06:35 +0100 Subject: [PATCH 01/43] Position cursors correctly in `Editor::delete_line` in a multi-buffer --- crates/editor/src/editor.rs | 35 ++++++++++++++++++++--------------- 1 file changed, 20 insertions(+), 15 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index d626f28e7cd974aa0800d452c1549650b830fc5a..16334f3016f368bd42e6c456d3c17aba0a251c38 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -1564,7 +1564,6 @@ impl Editor { let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); let buffer = self.buffer.read(cx).snapshot(cx); - let mut row_delta = 0; let mut new_cursors = Vec::new(); let mut edit_ranges = Vec::new(); let mut selections = selections.iter().peekable(); @@ -1590,7 +1589,7 @@ impl Editor { // If there's a line after the range, delete the \n from the end of the row range // and position the cursor on the next line. edit_end = Point::new(rows.end, 0).to_offset(&buffer); - cursor_buffer_row = rows.start; + cursor_buffer_row = rows.end; } else { // If there isn't a line after the range, delete the \n from the line before the // start of the row range and position the cursor there. @@ -1599,29 +1598,35 @@ impl Editor { cursor_buffer_row = rows.start.saturating_sub(1); } - let mut cursor = - Point::new(cursor_buffer_row - row_delta, 0).to_display_point(&display_map); + let mut cursor = Point::new(cursor_buffer_row, 0).to_display_point(&display_map); *cursor.column_mut() = cmp::min(goal_display_column, display_map.line_len(cursor.row())); - row_delta += rows.len() as u32; - new_cursors.push((selection.id, cursor.to_point(&display_map))); + new_cursors.push(( + selection.id, + buffer.anchor_after(cursor.to_point(&display_map)), + )); edit_ranges.push(edit_start..edit_end); } - new_cursors.sort_unstable_by_key(|(_, point)| point.clone()); + new_cursors.sort_unstable_by(|a, b| a.1.cmp(&b.1, &buffer).unwrap()); + let buffer = self.buffer.update(cx, |buffer, cx| { + buffer.edit(edit_ranges, "", cx); + buffer.snapshot(cx) + }); let new_selections = new_cursors .into_iter() - .map(|(id, cursor)| Selection { - id, - start: cursor, - end: cursor, - reversed: false, - goal: SelectionGoal::None, + .map(|(id, cursor)| { + let cursor = cursor.to_point(&buffer); + Selection { + id, + start: cursor, + end: cursor, + reversed: false, + goal: SelectionGoal::None, + } }) .collect(); - self.buffer - .update(cx, |buffer, cx| buffer.edit(edit_ranges, "", cx)); self.update_selections(new_selections, Some(Autoscroll::Fit), cx); self.end_transaction(cx); } From ed361f2d1a312a87b29a4d1c5c59e2e0e3e19fbf Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Mon, 3 Jan 2022 16:21:30 +0100 Subject: [PATCH 02/43] Position selections correctly when duplicating lines in a multi-buffer --- crates/editor/src/editor.rs | 19 ++----------------- 1 file changed, 2 insertions(+), 17 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 16334f3016f368bd42e6c456d3c17aba0a251c38..d7e3a1450094e07a03ec70078b1cd038333188bc 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -1634,7 +1634,7 @@ impl Editor { pub fn duplicate_line(&mut self, _: &DuplicateLine, cx: &mut ViewContext) { self.start_transaction(cx); - let mut selections = self.local_selections::(cx); + let selections = self.local_selections::(cx); let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); let buffer = &display_map.buffer_snapshot; @@ -1664,28 +1664,13 @@ impl Editor { edits.push((start, text, rows.len() as u32)); } - let mut edits_iter = edits.iter().peekable(); - let mut row_delta = 0; - for selection in selections.iter_mut() { - while let Some((point, _, line_count)) = edits_iter.peek() { - if *point <= selection.start { - row_delta += line_count; - edits_iter.next(); - } else { - break; - } - } - selection.start.row += row_delta; - selection.end.row += row_delta; - } - self.buffer.update(cx, |buffer, cx| { for (point, text, _) in edits.into_iter().rev() { buffer.edit(Some(point..point), text, cx); } }); - self.update_selections(selections, Some(Autoscroll::Fit), cx); + self.request_autoscroll(Autoscroll::Fit, cx); self.end_transaction(cx); } From 2b31a48ef96dc4097f41f5e69e256b8a1165d64b Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Mon, 3 Jan 2022 16:26:42 +0100 Subject: [PATCH 03/43] Clip right when moving to next word in case we land on a block line --- crates/editor/src/movement.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/editor/src/movement.rs b/crates/editor/src/movement.rs index 217b1e63e4f53dc1ac0766950bf9d31cf03bfa1c..466b6e932390fc7b0ae1e1da286fb455f50be634 100644 --- a/crates/editor/src/movement.rs +++ b/crates/editor/src/movement.rs @@ -172,7 +172,7 @@ pub fn next_word_boundary(map: &DisplaySnapshot, mut point: DisplayPoint) -> Dis } prev_char_kind = Some(char_kind); } - point + map.clip_point(point, Bias::Right) } pub fn is_inside_word(map: &DisplaySnapshot, point: DisplayPoint) -> bool { From 496066db59f90e26c970479c0c0ddf423a241b97 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Mon, 3 Jan 2022 16:58:36 +0100 Subject: [PATCH 04/43] Run `Project::diagnose` when registering a new language on Worktree --- crates/project/src/project.rs | 5 ++++- crates/project/src/worktree.rs | 2 ++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 182e205cc2f9a3bee1ed3f674ee16044017c5b6c..f23df06eb7ed42c71f19d54f2d39b3eacac7bfb3 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -475,7 +475,10 @@ impl Project { fn add_worktree(&mut self, worktree: ModelHandle, cx: &mut ModelContext) { cx.observe(&worktree, |_, _, cx| cx.notify()).detach(); - cx.subscribe(&worktree, |_, worktree, event, cx| match event { + cx.subscribe(&worktree, |this, worktree, event, cx| match event { + worktree::Event::LanguageRegistered => { + this.diagnose(cx); + } worktree::Event::DiagnosticsUpdated(path) => { cx.emit(Event::DiagnosticsUpdated(ProjectPath { worktree_id: worktree.id(), diff --git a/crates/project/src/worktree.rs b/crates/project/src/worktree.rs index 86bdeae7061891d3af91a4d6ef6a69f3b3fc7a38..82a0755223ae8ddd0f9a8c998bf6059ea5cd654d 100644 --- a/crates/project/src/worktree.rs +++ b/crates/project/src/worktree.rs @@ -68,6 +68,7 @@ pub enum Worktree { #[derive(Debug)] pub enum Event { + LanguageRegistered, DiagnosticsUpdated(Arc), } @@ -1060,6 +1061,7 @@ impl LocalWorktree { ) -> Option> { if !self.languages.iter().any(|l| Arc::ptr_eq(l, language)) { self.languages.push(language.clone()); + cx.emit(Event::LanguageRegistered); } if let Some(server) = self.language_servers.get(language.name()) { From 508b9dc0241bec2ba2c3b29869675757d9b7b198 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Tue, 4 Jan 2022 16:11:29 +0100 Subject: [PATCH 05/43] Rip out "diagnostic providers" --- crates/diagnostics/src/diagnostics.rs | 28 +-- crates/editor/src/editor.rs | 19 +- crates/editor/src/items.rs | 6 +- crates/editor/src/multi_buffer.rs | 5 +- crates/language/src/buffer.rs | 77 +++----- crates/language/src/diagnostic_set.rs | 17 +- crates/language/src/language.rs | 22 +-- crates/language/src/proto.rs | 134 ++++++-------- crates/language/src/tests.rs | 146 +++++++-------- crates/project/src/project.rs | 37 +--- crates/project/src/worktree.rs | 244 +++++++++----------------- crates/rpc/proto/zed.proto | 13 +- crates/rpc/src/peer.rs | 8 +- crates/server/src/rpc.rs | 2 +- crates/workspace/src/workspace.rs | 14 +- crates/zed/src/language.rs | 179 ------------------- 16 files changed, 265 insertions(+), 686 deletions(-) diff --git a/crates/diagnostics/src/diagnostics.rs b/crates/diagnostics/src/diagnostics.rs index 87d9c521ab677c99839555096980d8bc05813234..f523b4ca7f57fe2201f69b1ade63c43e0254143c 100644 --- a/crates/diagnostics/src/diagnostics.rs +++ b/crates/diagnostics/src/diagnostics.rs @@ -571,7 +571,7 @@ mod tests { use super::*; use client::{http::ServerResponse, test::FakeHttpClient, Client, UserStore}; use gpui::TestAppContext; - use language::{Diagnostic, DiagnosticEntry, DiagnosticSeverity, LanguageRegistry}; + use language::{Diagnostic, DiagnosticEntry, DiagnosticSeverity, LanguageRegistry, PointUtf16}; use project::FakeFs; use serde_json::json; use std::sync::Arc; @@ -629,11 +629,12 @@ mod tests { worktree.update(&mut cx, |worktree, cx| { worktree - .update_diagnostics_from_provider( + .update_diagnostic_entries( Arc::from("/test/main.rs".as_ref()), + None, vec![ DiagnosticEntry { - range: 20..21, + range: PointUtf16::new(1, 8)..PointUtf16::new(1, 9), diagnostic: Diagnostic { message: "move occurs because `x` has type `Vec`, which does not implement the `Copy` trait" @@ -646,7 +647,7 @@ mod tests { }, }, DiagnosticEntry { - range: 40..41, + range: PointUtf16::new(2, 8)..PointUtf16::new(2, 9), diagnostic: Diagnostic { message: "move occurs because `y` has type `Vec`, which does not implement the `Copy` trait" @@ -659,7 +660,7 @@ mod tests { }, }, DiagnosticEntry { - range: 58..59, + range: PointUtf16::new(3, 6)..PointUtf16::new(3, 7), diagnostic: Diagnostic { message: "value moved here".to_string(), severity: DiagnosticSeverity::INFORMATION, @@ -670,7 +671,7 @@ mod tests { }, }, DiagnosticEntry { - range: 68..69, + range: PointUtf16::new(4, 6)..PointUtf16::new(4, 7), diagnostic: Diagnostic { message: "value moved here".to_string(), severity: DiagnosticSeverity::INFORMATION, @@ -681,7 +682,7 @@ mod tests { }, }, DiagnosticEntry { - range: 112..113, + range: PointUtf16::new(7, 6)..PointUtf16::new(7, 7), diagnostic: Diagnostic { message: "use of moved value".to_string(), severity: DiagnosticSeverity::ERROR, @@ -692,7 +693,7 @@ mod tests { }, }, DiagnosticEntry { - range: 112..113, + range: PointUtf16::new(7, 6)..PointUtf16::new(7, 7), diagnostic: Diagnostic { message: "value used here after move".to_string(), severity: DiagnosticSeverity::INFORMATION, @@ -703,7 +704,7 @@ mod tests { }, }, DiagnosticEntry { - range: 122..123, + range: PointUtf16::new(8, 6)..PointUtf16::new(8, 7), diagnostic: Diagnostic { message: "use of moved value".to_string(), severity: DiagnosticSeverity::ERROR, @@ -714,7 +715,7 @@ mod tests { }, }, DiagnosticEntry { - range: 122..123, + range: PointUtf16::new(8, 6)..PointUtf16::new(8, 7), diagnostic: Diagnostic { message: "value used here after move".to_string(), severity: DiagnosticSeverity::INFORMATION, @@ -782,11 +783,12 @@ mod tests { worktree.update(&mut cx, |worktree, cx| { worktree - .update_diagnostics_from_provider( + .update_diagnostic_entries( Arc::from("/test/a.rs".as_ref()), + None, vec![ DiagnosticEntry { - range: 15..15, + range: PointUtf16::new(0, 15)..PointUtf16::new(0, 15), diagnostic: Diagnostic { message: "mismatched types".to_string(), severity: DiagnosticSeverity::ERROR, @@ -797,7 +799,7 @@ mod tests { }, }, DiagnosticEntry { - range: 15..15, + range: PointUtf16::new(0, 15)..PointUtf16::new(0, 15), diagnostic: Diagnostic { message: "expected `usize`, found `char`".to_string(), severity: DiagnosticSeverity::INFORMATION, diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index d7e3a1450094e07a03ec70078b1cd038333188bc..f418efd8d00908bb51984ffe8d2d9c891822fcd3 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -2857,19 +2857,19 @@ impl Editor { loop { let next_group = buffer .diagnostics_in_range::<_, usize>(search_start..buffer.len()) - .find_map(|(provider_name, entry)| { + .find_map(|entry| { if entry.diagnostic.is_primary && !entry.range.is_empty() && Some(entry.range.end) != active_primary_range.as_ref().map(|r| *r.end()) { - Some((provider_name, entry.range, entry.diagnostic.group_id)) + Some((entry.range, entry.diagnostic.group_id)) } else { None } }); - if let Some((provider_name, primary_range, group_id)) = next_group { - self.activate_diagnostics(provider_name, group_id, cx); + if let Some((primary_range, group_id)) = next_group { + self.activate_diagnostics(group_id, cx); self.update_selections( vec![Selection { id: selection.id, @@ -2897,7 +2897,7 @@ impl Editor { let primary_range_start = active_diagnostics.primary_range.start.to_offset(&buffer); let is_valid = buffer .diagnostics_in_range::<_, usize>(active_diagnostics.primary_range.clone()) - .any(|(_, entry)| { + .any(|entry| { entry.diagnostic.is_primary && !entry.range.is_empty() && entry.range.start == primary_range_start @@ -2923,12 +2923,7 @@ impl Editor { } } - fn activate_diagnostics( - &mut self, - provider_name: &str, - group_id: usize, - cx: &mut ViewContext, - ) { + fn activate_diagnostics(&mut self, group_id: usize, cx: &mut ViewContext) { self.dismiss_diagnostics(cx); self.active_diagnostics = self.display_map.update(cx, |display_map, cx| { let buffer = self.buffer.read(cx).snapshot(cx); @@ -2937,7 +2932,7 @@ impl Editor { let mut primary_message = None; let mut group_end = Point::zero(); let diagnostic_group = buffer - .diagnostic_group::(provider_name, group_id) + .diagnostic_group::(group_id) .map(|entry| { if entry.range.end > group_end { group_end = entry.range.end; diff --git a/crates/editor/src/items.rs b/crates/editor/src/items.rs index df819ec520004124054d339caed0b4f085f745f4..d88315fff7348aadafc02b9bd6f4c015693c01cd 100644 --- a/crates/editor/src/items.rs +++ b/crates/editor/src/items.rs @@ -298,9 +298,9 @@ impl DiagnosticMessage { let new_diagnostic = buffer .read(cx) .diagnostics_in_range::<_, usize>(cursor_position..cursor_position) - .filter(|(_, entry)| !entry.range.is_empty()) - .min_by_key(|(_, entry)| (entry.diagnostic.severity, entry.range.len())) - .map(|(_, entry)| entry.diagnostic); + .filter(|entry| !entry.range.is_empty()) + .min_by_key(|entry| (entry.diagnostic.severity, entry.range.len())) + .map(|entry| entry.diagnostic); if new_diagnostic != self.diagnostic { self.diagnostic = new_diagnostic; cx.notify(); diff --git a/crates/editor/src/multi_buffer.rs b/crates/editor/src/multi_buffer.rs index c875ce29a6cff7a671551cb0a7858a61f7676ef1..e96b8200f71e455427c4848ad75dff90f22769f0 100644 --- a/crates/editor/src/multi_buffer.rs +++ b/crates/editor/src/multi_buffer.rs @@ -1487,7 +1487,6 @@ impl MultiBufferSnapshot { pub fn diagnostic_group<'a, O>( &'a self, - provider_name: &'a str, group_id: usize, ) -> impl Iterator> + 'a where @@ -1495,13 +1494,13 @@ impl MultiBufferSnapshot { { self.as_singleton() .into_iter() - .flat_map(move |buffer| buffer.diagnostic_group(provider_name, group_id)) + .flat_map(move |buffer| buffer.diagnostic_group(group_id)) } pub fn diagnostics_in_range<'a, T, O>( &'a self, range: Range, - ) -> impl Iterator)> + 'a + ) -> impl Iterator> + 'a where T: 'a + ToOffset, O: 'a + text::FromAnchor, diff --git a/crates/language/src/buffer.rs b/crates/language/src/buffer.rs index 0c8d7ae57411cb73ee91a3fd7efb59dd3f312623..a23631a57766ad11a613b6f49c9c37e67cfb07cb 100644 --- a/crates/language/src/buffer.rs +++ b/crates/language/src/buffer.rs @@ -66,7 +66,7 @@ pub struct Buffer { parsing_in_background: bool, parse_count: usize, remote_selections: TreeMap]>>, - diagnostic_sets: Vec, + diagnostics: DiagnosticSet, diagnostics_update_count: usize, language_server: Option, deferred_ops: OperationQueue, @@ -77,7 +77,7 @@ pub struct Buffer { pub struct BufferSnapshot { text: text::BufferSnapshot, tree: Option, - diagnostic_sets: Vec, + diagnostics: DiagnosticSet, remote_selections: TreeMap]>>, diagnostics_update_count: usize, is_parsing: bool, @@ -121,7 +121,6 @@ struct LanguageServerSnapshot { pub enum Operation { Buffer(text::Operation), UpdateDiagnostics { - provider_name: String, diagnostics: Arc<[DiagnosticEntry]>, lamport_timestamp: clock::Lamport, }, @@ -306,17 +305,11 @@ impl Buffer { ); } let snapshot = this.snapshot(); - for diagnostic_set in message.diagnostic_sets { - let (provider_name, entries) = proto::deserialize_diagnostic_set(diagnostic_set); - this.apply_diagnostic_update( - DiagnosticSet::from_sorted_entries( - provider_name, - entries.into_iter().cloned(), - &snapshot, - ), - cx, - ); - } + let entries = proto::deserialize_diagnostics(message.diagnostics); + this.apply_diagnostic_update( + DiagnosticSet::from_sorted_entries(entries.into_iter().cloned(), &snapshot), + cx, + ); Ok(this) } @@ -338,13 +331,7 @@ impl Buffer { selections: proto::serialize_selections(selections), }) .collect(), - diagnostic_sets: self - .diagnostic_sets - .iter() - .map(|set| { - proto::serialize_diagnostic_set(set.provider_name().to_string(), set.iter()) - }) - .collect(), + diagnostics: proto::serialize_diagnostics(self.diagnostics.iter()), } } @@ -379,7 +366,7 @@ impl Buffer { pending_autoindent: Default::default(), language: None, remote_selections: Default::default(), - diagnostic_sets: Default::default(), + diagnostics: Default::default(), diagnostics_update_count: 0, language_server: None, deferred_ops: OperationQueue::new(), @@ -393,7 +380,7 @@ impl Buffer { text: self.text.snapshot(), tree: self.syntax_tree(), remote_selections: self.remote_selections.clone(), - diagnostic_sets: self.diagnostic_sets.clone(), + diagnostics: self.diagnostics.clone(), diagnostics_update_count: self.diagnostics_update_count, is_parsing: self.parsing_in_background, language: self.language.clone(), @@ -743,7 +730,6 @@ impl Buffer { pub fn update_diagnostics( &mut self, - provider_name: Arc, version: Option, mut diagnostics: Vec>, cx: &mut ModelContext, @@ -833,10 +819,9 @@ impl Buffer { } drop(edits_since_save); - let set = DiagnosticSet::new(provider_name, sanitized_diagnostics, content); + let set = DiagnosticSet::new(sanitized_diagnostics, content); self.apply_diagnostic_update(set.clone(), cx); Ok(Operation::UpdateDiagnostics { - provider_name: set.provider_name().to_string(), diagnostics: set.iter().cloned().collect(), lamport_timestamp: self.text.lamport_clock.tick(), }) @@ -1347,17 +1332,12 @@ impl Buffer { unreachable!("buffer operations should never be applied at this layer") } Operation::UpdateDiagnostics { - provider_name, diagnostics: diagnostic_set, .. } => { let snapshot = self.snapshot(); self.apply_diagnostic_update( - DiagnosticSet::from_sorted_entries( - provider_name, - diagnostic_set.iter().cloned(), - &snapshot, - ), + DiagnosticSet::from_sorted_entries(diagnostic_set.iter().cloned(), &snapshot), cx, ); } @@ -1379,15 +1359,8 @@ impl Buffer { } } - fn apply_diagnostic_update(&mut self, set: DiagnosticSet, cx: &mut ModelContext) { - match self - .diagnostic_sets - .binary_search_by_key(&set.provider_name(), |set| set.provider_name()) - { - Ok(ix) => self.diagnostic_sets[ix] = set.clone(), - Err(ix) => self.diagnostic_sets.insert(ix, set.clone()), - } - + fn apply_diagnostic_update(&mut self, diagnostics: DiagnosticSet, cx: &mut ModelContext) { + self.diagnostics = diagnostics; self.diagnostics_update_count += 1; cx.notify(); cx.emit(Event::DiagnosticsUpdated); @@ -1625,7 +1598,7 @@ impl BufferSnapshot { let mut highlights = None; let mut diagnostic_endpoints = Vec::::new(); if let Some(theme) = theme { - for (_, entry) in self.diagnostics_in_range::<_, usize>(range.clone()) { + for entry in self.diagnostics_in_range::<_, usize>(range.clone()) { diagnostic_endpoints.push(DiagnosticEndpoint { offset: entry.range.start, is_start: true, @@ -1756,38 +1729,28 @@ impl BufferSnapshot { pub fn diagnostics_in_range<'a, T, O>( &'a self, search_range: Range, - ) -> impl 'a + Iterator)> + ) -> impl 'a + Iterator> where T: 'a + Clone + ToOffset, O: 'a + FromAnchor, { - self.diagnostic_sets.iter().flat_map(move |set| { - set.range(search_range.clone(), self, true) - .map(|e| (set.provider_name(), e)) - }) + self.diagnostics.range(search_range.clone(), self, true) } pub fn diagnostic_groups(&self) -> Vec> { let mut groups = Vec::new(); - for set in &self.diagnostic_sets { - set.groups(&mut groups, self); - } + self.diagnostics.groups(&mut groups, self); groups } pub fn diagnostic_group<'a, O>( &'a self, - provider_name: &str, group_id: usize, ) -> impl 'a + Iterator> where O: 'a + FromAnchor, { - self.diagnostic_sets - .iter() - .find(|s| s.provider_name() == provider_name) - .into_iter() - .flat_map(move |s| s.group(group_id, self)) + self.diagnostics.group(group_id, self) } pub fn diagnostics_update_count(&self) -> usize { @@ -1805,7 +1768,7 @@ impl Clone for BufferSnapshot { text: self.text.clone(), tree: self.tree.clone(), remote_selections: self.remote_selections.clone(), - diagnostic_sets: self.diagnostic_sets.clone(), + diagnostics: self.diagnostics.clone(), diagnostics_update_count: self.diagnostics_update_count, is_parsing: self.is_parsing, language: self.language.clone(), diff --git a/crates/language/src/diagnostic_set.rs b/crates/language/src/diagnostic_set.rs index 05e19e635a3b044dc7416c72b272d795d9e85fd9..047513fce72d3bfab3c83f674e5ebc24b5026bc1 100644 --- a/crates/language/src/diagnostic_set.rs +++ b/crates/language/src/diagnostic_set.rs @@ -4,14 +4,12 @@ use std::{ cmp::{Ordering, Reverse}, iter, ops::Range, - sync::Arc, }; use sum_tree::{self, Bias, SumTree}; use text::{Anchor, FromAnchor, Point, ToOffset}; #[derive(Clone, Debug)] pub struct DiagnosticSet { - provider_name: Arc, diagnostics: SumTree>, } @@ -36,32 +34,22 @@ pub struct Summary { } impl DiagnosticSet { - pub fn provider_name(&self) -> &str { - &self.provider_name - } - - pub fn from_sorted_entries( - provider_name: impl Into>, - iter: I, - buffer: &text::BufferSnapshot, - ) -> Self + pub fn from_sorted_entries(iter: I, buffer: &text::BufferSnapshot) -> Self where I: IntoIterator>, { Self { - provider_name: provider_name.into(), diagnostics: SumTree::from_iter(iter, buffer), } } - pub fn new(provider_name: Arc, iter: I, buffer: &text::BufferSnapshot) -> Self + pub fn new(iter: I, buffer: &text::BufferSnapshot) -> Self where I: IntoIterator>, { let mut entries = iter.into_iter().collect::>(); entries.sort_unstable_by_key(|entry| (entry.range.start, Reverse(entry.range.end))); Self { - provider_name, diagnostics: SumTree::from_iter( entries.into_iter().map(|entry| DiagnosticEntry { range: buffer.anchor_before(entry.range.start) @@ -159,7 +147,6 @@ impl DiagnosticSet { impl Default for DiagnosticSet { fn default() -> Self { Self { - provider_name: "".into(), diagnostics: Default::default(), } } diff --git a/crates/language/src/language.rs b/crates/language/src/language.rs index d6c13a7fd45854de7fe992a1737c95a00c780b8b..fe832929a94b1afd52c9d54dd5ad12229a6fbf9d 100644 --- a/crates/language/src/language.rs +++ b/crates/language/src/language.rs @@ -6,10 +6,9 @@ pub mod proto; mod tests; use anyhow::{anyhow, Result}; -use async_trait::async_trait; pub use buffer::Operation; pub use buffer::*; -use collections::{HashMap, HashSet}; +use collections::HashSet; pub use diagnostic_set::DiagnosticEntry; use gpui::AppContext; use highlight_map::HighlightMap; @@ -60,18 +59,9 @@ pub struct BracketPair { pub newline: bool, } -#[async_trait] -pub trait DiagnosticProvider: 'static + Send + Sync { - async fn diagnose( - &self, - path: Arc, - ) -> Result, Vec>>>; -} - pub struct Language { pub(crate) config: LanguageConfig, pub(crate) grammar: Option>, - pub(crate) diagnostic_provider: Option>, } pub struct Grammar { @@ -136,7 +126,6 @@ impl Language { highlight_map: Default::default(), }) }), - diagnostic_provider: None, } } @@ -170,11 +159,6 @@ impl Language { Ok(self) } - pub fn with_diagnostic_provider(mut self, source: impl DiagnosticProvider) -> Self { - self.diagnostic_provider = Some(Arc::new(source)); - self - } - pub fn name(&self) -> &str { self.config.name.as_str() } @@ -208,10 +192,6 @@ impl Language { } } - pub fn diagnostic_provider(&self) -> Option<&Arc> { - self.diagnostic_provider.as_ref() - } - pub fn disk_based_diagnostic_sources(&self) -> Option<&HashSet> { self.config .language_server diff --git a/crates/language/src/proto.rs b/crates/language/src/proto.rs index 727b6a4d7928c3f82d827c698cfcd2a599c16d98..200a68705255a5cbcca8b2f911acd36a764a0af5 100644 --- a/crates/language/src/proto.rs +++ b/crates/language/src/proto.rs @@ -57,16 +57,12 @@ pub fn serialize_operation(operation: &Operation) -> proto::Operation { lamport_timestamp: lamport_timestamp.value, }), Operation::UpdateDiagnostics { - provider_name, diagnostics, lamport_timestamp, - } => proto::operation::Variant::UpdateDiagnosticSet(proto::UpdateDiagnosticSet { + } => proto::operation::Variant::UpdateDiagnostics(proto::UpdateDiagnostics { replica_id: lamport_timestamp.replica_id as u32, lamport_timestamp: lamport_timestamp.value, - diagnostic_set: Some(serialize_diagnostic_set( - provider_name.clone(), - diagnostics.iter(), - )), + diagnostics: serialize_diagnostics(diagnostics.iter()), }), }), } @@ -103,33 +99,29 @@ pub fn serialize_selections(selections: &Arc<[Selection]>) -> Vec( - provider_name: String, +pub fn serialize_diagnostics<'a>( diagnostics: impl IntoIterator>, -) -> proto::DiagnosticSet { - proto::DiagnosticSet { - provider_name, - diagnostics: diagnostics - .into_iter() - .map(|entry| proto::Diagnostic { - start: Some(serialize_anchor(&entry.range.start)), - end: Some(serialize_anchor(&entry.range.end)), - message: entry.diagnostic.message.clone(), - severity: match entry.diagnostic.severity { - DiagnosticSeverity::ERROR => proto::diagnostic::Severity::Error, - DiagnosticSeverity::WARNING => proto::diagnostic::Severity::Warning, - DiagnosticSeverity::INFORMATION => proto::diagnostic::Severity::Information, - DiagnosticSeverity::HINT => proto::diagnostic::Severity::Hint, - _ => proto::diagnostic::Severity::None, - } as i32, - group_id: entry.diagnostic.group_id as u64, - is_primary: entry.diagnostic.is_primary, - is_valid: entry.diagnostic.is_valid, - code: entry.diagnostic.code.clone(), - is_disk_based: entry.diagnostic.is_disk_based, - }) - .collect(), - } +) -> Vec { + diagnostics + .into_iter() + .map(|entry| proto::Diagnostic { + start: Some(serialize_anchor(&entry.range.start)), + end: Some(serialize_anchor(&entry.range.end)), + message: entry.diagnostic.message.clone(), + severity: match entry.diagnostic.severity { + DiagnosticSeverity::ERROR => proto::diagnostic::Severity::Error, + DiagnosticSeverity::WARNING => proto::diagnostic::Severity::Warning, + DiagnosticSeverity::INFORMATION => proto::diagnostic::Severity::Information, + DiagnosticSeverity::HINT => proto::diagnostic::Severity::Hint, + _ => proto::diagnostic::Severity::None, + } as i32, + group_id: entry.diagnostic.group_id as u64, + is_primary: entry.diagnostic.is_primary, + is_valid: entry.diagnostic.is_valid, + code: entry.diagnostic.code.clone(), + is_disk_based: entry.diagnostic.is_disk_based, + }) + .collect() } fn serialize_anchor(anchor: &Anchor) -> proto::Anchor { @@ -215,21 +207,13 @@ pub fn deserialize_operation(message: proto::Operation) -> Result { value: message.lamport_timestamp, }, }, - proto::operation::Variant::UpdateDiagnosticSet(message) => { - let (provider_name, diagnostics) = deserialize_diagnostic_set( - message - .diagnostic_set - .ok_or_else(|| anyhow!("missing diagnostic set"))?, - ); - Operation::UpdateDiagnostics { - provider_name, - diagnostics, - lamport_timestamp: clock::Lamport { - replica_id: message.replica_id as ReplicaId, - value: message.lamport_timestamp, - }, - } - } + proto::operation::Variant::UpdateDiagnostics(message) => Operation::UpdateDiagnostics { + diagnostics: deserialize_diagnostics(message.diagnostics), + lamport_timestamp: clock::Lamport { + replica_id: message.replica_id as ReplicaId, + value: message.lamport_timestamp, + }, + }, }, ) } @@ -269,40 +253,32 @@ pub fn deserialize_selections(selections: Vec) -> Arc<[Selecti ) } -pub fn deserialize_diagnostic_set( - message: proto::DiagnosticSet, -) -> (String, Arc<[DiagnosticEntry]>) { - ( - message.provider_name, - message - .diagnostics - .into_iter() - .filter_map(|diagnostic| { - Some(DiagnosticEntry { - range: deserialize_anchor(diagnostic.start?)? - ..deserialize_anchor(diagnostic.end?)?, - diagnostic: Diagnostic { - severity: match proto::diagnostic::Severity::from_i32(diagnostic.severity)? - { - proto::diagnostic::Severity::Error => DiagnosticSeverity::ERROR, - proto::diagnostic::Severity::Warning => DiagnosticSeverity::WARNING, - proto::diagnostic::Severity::Information => { - DiagnosticSeverity::INFORMATION - } - proto::diagnostic::Severity::Hint => DiagnosticSeverity::HINT, - proto::diagnostic::Severity::None => return None, - }, - message: diagnostic.message, - group_id: diagnostic.group_id as usize, - code: diagnostic.code, - is_valid: diagnostic.is_valid, - is_primary: diagnostic.is_primary, - is_disk_based: diagnostic.is_disk_based, +pub fn deserialize_diagnostics( + diagnostics: Vec, +) -> Arc<[DiagnosticEntry]> { + diagnostics + .into_iter() + .filter_map(|diagnostic| { + Some(DiagnosticEntry { + range: deserialize_anchor(diagnostic.start?)?..deserialize_anchor(diagnostic.end?)?, + diagnostic: Diagnostic { + severity: match proto::diagnostic::Severity::from_i32(diagnostic.severity)? { + proto::diagnostic::Severity::Error => DiagnosticSeverity::ERROR, + proto::diagnostic::Severity::Warning => DiagnosticSeverity::WARNING, + proto::diagnostic::Severity::Information => DiagnosticSeverity::INFORMATION, + proto::diagnostic::Severity::Hint => DiagnosticSeverity::HINT, + proto::diagnostic::Severity::None => return None, }, - }) + message: diagnostic.message, + group_id: diagnostic.group_id as usize, + code: diagnostic.code, + is_valid: diagnostic.is_valid, + is_primary: diagnostic.is_primary, + is_disk_based: diagnostic.is_disk_based, + }, }) - .collect(), - ) + }) + .collect() } fn deserialize_anchor(anchor: proto::Anchor) -> Option { diff --git a/crates/language/src/tests.rs b/crates/language/src/tests.rs index 4db5e788f84787964b9bf65922ebe5916a340868..e94ff781f3ddfa6c4c0ba407b322cab0eb97c451 100644 --- a/crates/language/src/tests.rs +++ b/crates/language/src/tests.rs @@ -455,7 +455,6 @@ async fn test_diagnostics(mut cx: gpui::TestAppContext) { // Receive diagnostics for an earlier version of the buffer. buffer .update_diagnostics( - "lsp".into(), Some(open_notification.text_document.version), vec![ DiagnosticEntry { @@ -503,34 +502,28 @@ async fn test_diagnostics(mut cx: gpui::TestAppContext) { .diagnostics_in_range::<_, Point>(Point::new(3, 0)..Point::new(5, 0)) .collect::>(), &[ - ( - "lsp", - DiagnosticEntry { - range: Point::new(3, 9)..Point::new(3, 11), - diagnostic: Diagnostic { - severity: DiagnosticSeverity::ERROR, - message: "undefined variable 'BB'".to_string(), - is_disk_based: true, - group_id: 1, - is_primary: true, - ..Default::default() - }, - } - ), - ( - "lsp", - DiagnosticEntry { - range: Point::new(4, 9)..Point::new(4, 12), - diagnostic: Diagnostic { - severity: DiagnosticSeverity::ERROR, - message: "undefined variable 'CCC'".to_string(), - is_disk_based: true, - group_id: 2, - is_primary: true, - ..Default::default() - } + DiagnosticEntry { + range: Point::new(3, 9)..Point::new(3, 11), + diagnostic: Diagnostic { + severity: DiagnosticSeverity::ERROR, + message: "undefined variable 'BB'".to_string(), + is_disk_based: true, + group_id: 1, + is_primary: true, + ..Default::default() + }, + }, + DiagnosticEntry { + range: Point::new(4, 9)..Point::new(4, 12), + diagnostic: Diagnostic { + severity: DiagnosticSeverity::ERROR, + message: "undefined variable 'CCC'".to_string(), + is_disk_based: true, + group_id: 2, + is_primary: true, + ..Default::default() } - ) + } ] ); assert_eq!( @@ -557,7 +550,6 @@ async fn test_diagnostics(mut cx: gpui::TestAppContext) { // Ensure overlapping diagnostics are highlighted correctly. buffer .update_diagnostics( - "lsp".into(), Some(open_notification.text_document.version), vec![ DiagnosticEntry { @@ -591,33 +583,27 @@ async fn test_diagnostics(mut cx: gpui::TestAppContext) { .diagnostics_in_range::<_, Point>(Point::new(2, 0)..Point::new(3, 0)) .collect::>(), &[ - ( - "lsp", - DiagnosticEntry { - range: Point::new(2, 9)..Point::new(2, 12), - diagnostic: Diagnostic { - severity: DiagnosticSeverity::WARNING, - message: "unreachable statement".to_string(), - group_id: 1, - is_primary: true, - ..Default::default() - } - } - ), - ( - "lsp", - DiagnosticEntry { - range: Point::new(2, 9)..Point::new(2, 10), - diagnostic: Diagnostic { - severity: DiagnosticSeverity::ERROR, - message: "undefined variable 'A'".to_string(), - is_disk_based: true, - group_id: 0, - is_primary: true, - ..Default::default() - }, + DiagnosticEntry { + range: Point::new(2, 9)..Point::new(2, 12), + diagnostic: Diagnostic { + severity: DiagnosticSeverity::WARNING, + message: "unreachable statement".to_string(), + group_id: 1, + is_primary: true, + ..Default::default() } - ) + }, + DiagnosticEntry { + range: Point::new(2, 9)..Point::new(2, 10), + diagnostic: Diagnostic { + severity: DiagnosticSeverity::ERROR, + message: "undefined variable 'A'".to_string(), + is_disk_based: true, + group_id: 0, + is_primary: true, + ..Default::default() + }, + } ] ); assert_eq!( @@ -654,7 +640,6 @@ async fn test_diagnostics(mut cx: gpui::TestAppContext) { buffer.update(&mut cx, |buffer, cx| { buffer .update_diagnostics( - "lsp".into(), Some(change_notification_2.text_document.version), vec![ DiagnosticEntry { @@ -689,34 +674,28 @@ async fn test_diagnostics(mut cx: gpui::TestAppContext) { .diagnostics_in_range::<_, Point>(0..buffer.len()) .collect::>(), &[ - ( - "lsp", - DiagnosticEntry { - range: Point::new(2, 21)..Point::new(2, 22), - diagnostic: Diagnostic { - severity: DiagnosticSeverity::ERROR, - message: "undefined variable 'A'".to_string(), - is_disk_based: true, - group_id: 0, - is_primary: true, - ..Default::default() - } + DiagnosticEntry { + range: Point::new(2, 21)..Point::new(2, 22), + diagnostic: Diagnostic { + severity: DiagnosticSeverity::ERROR, + message: "undefined variable 'A'".to_string(), + is_disk_based: true, + group_id: 0, + is_primary: true, + ..Default::default() } - ), - ( - "lsp", - DiagnosticEntry { - range: Point::new(3, 9)..Point::new(3, 11), - diagnostic: Diagnostic { - severity: DiagnosticSeverity::ERROR, - message: "undefined variable 'BB'".to_string(), - is_disk_based: true, - group_id: 1, - is_primary: true, - ..Default::default() - }, - } - ) + }, + DiagnosticEntry { + range: Point::new(3, 9)..Point::new(3, 11), + diagnostic: Diagnostic { + severity: DiagnosticSeverity::ERROR, + message: "undefined variable 'BB'".to_string(), + is_disk_based: true, + group_id: 1, + is_primary: true, + ..Default::default() + }, + } ] ); }); @@ -735,7 +714,6 @@ async fn test_empty_diagnostic_ranges(mut cx: gpui::TestAppContext) { buffer.set_language(Some(Arc::new(rust_lang())), None, cx); buffer .update_diagnostics( - "lsp".into(), None, vec![ DiagnosticEntry { diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index f23df06eb7ed42c71f19d54f2d39b3eacac7bfb3..499b3d4a5261b6b72db374dd0a653a640440cb44 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -18,7 +18,7 @@ use std::{ path::Path, sync::{atomic::AtomicBool, Arc}, }; -use util::{ResultExt, TryFutureExt as _}; +use util::TryFutureExt as _; pub use fs::*; pub use worktree::*; @@ -475,10 +475,7 @@ impl Project { fn add_worktree(&mut self, worktree: ModelHandle, cx: &mut ModelContext) { cx.observe(&worktree, |_, _, cx| cx.notify()).detach(); - cx.subscribe(&worktree, |this, worktree, event, cx| match event { - worktree::Event::LanguageRegistered => { - this.diagnose(cx); - } + cx.subscribe(&worktree, |_, worktree, event, cx| match event { worktree::Event::DiagnosticsUpdated(path) => { cx.emit(Event::DiagnosticsUpdated(ProjectPath { worktree_id: worktree.id(), @@ -506,36 +503,6 @@ impl Project { } } - pub fn diagnose(&self, cx: &mut ModelContext) { - for worktree_handle in &self.worktrees { - if let Some(worktree) = worktree_handle.read(cx).as_local() { - for language in worktree.languages() { - if let Some(provider) = language.diagnostic_provider().cloned() { - let worktree_path = worktree.abs_path().clone(); - let worktree_handle = worktree_handle.downgrade(); - cx.spawn_weak(|_, mut cx| async move { - let diagnostics = provider.diagnose(worktree_path).await.log_err()?; - let worktree_handle = worktree_handle.upgrade(&cx)?; - worktree_handle.update(&mut cx, |worktree, cx| { - for (path, diagnostics) in diagnostics { - worktree - .update_diagnostics_from_provider( - path.into(), - diagnostics, - cx, - ) - .log_err()?; - } - Some(()) - }) - }) - .detach(); - } - } - } - } - } - pub fn diagnostic_summaries<'a>( &'a self, cx: &'a AppContext, diff --git a/crates/project/src/worktree.rs b/crates/project/src/worktree.rs index 82a0755223ae8ddd0f9a8c998bf6059ea5cd654d..76f0e2b270792553a4b1c81e57fb30b1cc99912b 100644 --- a/crates/project/src/worktree.rs +++ b/crates/project/src/worktree.rs @@ -50,8 +50,6 @@ use util::{post_inc, ResultExt, TryFutureExt}; lazy_static! { static ref GITIGNORE: &'static OsStr = OsStr::new(".gitignore"); - static ref DIAGNOSTIC_PROVIDER_NAME: Arc = Arc::from("diagnostic_source"); - static ref LSP_PROVIDER_NAME: Arc = Arc::from("lsp"); } #[derive(Clone, Debug)] @@ -68,7 +66,6 @@ pub enum Worktree { #[derive(Debug)] pub enum Event { - LanguageRegistered, DiagnosticsUpdated(Arc), } @@ -675,7 +672,7 @@ impl Worktree { } } - pub fn update_diagnostics_from_lsp( + pub fn update_diagnostics( &mut self, mut params: lsp::PublishDiagnosticsParams, disk_based_sources: &HashSet, @@ -745,45 +742,16 @@ impl Worktree { }) .collect::>(); - let this = self.as_local_mut().unwrap(); - for buffer in this.open_buffers.values() { - if let Some(buffer) = buffer.upgrade(cx) { - if buffer - .read(cx) - .file() - .map_or(false, |file| *file.path() == worktree_path) - { - let (remote_id, operation) = buffer.update(cx, |buffer, cx| { - ( - buffer.remote_id(), - buffer.update_diagnostics( - LSP_PROVIDER_NAME.clone(), - params.version, - diagnostics.clone(), - cx, - ), - ) - }); - self.send_buffer_update(remote_id, operation?, cx); - break; - } - } - } - - let this = self.as_local_mut().unwrap(); - this.diagnostic_summaries - .insert(worktree_path.clone(), DiagnosticSummary::new(&diagnostics)); - this.lsp_diagnostics - .insert(worktree_path.clone(), diagnostics); - cx.emit(Event::DiagnosticsUpdated(worktree_path.clone())); + self.update_diagnostic_entries(worktree_path, params.version, diagnostics, cx)?; Ok(()) } - pub fn update_diagnostics_from_provider( + pub fn update_diagnostic_entries( &mut self, - path: Arc, - diagnostics: Vec>, - cx: &mut ModelContext, + worktree_path: Arc, + version: Option, + diagnostics: Vec>, + cx: &mut ModelContext, ) -> Result<()> { let this = self.as_local_mut().unwrap(); for buffer in this.open_buffers.values() { @@ -791,17 +759,12 @@ impl Worktree { if buffer .read(cx) .file() - .map_or(false, |file| *file.path() == path) + .map_or(false, |file| *file.path() == worktree_path) { let (remote_id, operation) = buffer.update(cx, |buffer, cx| { ( buffer.remote_id(), - buffer.update_diagnostics( - DIAGNOSTIC_PROVIDER_NAME.clone(), - None, - diagnostics.clone(), - cx, - ), + buffer.update_diagnostics(version, diagnostics.clone(), cx), ) }); self.send_buffer_update(remote_id, operation?, cx); @@ -812,9 +775,9 @@ impl Worktree { let this = self.as_local_mut().unwrap(); this.diagnostic_summaries - .insert(path.clone(), DiagnosticSummary::new(&diagnostics)); - this.provider_diagnostics.insert(path.clone(), diagnostics); - cx.emit(Event::DiagnosticsUpdated(path.clone())); + .insert(worktree_path.clone(), DiagnosticSummary::new(&diagnostics)); + this.diagnostics.insert(worktree_path.clone(), diagnostics); + cx.emit(Event::DiagnosticsUpdated(worktree_path.clone())); Ok(()) } @@ -888,8 +851,7 @@ pub struct LocalWorktree { loading_buffers: LoadingBuffers, open_buffers: HashMap>, shared_buffers: HashMap>>, - lsp_diagnostics: HashMap, Vec>>, - provider_diagnostics: HashMap, Vec>>, + diagnostics: HashMap, Vec>>, diagnostic_summaries: BTreeMap, DiagnosticSummary>, queued_operations: Vec<(u64, Operation)>, language_registry: Arc, @@ -997,8 +959,7 @@ impl LocalWorktree { loading_buffers: Default::default(), open_buffers: Default::default(), shared_buffers: Default::default(), - lsp_diagnostics: Default::default(), - provider_diagnostics: Default::default(), + diagnostics: Default::default(), diagnostic_summaries: Default::default(), queued_operations: Default::default(), language_registry: languages, @@ -1061,7 +1022,6 @@ impl LocalWorktree { ) -> Option> { if !self.languages.iter().any(|l| Arc::ptr_eq(l, language)) { self.languages.push(language.clone()); - cx.emit(Event::LanguageRegistered); } if let Some(server) = self.language_servers.get(language.name()) { @@ -1087,7 +1047,7 @@ impl LocalWorktree { while let Ok(diagnostics) = diagnostics_rx.recv().await { if let Some(handle) = cx.read(|cx| this.upgrade(cx)) { handle.update(&mut cx, |this, cx| { - this.update_diagnostics_from_lsp(diagnostics, &disk_based_sources, cx) + this.update_diagnostics(diagnostics, &disk_based_sources, cx) .log_err(); }); } else { @@ -1138,35 +1098,25 @@ impl LocalWorktree { .update(&mut cx, |t, cx| t.as_local().unwrap().load(&path, cx)) .await?; - let (lsp_diagnostics, provider_diagnostics, language, language_server) = - this.update(&mut cx, |this, cx| { - let this = this.as_local_mut().unwrap(); - let lsp_diagnostics = this.lsp_diagnostics.remove(&path); - let provider_diagnostics = this.provider_diagnostics.remove(&path); - let language = this - .language_registry - .select_language(file.full_path()) - .cloned(); - let server = language - .as_ref() - .and_then(|language| this.register_language(language, cx)); - (lsp_diagnostics, provider_diagnostics, language, server) - }); + let (diagnostics, language, language_server) = this.update(&mut cx, |this, cx| { + let this = this.as_local_mut().unwrap(); + let diagnostics = this.diagnostics.remove(&path); + let language = this + .language_registry + .select_language(file.full_path()) + .cloned(); + let server = language + .as_ref() + .and_then(|language| this.register_language(language, cx)); + (diagnostics, language, server) + }); let mut buffer_operations = Vec::new(); let buffer = cx.add_model(|cx| { let mut buffer = Buffer::from_file(0, contents, Box::new(file), cx); buffer.set_language(language, language_server, cx); - if let Some(diagnostics) = lsp_diagnostics { - let op = buffer - .update_diagnostics(LSP_PROVIDER_NAME.clone(), None, diagnostics, cx) - .unwrap(); - buffer_operations.push(op); - } - if let Some(diagnostics) = provider_diagnostics { - let op = buffer - .update_diagnostics(DIAGNOSTIC_PROVIDER_NAME.clone(), None, diagnostics, cx) - .unwrap(); + if let Some(diagnostics) = diagnostics { + let op = buffer.update_diagnostics(None, diagnostics, cx).unwrap(); buffer_operations.push(op); } buffer @@ -3739,19 +3689,16 @@ mod tests { .collect::>(); assert_eq!( diagnostics, - &[( - LSP_PROVIDER_NAME.as_ref(), - DiagnosticEntry { - range: Point::new(0, 9)..Point::new(0, 10), - diagnostic: Diagnostic { - severity: lsp::DiagnosticSeverity::ERROR, - message: "undefined variable 'A'".to_string(), - group_id: 0, - is_primary: true, - ..Default::default() - } + &[DiagnosticEntry { + range: Point::new(0, 9)..Point::new(0, 10), + diagnostic: Diagnostic { + severity: lsp::DiagnosticSeverity::ERROR, + message: "undefined variable 'A'".to_string(), + group_id: 0, + is_primary: true, + ..Default::default() } - )] + }] ) }); } @@ -3896,7 +3843,7 @@ mod tests { worktree .update(&mut cx, |tree, cx| { - tree.update_diagnostics_from_lsp(message, &Default::default(), cx) + tree.update_diagnostics(message, &Default::default(), cx) }) .unwrap(); let buffer = buffer.read_with(&cx, |buffer, _| buffer.snapshot()); @@ -3906,78 +3853,61 @@ mod tests { .diagnostics_in_range::<_, Point>(0..buffer.len()) .collect::>(), &[ - ( - LSP_PROVIDER_NAME.as_ref(), - DiagnosticEntry { - range: Point::new(1, 8)..Point::new(1, 9), - diagnostic: Diagnostic { - severity: DiagnosticSeverity::WARNING, - message: "error 1".to_string(), - group_id: 0, - is_primary: true, - ..Default::default() - } + DiagnosticEntry { + range: Point::new(1, 8)..Point::new(1, 9), + diagnostic: Diagnostic { + severity: DiagnosticSeverity::WARNING, + message: "error 1".to_string(), + group_id: 0, + is_primary: true, + ..Default::default() } - ), - ( - LSP_PROVIDER_NAME.as_ref(), - DiagnosticEntry { - range: Point::new(1, 8)..Point::new(1, 9), - diagnostic: Diagnostic { - severity: DiagnosticSeverity::HINT, - message: "error 1 hint 1".to_string(), - group_id: 0, - is_primary: false, - ..Default::default() - } + }, + DiagnosticEntry { + range: Point::new(1, 8)..Point::new(1, 9), + diagnostic: Diagnostic { + severity: DiagnosticSeverity::HINT, + message: "error 1 hint 1".to_string(), + group_id: 0, + is_primary: false, + ..Default::default() } - ), - ( - LSP_PROVIDER_NAME.as_ref(), - DiagnosticEntry { - range: Point::new(1, 13)..Point::new(1, 15), - diagnostic: Diagnostic { - severity: DiagnosticSeverity::HINT, - message: "error 2 hint 1".to_string(), - group_id: 1, - is_primary: false, - ..Default::default() - } + }, + DiagnosticEntry { + range: Point::new(1, 13)..Point::new(1, 15), + diagnostic: Diagnostic { + severity: DiagnosticSeverity::HINT, + message: "error 2 hint 1".to_string(), + group_id: 1, + is_primary: false, + ..Default::default() } - ), - ( - LSP_PROVIDER_NAME.as_ref(), - DiagnosticEntry { - range: Point::new(1, 13)..Point::new(1, 15), - diagnostic: Diagnostic { - severity: DiagnosticSeverity::HINT, - message: "error 2 hint 2".to_string(), - group_id: 1, - is_primary: false, - ..Default::default() - } + }, + DiagnosticEntry { + range: Point::new(1, 13)..Point::new(1, 15), + diagnostic: Diagnostic { + severity: DiagnosticSeverity::HINT, + message: "error 2 hint 2".to_string(), + group_id: 1, + is_primary: false, + ..Default::default() } - ), - ( - LSP_PROVIDER_NAME.as_ref(), - DiagnosticEntry { - range: Point::new(2, 8)..Point::new(2, 17), - diagnostic: Diagnostic { - severity: DiagnosticSeverity::ERROR, - message: "error 2".to_string(), - group_id: 1, - is_primary: true, - ..Default::default() - } + }, + DiagnosticEntry { + range: Point::new(2, 8)..Point::new(2, 17), + diagnostic: Diagnostic { + severity: DiagnosticSeverity::ERROR, + message: "error 2".to_string(), + group_id: 1, + is_primary: true, + ..Default::default() } - ) + } ] ); assert_eq!( - buffer - .diagnostic_group::(&LSP_PROVIDER_NAME, 0) - .collect::>(), + buffer.diagnostic_group::(0).collect::>(), &[ DiagnosticEntry { range: Point::new(1, 8)..Point::new(1, 9), @@ -4002,9 +3932,7 @@ mod tests { ] ); assert_eq!( - buffer - .diagnostic_group::(&LSP_PROVIDER_NAME, 1) - .collect::>(), + buffer.diagnostic_group::(1).collect::>(), &[ DiagnosticEntry { range: Point::new(1, 13)..Point::new(1, 15), diff --git a/crates/rpc/proto/zed.proto b/crates/rpc/proto/zed.proto index 0bcd99278820278bb462c966faf45718664b543c..5ef34960e78511f63b5c77dba664a7d8c716f384 100644 --- a/crates/rpc/proto/zed.proto +++ b/crates/rpc/proto/zed.proto @@ -265,7 +265,7 @@ message Buffer { string content = 2; repeated Operation.Edit history = 3; repeated SelectionSet selections = 4; - repeated DiagnosticSet diagnostic_sets = 5; + repeated Diagnostic diagnostics = 5; } message SelectionSet { @@ -292,15 +292,10 @@ enum Bias { Right = 1; } -message UpdateDiagnosticSet { +message UpdateDiagnostics { uint32 replica_id = 1; uint32 lamport_timestamp = 2; - DiagnosticSet diagnostic_set = 3; -} - -message DiagnosticSet { - string provider_name = 1; - repeated Diagnostic diagnostics = 2; + repeated Diagnostic diagnostics = 3; } message Diagnostic { @@ -329,7 +324,7 @@ message Operation { Undo undo = 2; UpdateSelections update_selections = 3; RemoveSelections remove_selections = 4; - UpdateDiagnosticSet update_diagnostic_set = 5; + UpdateDiagnostics update_diagnostics = 5; } message Edit { diff --git a/crates/rpc/src/peer.rs b/crates/rpc/src/peer.rs index 7d4adededd47ebbf1cdfdaa7034d7073b493d36d..bd5d1c384f241c328bbacf7df2f028f85db68470 100644 --- a/crates/rpc/src/peer.rs +++ b/crates/rpc/src/peer.rs @@ -401,7 +401,7 @@ mod tests { content: "path/one content".to_string(), history: vec![], selections: vec![], - diagnostic_sets: vec![], + diagnostics: vec![], }), } ); @@ -424,7 +424,7 @@ mod tests { content: "path/two content".to_string(), history: vec![], selections: vec![], - diagnostic_sets: vec![], + diagnostics: vec![], }), } ); @@ -455,7 +455,7 @@ mod tests { content: "path/one content".to_string(), history: vec![], selections: vec![], - diagnostic_sets: vec![], + diagnostics: vec![], }), } } @@ -467,7 +467,7 @@ mod tests { content: "path/two content".to_string(), history: vec![], selections: vec![], - diagnostic_sets: vec![], + diagnostics: vec![], }), } } diff --git a/crates/server/src/rpc.rs b/crates/server/src/rpc.rs index e507d3d7b82828ae86a7e46b7569e614400d4b17..0b1b7e7aab3374c44f60055a3614f4d1530d6e02 100644 --- a/crates/server/src/rpc.rs +++ b/crates/server/src/rpc.rs @@ -1831,7 +1831,7 @@ mod tests { buffer .snapshot() .diagnostics_in_range::<_, Point>(0..buffer.len()) - .map(|(_, entry)| entry) + .map(|entry| entry) .collect::>(), &[ DiagnosticEntry { diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index ffbfee7d6d63a53f850d1a4acf354d5a20c256a6..b45d292cf23cc25fc89b7f24cca356bb782a4479 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -791,24 +791,16 @@ impl Workspace { { error!("failed to save item: {:?}, ", error); } - - handle.update(&mut cx, |this, cx| { - this.project.update(cx, |project, cx| project.diagnose(cx)) - }); }) .detach(); } }, ); } else { - cx.spawn(|this, mut cx| async move { + cx.spawn(|_, mut cx| async move { if let Err(error) = cx.update(|cx| item.save(cx)).unwrap().await { error!("failed to save item: {:?}, ", error); } - - this.update(&mut cx, |this, cx| { - this.project.update(cx, |project, cx| project.diagnose(cx)) - }); }) .detach(); } @@ -840,10 +832,6 @@ impl Workspace { if let Err(error) = result { error!("failed to save item: {:?}, ", error); } - - handle.update(&mut cx, |this, cx| { - this.project.update(cx, |project, cx| project.diagnose(cx)) - }); }) .detach() } diff --git a/crates/zed/src/language.rs b/crates/zed/src/language.rs index 293deada4054270b61a8cc0f7ff7401a7a944394..a84d2cbd40b7a9d16734056e29ce79c18a173bff 100644 --- a/crates/zed/src/language.rs +++ b/crates/zed/src/language.rs @@ -7,184 +7,6 @@ use std::{str, sync::Arc}; #[folder = "languages"] struct LanguageDir; -mod rust { - use anyhow::Result; - use async_trait::async_trait; - use collections::{HashMap, HashSet}; - use language::{Diagnostic, DiagnosticEntry, DiagnosticSeverity}; - use parking_lot::Mutex; - use serde::Deserialize; - use serde_json::Deserializer; - use smol::process::Command; - use std::path::{Path, PathBuf}; - use std::sync::Arc; - - #[derive(Default)] - pub struct DiagnosticProvider { - reported_paths: Mutex>>, - } - - #[derive(Debug, Deserialize)] - struct Check { - message: CompilerMessage, - } - - #[derive(Debug, Deserialize)] - struct CompilerMessage { - code: Option, - spans: Vec, - message: String, - level: ErrorLevel, - children: Vec, - } - - #[derive(Debug, Deserialize)] - enum ErrorLevel { - #[serde(rename = "warning")] - Warning, - #[serde(rename = "error")] - Error, - #[serde(rename = "help")] - Help, - #[serde(rename = "note")] - Note, - } - - #[derive(Debug, Deserialize)] - struct ErrorCode { - code: String, - } - - #[derive(Clone, Debug, Deserialize)] - struct Span { - is_primary: bool, - file_name: PathBuf, - byte_start: usize, - byte_end: usize, - expansion: Option>, - } - - #[derive(Clone, Debug, Deserialize)] - struct Expansion { - span: Span, - } - - #[async_trait] - impl language::DiagnosticProvider for DiagnosticProvider { - async fn diagnose( - &self, - root_path: Arc, - ) -> Result, Vec>>> { - let output = Command::new("cargo") - .arg("check") - .args(["--message-format", "json"]) - .current_dir(&root_path) - .output() - .await?; - - let mut group_id = 0; - let mut diagnostics_by_path = HashMap::default(); - let mut new_reported_paths = HashSet::default(); - for value in - Deserializer::from_slice(&output.stdout).into_iter::<&serde_json::value::RawValue>() - { - if let Ok(check) = serde_json::from_str::(value?.get()) { - let check_severity = match check.message.level { - ErrorLevel::Warning => DiagnosticSeverity::WARNING, - ErrorLevel::Error => DiagnosticSeverity::ERROR, - ErrorLevel::Help => DiagnosticSeverity::HINT, - ErrorLevel::Note => DiagnosticSeverity::INFORMATION, - }; - - let mut primary_span = None; - for mut span in check.message.spans { - if let Some(mut expansion) = span.expansion { - expansion.span.is_primary = span.is_primary; - span = expansion.span; - } - - let span_path: Arc = span.file_name.as_path().into(); - new_reported_paths.insert(span_path.clone()); - diagnostics_by_path - .entry(span_path) - .or_insert(Vec::new()) - .push(DiagnosticEntry { - range: span.byte_start..span.byte_end, - diagnostic: Diagnostic { - code: check.message.code.as_ref().map(|c| c.code.clone()), - severity: check_severity, - message: check.message.message.clone(), - group_id, - is_valid: true, - is_primary: span.is_primary, - is_disk_based: true, - }, - }); - - if span.is_primary { - primary_span = Some(span); - } - } - - for mut child in check.message.children { - if child.spans.is_empty() { - if let Some(primary_span) = primary_span.clone() { - child.spans.push(primary_span); - } - } else { - // TODO - continue; - } - - let child_severity = match child.level { - ErrorLevel::Warning => DiagnosticSeverity::WARNING, - ErrorLevel::Error => DiagnosticSeverity::ERROR, - ErrorLevel::Help => DiagnosticSeverity::HINT, - ErrorLevel::Note => DiagnosticSeverity::INFORMATION, - }; - - for mut span in child.spans { - if let Some(expansion) = span.expansion { - span = expansion.span; - } - - let span_path: Arc = span.file_name.as_path().into(); - new_reported_paths.insert(span_path.clone()); - diagnostics_by_path - .entry(span_path) - .or_insert(Vec::new()) - .push(DiagnosticEntry { - range: span.byte_start..span.byte_end, - diagnostic: Diagnostic { - code: child.code.as_ref().map(|c| c.code.clone()), - severity: child_severity, - message: child.message.clone(), - group_id, - is_valid: true, - is_primary: false, - is_disk_based: true, - }, - }); - } - } - - group_id += 1; - } - } - - let reported_paths = &mut *self.reported_paths.lock(); - for old_reported_path in reported_paths.iter() { - if !diagnostics_by_path.contains_key(old_reported_path) { - diagnostics_by_path.insert(old_reported_path.clone(), Default::default()); - } - } - *reported_paths = new_reported_paths; - - Ok(diagnostics_by_path) - } - } -} - pub fn build_language_registry() -> LanguageRegistry { let mut languages = LanguageRegistry::default(); languages.add(Arc::new(rust())); @@ -202,7 +24,6 @@ fn rust() -> Language { .unwrap() .with_indents_query(load_query("rust/indents.scm").as_ref()) .unwrap() - .with_diagnostic_provider(rust::DiagnosticProvider::default()) } fn markdown() -> Language { From ad1db117e6ceec1b594eff0ac4a201a186590b12 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Tue, 4 Jan 2022 17:38:45 +0100 Subject: [PATCH 06/43] Re-enable cargo check for rust-analyzer --- crates/diagnostics/src/diagnostics.rs | 80 +++++++++++++++------------ crates/language/src/diagnostic_set.rs | 1 + crates/language/src/language.rs | 8 +++ crates/lsp/src/lsp.rs | 18 +++--- crates/project/src/project.rs | 8 ++- crates/project/src/worktree.rs | 52 +++++++++++++++-- crates/zed/languages/rust/config.toml | 1 + 7 files changed, 118 insertions(+), 50 deletions(-) diff --git a/crates/diagnostics/src/diagnostics.rs b/crates/diagnostics/src/diagnostics.rs index f523b4ca7f57fe2201f69b1ade63c43e0254143c..aa917461c1dab42a3e1153654b3e6d9fe72348db 100644 --- a/crates/diagnostics/src/diagnostics.rs +++ b/crates/diagnostics/src/diagnostics.rs @@ -11,7 +11,7 @@ use gpui::{ }; use language::{Bias, Buffer, Diagnostic, DiagnosticEntry, Point}; use postage::watch; -use project::Project; +use project::{Project, ProjectPath}; use std::{cmp::Ordering, ops::Range, path::Path, sync::Arc}; use util::TryFutureExt; use workspace::Workspace; @@ -41,9 +41,11 @@ struct ProjectDiagnostics { } struct ProjectDiagnosticsEditor { + project: ModelHandle, editor: ViewHandle, excerpts: ModelHandle, path_states: Vec<(Arc, Vec)>, + paths_to_update: HashMap>, build_settings: BuildSettings, } @@ -95,41 +97,19 @@ impl ProjectDiagnosticsEditor { settings: watch::Receiver, cx: &mut ViewContext, ) -> Self { - let project_paths = project - .read(cx) - .diagnostic_summaries(cx) - .map(|e| e.0) - .collect::>(); - - cx.spawn(|this, mut cx| { - let project = project.clone(); - async move { - for project_path in project_paths { - let buffer = project - .update(&mut cx, |project, cx| project.open_buffer(project_path, cx)) - .await?; - this.update(&mut cx, |view, cx| view.populate_excerpts(buffer, cx)) + cx.subscribe(&project, |this, _, event, cx| match event { + project::Event::DiskBasedDiagnosticsUpdated { worktree_id } => { + if let Some(paths) = this.paths_to_update.remove(&worktree_id) { + this.update_excerpts(paths, cx); } - Result::<_, anyhow::Error>::Ok(()) } - }) - .detach(); - - cx.subscribe(&project, |_, project, event, cx| { - if let project::Event::DiagnosticsUpdated(project_path) = event { - let project_path = project_path.clone(); - cx.spawn(|this, mut cx| { - async move { - let buffer = project - .update(&mut cx, |project, cx| project.open_buffer(project_path, cx)) - .await?; - this.update(&mut cx, |view, cx| view.populate_excerpts(buffer, cx)); - Ok(()) - } - .log_err() - }) - .detach(); + project::Event::DiagnosticsUpdated(path) => { + this.paths_to_update + .entry(path.worktree_id) + .or_default() + .insert(path.clone()); } + _ => {} }) .detach(); @@ -139,12 +119,22 @@ impl ProjectDiagnosticsEditor { cx.add_view(|cx| Editor::for_buffer(excerpts.clone(), build_settings.clone(), cx)); cx.subscribe(&editor, |_, _, event, cx| cx.emit(*event)) .detach(); - Self { + + let paths_to_update = project + .read(cx) + .diagnostic_summaries(cx) + .map(|e| e.0) + .collect(); + let this = Self { + project, excerpts, editor, build_settings, path_states: Default::default(), - } + paths_to_update: Default::default(), + }; + this.update_excerpts(paths_to_update, cx); + this } #[cfg(test)] @@ -189,6 +179,23 @@ impl ProjectDiagnosticsEditor { .update(cx, |editor, cx| editor.remove_blocks(blocks_to_delete, cx)); } + fn update_excerpts(&self, paths: HashSet, cx: &mut ViewContext) { + let project = self.project.clone(); + cx.spawn(|this, mut cx| { + async move { + for path in paths { + let buffer = project + .update(&mut cx, |project, cx| project.open_buffer(path, cx)) + .await?; + this.update(&mut cx, |view, cx| view.populate_excerpts(buffer, cx)) + } + Result::<_, anyhow::Error>::Ok(()) + } + .log_err() + }) + .detach(); + } + fn populate_excerpts(&mut self, buffer: ModelHandle, cx: &mut ViewContext) { let snapshot; let path; @@ -572,7 +579,7 @@ mod tests { use client::{http::ServerResponse, test::FakeHttpClient, Client, UserStore}; use gpui::TestAppContext; use language::{Diagnostic, DiagnosticEntry, DiagnosticSeverity, LanguageRegistry, PointUtf16}; - use project::FakeFs; + use project::{worktree, FakeFs}; use serde_json::json; use std::sync::Arc; use unindent::Unindent as _; @@ -813,6 +820,7 @@ mod tests { cx, ) .unwrap(); + cx.emit(worktree::Event::DiskBasedDiagnosticsUpdated); }); view.condition(&mut cx, |view, cx| view.text(cx).contains("const a")) diff --git a/crates/language/src/diagnostic_set.rs b/crates/language/src/diagnostic_set.rs index 047513fce72d3bfab3c83f674e5ebc24b5026bc1..9c2091739f15acb8b2ddb6046104a5f18a5baf84 100644 --- a/crates/language/src/diagnostic_set.rs +++ b/crates/language/src/diagnostic_set.rs @@ -19,6 +19,7 @@ pub struct DiagnosticEntry { pub diagnostic: Diagnostic, } +#[derive(Debug)] pub struct DiagnosticGroup { pub entries: Vec>, pub primary_ix: usize, diff --git a/crates/language/src/language.rs b/crates/language/src/language.rs index fe832929a94b1afd52c9d54dd5ad12229a6fbf9d..bd5f91b792176ae70d8abfed7eec84bb0ea1f429 100644 --- a/crates/language/src/language.rs +++ b/crates/language/src/language.rs @@ -46,6 +46,7 @@ pub struct LanguageConfig { pub struct LanguageServerConfig { pub binary: String, pub disk_based_diagnostic_sources: HashSet, + pub disk_based_diagnostics_progress_token: Option, #[cfg(any(test, feature = "test-support"))] #[serde(skip)] pub fake_server: Option<(Arc, Arc)>, @@ -199,6 +200,13 @@ impl Language { .map(|config| &config.disk_based_diagnostic_sources) } + pub fn disk_based_diagnostics_progress_token(&self) -> Option<&String> { + self.config + .language_server + .as_ref() + .and_then(|config| config.disk_based_diagnostics_progress_token.as_ref()) + } + pub fn brackets(&self) -> &[BracketPair] { &self.config.brackets } diff --git a/crates/lsp/src/lsp.rs b/crates/lsp/src/lsp.rs index 769922523c3ca422e97f11e0964aef44c3a247b7..d0ce93b97365e4cecbbe3adc1bc3247678b44b37 100644 --- a/crates/lsp/src/lsp.rs +++ b/crates/lsp/src/lsp.rs @@ -28,7 +28,7 @@ pub use lsp_types::*; const JSON_RPC_VERSION: &'static str = "2.0"; const CONTENT_LEN_HEADER: &'static str = "Content-Length: "; -type NotificationHandler = Box; +type NotificationHandler = Box; type ResponseHandler = Box)>; pub struct LanguageServer { @@ -139,7 +139,7 @@ impl LanguageServer { if let Ok(AnyNotification { method, params }) = serde_json::from_slice(&buffer) { - if let Some(handler) = notification_handlers.read().get(method) { + if let Some(handler) = notification_handlers.write().get_mut(method) { handler(params.get()); } else { log::info!( @@ -226,15 +226,15 @@ impl LanguageServer { process_id: Default::default(), root_path: Default::default(), root_uri: Some(root_uri), - initialization_options: Some(json!({ - "checkOnSave": { - "enable": false - }, - })), + initialization_options: Default::default(), capabilities: lsp_types::ClientCapabilities { experimental: Some(json!({ "serverStatusNotification": true, })), + window: Some(lsp_types::WindowClientCapabilities { + work_done_progress: Some(true), + ..Default::default() + }), ..Default::default() }, trace: Default::default(), @@ -283,10 +283,10 @@ impl LanguageServer { } } - pub fn on_notification(&self, f: F) -> Subscription + pub fn on_notification(&self, mut f: F) -> Subscription where T: lsp_types::notification::Notification, - F: 'static + Send + Sync + Fn(T::Params), + F: 'static + Send + Sync + FnMut(T::Params), { let prev_handler = self.notification_handlers.write().insert( T::METHOD, diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 499b3d4a5261b6b72db374dd0a653a640440cb44..acabca00332c1df022410ecf7d509087b506dbf4 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -1,6 +1,6 @@ pub mod fs; mod ignore; -mod worktree; +pub mod worktree; use anyhow::{anyhow, Result}; use client::{proto, Client, PeerId, TypedEnvelope, User, UserStore}; @@ -60,6 +60,7 @@ pub struct Collaborator { pub enum Event { ActiveEntryChanged(Option), WorktreeRemoved(usize), + DiskBasedDiagnosticsUpdated { worktree_id: usize }, DiagnosticsUpdated(ProjectPath), } @@ -482,6 +483,11 @@ impl Project { path: path.clone(), })); } + worktree::Event::DiskBasedDiagnosticsUpdated => { + cx.emit(Event::DiskBasedDiagnosticsUpdated { + worktree_id: worktree.id(), + }); + } }) .detach(); self.worktrees.push(worktree); diff --git a/crates/project/src/worktree.rs b/crates/project/src/worktree.rs index 76f0e2b270792553a4b1c81e57fb30b1cc99912b..c74a2dc62fa30a1e9dd1a4ecec125fff340c9072 100644 --- a/crates/project/src/worktree.rs +++ b/crates/project/src/worktree.rs @@ -66,6 +66,7 @@ pub enum Worktree { #[derive(Debug)] pub enum Event { + DiskBasedDiagnosticsUpdated, DiagnosticsUpdated(Arc), } @@ -1037,18 +1038,61 @@ impl LocalWorktree { .disk_based_diagnostic_sources() .cloned() .unwrap_or_default(); + let disk_based_diagnostics_progress_token = + language.disk_based_diagnostics_progress_token().cloned(); let (diagnostics_tx, diagnostics_rx) = smol::channel::unbounded(); language_server .on_notification::(move |params| { smol::block_on(diagnostics_tx.send(params)).ok(); }) .detach(); + cx.spawn_weak(|this, mut cx| { + let has_disk_based_diagnostic_progress_token = + disk_based_diagnostics_progress_token.is_some(); + async move { + while let Ok(diagnostics) = diagnostics_rx.recv().await { + if let Some(handle) = cx.read(|cx| this.upgrade(cx)) { + handle.update(&mut cx, |this, cx| { + this.update_diagnostics(diagnostics, &disk_based_sources, cx) + .log_err(); + if !has_disk_based_diagnostic_progress_token { + cx.emit(Event::DiskBasedDiagnosticsUpdated); + } + }); + } else { + break; + } + } + } + }) + .detach(); + + let (mut disk_based_diagnostics_done_tx, mut disk_based_diagnostics_done_rx) = + watch::channel_with(()); + language_server + .on_notification::(move |params| { + let token = match params.token { + lsp::NumberOrString::Number(_) => None, + lsp::NumberOrString::String(token) => Some(token), + }; + + if token == disk_based_diagnostics_progress_token { + match params.value { + lsp::ProgressParamsValue::WorkDone(progress) => match progress { + lsp::WorkDoneProgress::End(_) => { + smol::block_on(disk_based_diagnostics_done_tx.send(())).ok(); + } + _ => {} + }, + } + } + }) + .detach(); cx.spawn_weak(|this, mut cx| async move { - while let Ok(diagnostics) = diagnostics_rx.recv().await { + while let Some(()) = disk_based_diagnostics_done_rx.recv().await { if let Some(handle) = cx.read(|cx| this.upgrade(cx)) { - handle.update(&mut cx, |this, cx| { - this.update_diagnostics(diagnostics, &disk_based_sources, cx) - .log_err(); + handle.update(&mut cx, |_, cx| { + cx.emit(Event::DiskBasedDiagnosticsUpdated); }); } else { break; diff --git a/crates/zed/languages/rust/config.toml b/crates/zed/languages/rust/config.toml index 655a264e6ca6c4f37a9afc8c343936b15d9bf8ac..426dcc2b48b3c16b74a255499180605b099c6a7c 100644 --- a/crates/zed/languages/rust/config.toml +++ b/crates/zed/languages/rust/config.toml @@ -13,3 +13,4 @@ brackets = [ [language_server] binary = "rust-analyzer" disk_based_diagnostic_sources = ["rustc"] +disk_based_diagnostics_progress_token = "rustAnalyzer/cargo check" From d8b888c9cb366870748b318b157209c84a714812 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Tue, 4 Jan 2022 14:29:22 -0800 Subject: [PATCH 07/43] Replicate diagnostic summaries Co-Authored-By: Antonio Scandurra --- crates/gpui/src/app.rs | 4 ++ crates/gpui/src/executor.rs | 20 +++++++-- crates/project/src/project.rs | 44 ++++++++++++++++++- crates/project/src/worktree.rs | 77 ++++++++++++++++++++++++++++++---- crates/rpc/proto/zed.proto | 60 ++++++++++++++------------ crates/rpc/src/proto.rs | 10 +++-- crates/server/src/rpc.rs | 74 ++++++++++++++++++++++++++------ 7 files changed, 235 insertions(+), 54 deletions(-) diff --git a/crates/gpui/src/app.rs b/crates/gpui/src/app.rs index a14664a9f824ec19a993ccbae9e21544003bb6ab..e1b919e5fea999c333b33e07fb8046ec8251b69b 100644 --- a/crates/gpui/src/app.rs +++ b/crates/gpui/src/app.rs @@ -2672,6 +2672,10 @@ impl ModelHandle { } } + if cx.borrow_mut().foreground().would_park() { + panic!("parked while waiting on condition"); + } + rx.recv() .await .expect("model dropped with pending condition"); diff --git a/crates/gpui/src/executor.rs b/crates/gpui/src/executor.rs index 23b870c11f09539c44a3ad0efb629420cee3be46..3283ab755397d874336d8817d4fc868c702aea4f 100644 --- a/crates/gpui/src/executor.rs +++ b/crates/gpui/src/executor.rs @@ -206,10 +206,7 @@ impl Deterministic { } let state = self.state.lock(); - if state.scheduled_from_foreground.is_empty() - && state.scheduled_from_background.is_empty() - && state.spawned_from_foreground.is_empty() - { + if state.would_park() { return None; } } @@ -261,6 +258,14 @@ impl Deterministic { } } +impl DeterministicState { + fn would_park(&self) -> bool { + self.scheduled_from_foreground.is_empty() + && self.scheduled_from_background.is_empty() + && self.spawned_from_foreground.is_empty() + } +} + #[derive(Default)] struct Trace { executed: Vec, @@ -433,6 +438,13 @@ impl Foreground { *any_value.downcast().unwrap() } + pub fn would_park(&self) -> bool { + match self { + Self::Deterministic(executor) => executor.state.lock().would_park(), + _ => panic!("this method can only be called on a deterministic executor"), + } + } + pub fn forbid_parking(&self) { match self { Self::Deterministic(executor) => { diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index c05e0c8f4aa37e5dc4febca963bc13e1160cf6ad..8ce3504e5d90c3f8298aaf855f41ba81393dda39 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -70,7 +70,7 @@ pub struct ProjectPath { pub path: Arc, } -#[derive(Clone)] +#[derive(Clone, Debug, Default, PartialEq)] pub struct DiagnosticSummary { pub error_count: usize, pub warning_count: usize, @@ -243,6 +243,12 @@ impl Project { client.subscribe_to_entity(remote_id, cx, Self::handle_share_worktree), client.subscribe_to_entity(remote_id, cx, Self::handle_unregister_worktree), client.subscribe_to_entity(remote_id, cx, Self::handle_update_worktree), + client.subscribe_to_entity(remote_id, cx, Self::handle_update_diagnostic_summary), + client.subscribe_to_entity( + remote_id, + cx, + Self::handle_disk_based_diagnostics_updated, + ), client.subscribe_to_entity(remote_id, cx, Self::handle_update_buffer), client.subscribe_to_entity(remote_id, cx, Self::handle_buffer_saved), ], @@ -661,6 +667,42 @@ impl Project { Ok(()) } + fn handle_update_diagnostic_summary( + &mut self, + envelope: TypedEnvelope, + _: Arc, + cx: &mut ModelContext, + ) -> Result<()> { + let worktree_id = WorktreeId::from_proto(envelope.payload.worktree_id); + if let Some(worktree) = self.worktree_for_id(worktree_id, cx) { + worktree.update(cx, |worktree, cx| { + worktree + .as_remote_mut() + .unwrap() + .update_diagnostic_summary(envelope, cx); + }); + } + Ok(()) + } + + fn handle_disk_based_diagnostics_updated( + &mut self, + envelope: TypedEnvelope, + _: Arc, + cx: &mut ModelContext, + ) -> Result<()> { + let worktree_id = WorktreeId::from_proto(envelope.payload.worktree_id); + if let Some(worktree) = self.worktree_for_id(worktree_id, cx) { + worktree.update(cx, |worktree, cx| { + worktree + .as_remote() + .unwrap() + .disk_based_diagnostics_updated(cx); + }); + } + Ok(()) + } + pub fn handle_update_buffer( &mut self, envelope: TypedEnvelope, diff --git a/crates/project/src/worktree.rs b/crates/project/src/worktree.rs index 030e55deae3c61dfa783757d9d9835f73b31f17a..f7538f629463be2a98e6ac62d857cae69f0521e4 100644 --- a/crates/project/src/worktree.rs +++ b/crates/project/src/worktree.rs @@ -777,10 +777,38 @@ impl Worktree { } let this = self.as_local_mut().unwrap(); + let summary = DiagnosticSummary::new(&diagnostics); this.diagnostic_summaries - .insert(worktree_path.clone(), DiagnosticSummary::new(&diagnostics)); + .insert(worktree_path.clone(), summary.clone()); this.diagnostics.insert(worktree_path.clone(), diagnostics); + cx.emit(Event::DiagnosticsUpdated(worktree_path.clone())); + + if let Some(share) = this.share.as_ref() { + cx.foreground() + .spawn({ + let client = this.client.clone(); + let project_id = share.project_id; + let worktree_id = this.id().to_proto(); + let path = worktree_path.to_string_lossy().to_string(); + async move { + client + .send(proto::UpdateDiagnosticSummary { + project_id, + worktree_id, + path, + error_count: summary.error_count as u32, + warning_count: summary.warning_count as u32, + info_count: summary.info_count as u32, + hint_count: summary.hint_count as u32, + }) + .await + .log_err() + } + }) + .detach(); + } + Ok(()) } @@ -1063,6 +1091,8 @@ impl LocalWorktree { let disk_based_diagnostics_progress_token = language.disk_based_diagnostics_progress_token().cloned(); let (diagnostics_tx, diagnostics_rx) = smol::channel::unbounded(); + let (disk_based_diagnostics_done_tx, disk_based_diagnostics_done_rx) = + smol::channel::unbounded(); language_server .on_notification::(move |params| { smol::block_on(diagnostics_tx.send(params)).ok(); @@ -1071,6 +1101,7 @@ impl LocalWorktree { cx.spawn_weak(|this, mut cx| { let has_disk_based_diagnostic_progress_token = disk_based_diagnostics_progress_token.is_some(); + let disk_based_diagnostics_done_tx = disk_based_diagnostics_done_tx.clone(); async move { while let Ok(diagnostics) = diagnostics_rx.recv().await { if let Some(handle) = cx.read(|cx| this.upgrade(cx)) { @@ -1078,9 +1109,9 @@ impl LocalWorktree { this.update_diagnostics(diagnostics, &disk_based_sources, cx) .log_err(); if !has_disk_based_diagnostic_progress_token { - cx.emit(Event::DiskBasedDiagnosticsUpdated); + smol::block_on(disk_based_diagnostics_done_tx.send(())).ok(); } - }); + }) } else { break; } @@ -1089,8 +1120,6 @@ impl LocalWorktree { }) .detach(); - let (mut disk_based_diagnostics_done_tx, mut disk_based_diagnostics_done_rx) = - watch::channel_with(()); language_server .on_notification::(move |params| { let token = match params.token { @@ -1110,12 +1139,24 @@ impl LocalWorktree { } }) .detach(); + let rpc = self.client.clone(); cx.spawn_weak(|this, mut cx| async move { - while let Some(()) = disk_based_diagnostics_done_rx.recv().await { + while let Ok(()) = disk_based_diagnostics_done_rx.recv().await { if let Some(handle) = cx.read(|cx| this.upgrade(cx)) { - handle.update(&mut cx, |_, cx| { + let message = handle.update(&mut cx, |this, cx| { cx.emit(Event::DiskBasedDiagnosticsUpdated); + let this = this.as_local().unwrap(); + this.share + .as_ref() + .map(|share| proto::DiskBasedDiagnosticsUpdated { + project_id: share.project_id, + worktree_id: this.id().to_proto(), + }) }); + + if let Some(message) = message { + rpc.send(message).await.log_err(); + } } else { break; } @@ -1572,6 +1613,28 @@ impl RemoteWorktree { Ok(()) } + pub fn update_diagnostic_summary( + &mut self, + envelope: TypedEnvelope, + cx: &mut ModelContext, + ) { + let path: Arc = Path::new(&envelope.payload.path).into(); + self.diagnostic_summaries.insert( + path.clone(), + DiagnosticSummary { + error_count: envelope.payload.error_count as usize, + warning_count: envelope.payload.warning_count as usize, + info_count: envelope.payload.info_count as usize, + hint_count: envelope.payload.hint_count as usize, + }, + ); + cx.emit(Event::DiagnosticsUpdated(path)); + } + + pub fn disk_based_diagnostics_updated(&self, cx: &mut ModelContext) { + cx.emit(Event::DiskBasedDiagnosticsUpdated); + } + pub fn remove_collaborator(&mut self, replica_id: ReplicaId, cx: &mut ModelContext) { for (_, buffer) in &self.open_buffers { if let Some(buffer) = buffer.upgrade(cx) { diff --git a/crates/rpc/proto/zed.proto b/crates/rpc/proto/zed.proto index 5ef34960e78511f63b5c77dba664a7d8c716f384..9ec5f907199683b72cb066926dcfc1820a914e3d 100644 --- a/crates/rpc/proto/zed.proto +++ b/crates/rpc/proto/zed.proto @@ -23,32 +23,33 @@ message Envelope { RegisterWorktree register_worktree = 17; UnregisterWorktree unregister_worktree = 18; - ShareWorktree share_worktree = 100; - UpdateWorktree update_worktree = 19; - UpdateDiagnosticSummary update_diagnostic_summary = 20; - - OpenBuffer open_buffer = 22; - OpenBufferResponse open_buffer_response = 23; - CloseBuffer close_buffer = 24; - UpdateBuffer update_buffer = 25; - SaveBuffer save_buffer = 26; - BufferSaved buffer_saved = 27; - - GetChannels get_channels = 28; - GetChannelsResponse get_channels_response = 29; - JoinChannel join_channel = 30; - JoinChannelResponse join_channel_response = 31; - LeaveChannel leave_channel = 32; - SendChannelMessage send_channel_message = 33; - SendChannelMessageResponse send_channel_message_response = 34; - ChannelMessageSent channel_message_sent = 35; - GetChannelMessages get_channel_messages = 36; - GetChannelMessagesResponse get_channel_messages_response = 37; - - UpdateContacts update_contacts = 38; - - GetUsers get_users = 39; - GetUsersResponse get_users_response = 40; + ShareWorktree share_worktree = 19; + UpdateWorktree update_worktree = 20; + UpdateDiagnosticSummary update_diagnostic_summary = 21; + DiskBasedDiagnosticsUpdated disk_based_diagnostics_updated = 22; + + OpenBuffer open_buffer = 23; + OpenBufferResponse open_buffer_response = 24; + CloseBuffer close_buffer = 25; + UpdateBuffer update_buffer = 26; + SaveBuffer save_buffer = 27; + BufferSaved buffer_saved = 28; + + GetChannels get_channels = 29; + GetChannelsResponse get_channels_response = 30; + JoinChannel join_channel = 31; + JoinChannelResponse join_channel_response = 32; + LeaveChannel leave_channel = 33; + SendChannelMessage send_channel_message = 34; + SendChannelMessageResponse send_channel_message_response = 35; + ChannelMessageSent channel_message_sent = 36; + GetChannelMessages get_channel_messages = 37; + GetChannelMessagesResponse get_channel_messages_response = 38; + + UpdateContacts update_contacts = 39; + + GetUsers get_users = 40; + GetUsersResponse get_users_response = 41; } } @@ -172,6 +173,13 @@ message UpdateDiagnosticSummary { string path = 3; uint32 error_count = 4; uint32 warning_count = 5; + uint32 info_count = 6; + uint32 hint_count = 7; +} + +message DiskBasedDiagnosticsUpdated { + uint64 project_id = 1; + uint64 worktree_id = 2; } message GetChannels {} diff --git a/crates/rpc/src/proto.rs b/crates/rpc/src/proto.rs index 9274049a7b601e1c25bbcdb816c8ed3d8b07ccbe..2cabdc921859c9a90cc218cde37a1f0f60720107 100644 --- a/crates/rpc/src/proto.rs +++ b/crates/rpc/src/proto.rs @@ -125,6 +125,7 @@ messages!( BufferSaved, ChannelMessageSent, CloseBuffer, + DiskBasedDiagnosticsUpdated, Error, GetChannelMessages, GetChannelMessagesResponse, @@ -155,6 +156,7 @@ messages!( UnshareProject, UpdateBuffer, UpdateContacts, + UpdateDiagnosticSummary, UpdateWorktree, ); @@ -178,17 +180,19 @@ request_messages!( entity_messages!( project_id, AddProjectCollaborator, - RemoveProjectCollaborator, + BufferSaved, + CloseBuffer, + DiskBasedDiagnosticsUpdated, JoinProject, LeaveProject, - BufferSaved, OpenBuffer, - CloseBuffer, + RemoveProjectCollaborator, SaveBuffer, ShareWorktree, UnregisterWorktree, UnshareProject, UpdateBuffer, + UpdateDiagnosticSummary, UpdateWorktree, ); diff --git a/crates/server/src/rpc.rs b/crates/server/src/rpc.rs index 0b1b7e7aab3374c44f60055a3614f4d1530d6e02..7e955ca335d41b14670a5f62886210c166a4660c 100644 --- a/crates/server/src/rpc.rs +++ b/crates/server/src/rpc.rs @@ -71,6 +71,8 @@ impl Server { .add_handler(Server::unregister_worktree) .add_handler(Server::share_worktree) .add_handler(Server::update_worktree) + .add_handler(Server::update_diagnostic_summary) + .add_handler(Server::disk_based_diagnostics_updated) .add_handler(Server::open_buffer) .add_handler(Server::close_buffer) .add_handler(Server::update_buffer) @@ -517,6 +519,38 @@ impl Server { Ok(()) } + async fn update_diagnostic_summary( + self: Arc, + request: TypedEnvelope, + ) -> tide::Result<()> { + let receiver_ids = self + .state() + .project_connection_ids(request.payload.project_id, request.sender_id) + .ok_or_else(|| anyhow!(NO_SUCH_PROJECT))?; + broadcast(request.sender_id, receiver_ids, |connection_id| { + self.peer + .forward_send(request.sender_id, connection_id, request.payload.clone()) + }) + .await?; + Ok(()) + } + + async fn disk_based_diagnostics_updated( + self: Arc, + request: TypedEnvelope, + ) -> tide::Result<()> { + let receiver_ids = self + .state() + .project_connection_ids(request.payload.project_id, request.sender_id) + .ok_or_else(|| anyhow!(NO_SUCH_PROJECT))?; + broadcast(request.sender_id, receiver_ids, |connection_id| { + self.peer + .forward_send(request.sender_id, connection_id, request.payload.clone()) + }) + .await?; + Ok(()) + } + async fn open_buffer( self: Arc, request: TypedEnvelope, @@ -1026,7 +1060,7 @@ mod tests { LanguageRegistry, LanguageServerConfig, Point, }, lsp, - project::Project, + project::{DiagnosticSummary, Project}, }; #[gpui::test] @@ -1781,6 +1815,19 @@ mod tests { .await .unwrap(); + // Join the worktree as client B. + let project_b = Project::remote( + project_id, + client_b.clone(), + client_b.user_store.clone(), + lang_registry.clone(), + fs.clone(), + &mut cx_b.to_async(), + ) + .await + .unwrap(); + let worktree_b = project_b.update(&mut cx_b, |p, _| p.worktrees()[0].clone()); + // Simulate a language server reporting errors for a file. fake_language_server .notify::(lsp::PublishDiagnosticsParams { @@ -1806,18 +1853,19 @@ mod tests { }) .await; - // Join the worktree as client B. - let project_b = Project::remote( - project_id, - client_b.clone(), - client_b.user_store.clone(), - lang_registry.clone(), - fs.clone(), - &mut cx_b.to_async(), - ) - .await - .unwrap(); - let worktree_b = project_b.update(&mut cx_b, |p, _| p.worktrees()[0].clone()); + worktree_b + .condition(&cx_b, |worktree, _| { + worktree.diagnostic_summaries().collect::>() + == &[( + Arc::from(Path::new("a.rs")), + DiagnosticSummary { + error_count: 1, + warning_count: 1, + ..Default::default() + }, + )] + }) + .await; // Open the file with the errors. let buffer_b = cx_b From 0bcd0a3f08144351a50dd11edb8902c49e473bec Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Tue, 4 Jan 2022 15:49:29 -0800 Subject: [PATCH 08/43] Forward events from remote worktrees to their projects --- crates/project/src/project.rs | 66 ++++++++++++++++++++--------------- crates/server/src/rpc.rs | 16 +++++---- 2 files changed, 48 insertions(+), 34 deletions(-) diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 8ce3504e5d90c3f8298aaf855f41ba81393dda39..a0ed7b811d24753cdb67aa7e5aa6fc961ea38ee6 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -229,35 +229,45 @@ impl Project { collaborators.insert(collaborator.peer_id, collaborator); } - Ok(cx.add_model(|cx| Self { - worktrees, - active_entry: None, - collaborators, - languages, - user_store, - fs, - subscriptions: vec![ - client.subscribe_to_entity(remote_id, cx, Self::handle_unshare_project), - client.subscribe_to_entity(remote_id, cx, Self::handle_add_collaborator), - client.subscribe_to_entity(remote_id, cx, Self::handle_remove_collaborator), - client.subscribe_to_entity(remote_id, cx, Self::handle_share_worktree), - client.subscribe_to_entity(remote_id, cx, Self::handle_unregister_worktree), - client.subscribe_to_entity(remote_id, cx, Self::handle_update_worktree), - client.subscribe_to_entity(remote_id, cx, Self::handle_update_diagnostic_summary), - client.subscribe_to_entity( + Ok(cx.add_model(|cx| { + let mut this = Self { + worktrees: Vec::new(), + active_entry: None, + collaborators, + languages, + user_store, + fs, + subscriptions: vec![ + client.subscribe_to_entity(remote_id, cx, Self::handle_unshare_project), + client.subscribe_to_entity(remote_id, cx, Self::handle_add_collaborator), + client.subscribe_to_entity(remote_id, cx, Self::handle_remove_collaborator), + client.subscribe_to_entity(remote_id, cx, Self::handle_share_worktree), + client.subscribe_to_entity(remote_id, cx, Self::handle_unregister_worktree), + client.subscribe_to_entity(remote_id, cx, Self::handle_update_worktree), + client.subscribe_to_entity( + remote_id, + cx, + Self::handle_update_diagnostic_summary, + ), + client.subscribe_to_entity( + remote_id, + cx, + Self::handle_disk_based_diagnostics_updated, + ), + client.subscribe_to_entity(remote_id, cx, Self::handle_update_buffer), + client.subscribe_to_entity(remote_id, cx, Self::handle_buffer_saved), + ], + client, + client_state: ProjectClientState::Remote { + sharing_has_stopped: false, remote_id, - cx, - Self::handle_disk_based_diagnostics_updated, - ), - client.subscribe_to_entity(remote_id, cx, Self::handle_update_buffer), - client.subscribe_to_entity(remote_id, cx, Self::handle_buffer_saved), - ], - client, - client_state: ProjectClientState::Remote { - sharing_has_stopped: false, - remote_id, - replica_id, - }, + replica_id, + }, + }; + for worktree in worktrees { + this.add_worktree(worktree, cx); + } + this })) } diff --git a/crates/server/src/rpc.rs b/crates/server/src/rpc.rs index 7e955ca335d41b14670a5f62886210c166a4660c..0d84a89e1435168c2b805d20806c303ec223c69f 100644 --- a/crates/server/src/rpc.rs +++ b/crates/server/src/rpc.rs @@ -1060,7 +1060,7 @@ mod tests { LanguageRegistry, LanguageServerConfig, Point, }, lsp, - project::{DiagnosticSummary, Project}, + project::{DiagnosticSummary, Project, ProjectPath}, }; #[gpui::test] @@ -1801,6 +1801,7 @@ mod tests { let project_id = project_a .update(&mut cx_a, |project, _| project.next_remote_id()) .await; + let worktree_id = worktree_a.read_with(&cx_a, |tree, _| tree.id()); project_a .update(&mut cx_a, |project, cx| project.share(cx)) .await @@ -1826,7 +1827,6 @@ mod tests { ) .await .unwrap(); - let worktree_b = project_b.update(&mut cx_b, |p, _| p.worktrees()[0].clone()); // Simulate a language server reporting errors for a file. fake_language_server @@ -1853,11 +1853,14 @@ mod tests { }) .await; - worktree_b - .condition(&cx_b, |worktree, _| { - worktree.diagnostic_summaries().collect::>() + project_b + .condition(&cx_b, |project, cx| { + project.diagnostic_summaries(cx).collect::>() == &[( - Arc::from(Path::new("a.rs")), + ProjectPath { + worktree_id, + path: Arc::from(Path::new("a.rs")), + }, DiagnosticSummary { error_count: 1, warning_count: 1, @@ -1868,6 +1871,7 @@ mod tests { .await; // Open the file with the errors. + let worktree_b = project_b.update(&mut cx_b, |p, _| p.worktrees()[0].clone()); let buffer_b = cx_b .background() .spawn(worktree_b.update(&mut cx_b, |worktree, cx| worktree.open_buffer("a.rs", cx))) From fe28abe8cf464251791afccb3f11606a7ae26c68 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Wed, 5 Jan 2022 17:25:03 +0100 Subject: [PATCH 09/43] Show a message when no diagnostics are available --- crates/diagnostics/src/diagnostics.rs | 66 +++++++++------------------ crates/theme/src/theme.rs | 8 ++++ crates/zed/assets/themes/_base.toml | 4 ++ 3 files changed, 34 insertions(+), 44 deletions(-) diff --git a/crates/diagnostics/src/diagnostics.rs b/crates/diagnostics/src/diagnostics.rs index 42230c6d1b30391c175401e0e0a5d0862f6e9d0c..da1b661333ff3855aff4b1103c0564af53f38a12 100644 --- a/crates/diagnostics/src/diagnostics.rs +++ b/crates/diagnostics/src/diagnostics.rs @@ -17,21 +17,12 @@ use util::TryFutureExt; use workspace::Workspace; action!(Toggle); -action!(ClearInvalid); const CONTEXT_LINE_COUNT: u32 = 1; pub fn init(cx: &mut MutableAppContext) { - cx.add_bindings([ - Binding::new("alt-shift-D", Toggle, None), - Binding::new( - "alt-shift-C", - ClearInvalid, - Some("ProjectDiagnosticsEditor"), - ), - ]); + cx.add_bindings([Binding::new("alt-shift-D", Toggle, None)]); cx.add_action(ProjectDiagnosticsEditor::toggle); - cx.add_action(ProjectDiagnosticsEditor::clear_invalid); } type Event = editor::Event; @@ -47,6 +38,7 @@ struct ProjectDiagnosticsEditor { path_states: Vec<(Arc, Vec)>, paths_to_update: HashMap>, build_settings: BuildSettings, + settings: watch::Receiver, } struct DiagnosticGroupState { @@ -83,11 +75,25 @@ impl View for ProjectDiagnosticsEditor { } fn render(&mut self, _: &mut RenderContext) -> ElementBox { - ChildView::new(self.editor.id()).boxed() + if self.path_states.is_empty() { + let theme = &self.settings.borrow().theme.project_diagnostics; + Label::new( + "No problems detected in the project".to_string(), + theme.empty_message.clone(), + ) + .aligned() + .contained() + .with_style(theme.container) + .boxed() + } else { + ChildView::new(self.editor.id()).boxed() + } } fn on_focus(&mut self, cx: &mut ViewContext) { - cx.focus(&self.editor); + if !self.path_states.is_empty() { + cx.focus(&self.editor); + } } } @@ -130,6 +136,7 @@ impl ProjectDiagnosticsEditor { excerpts, editor, build_settings, + settings, path_states: Default::default(), paths_to_update: Default::default(), }; @@ -147,38 +154,6 @@ impl ProjectDiagnosticsEditor { workspace.add_item(diagnostics, cx); } - fn clear_invalid(&mut self, _: &ClearInvalid, cx: &mut ViewContext) { - let mut blocks_to_delete = HashSet::default(); - let mut excerpts_to_delete = Vec::new(); - let mut path_ixs_to_delete = Vec::new(); - for (ix, (_, groups)) in self.path_states.iter_mut().enumerate() { - groups.retain(|group| { - if group.is_valid { - true - } else { - blocks_to_delete.extend(group.blocks.keys().copied()); - excerpts_to_delete.extend(group.excerpts.iter().cloned()); - false - } - }); - - if groups.is_empty() { - path_ixs_to_delete.push(ix); - } - } - - for ix in path_ixs_to_delete.into_iter().rev() { - self.path_states.remove(ix); - } - - self.excerpts.update(cx, |excerpts, cx| { - excerpts_to_delete.sort_unstable(); - excerpts.remove_excerpts(&excerpts_to_delete, cx) - }); - self.editor - .update(cx, |editor, cx| editor.remove_blocks(blocks_to_delete, cx)); - } - fn update_excerpts(&self, paths: HashSet, cx: &mut ViewContext) { let project = self.project.clone(); cx.spawn(|this, mut cx| { @@ -488,6 +463,9 @@ impl ProjectDiagnosticsEditor { self.path_states.remove(path_ix); } + if self.editor.is_focused(cx) && self.path_states.is_empty() { + cx.focus_self(); + } cx.notify(); } } diff --git a/crates/theme/src/theme.rs b/crates/theme/src/theme.rs index 6fca6966896c2290f2bc2ed2a7c69b15ec50b872..6a67789afe114e2b19c288259ba08ed5c7a099f4 100644 --- a/crates/theme/src/theme.rs +++ b/crates/theme/src/theme.rs @@ -24,6 +24,7 @@ pub struct Theme { pub project_panel: ProjectPanel, pub selector: Selector, pub editor: EditorStyle, + pub project_diagnostics: ProjectDiagnostics, } #[derive(Deserialize, Default)] @@ -226,6 +227,13 @@ pub struct ContainedLabel { pub label: LabelStyle, } +#[derive(Clone, Deserialize, Default)] +pub struct ProjectDiagnostics { + #[serde(flatten)] + pub container: ContainerStyle, + pub empty_message: TextStyle, +} + #[derive(Clone, Deserialize, Default)] pub struct EditorStyle { pub text: TextStyle, diff --git a/crates/zed/assets/themes/_base.toml b/crates/zed/assets/themes/_base.toml index 958620521d8e9048feea444275fb79ec101f0cf5..4063fc92f95a7c95351af9eafb69ab78c0dc7d85 100644 --- a/crates/zed/assets/themes/_base.toml +++ b/crates/zed/assets/themes/_base.toml @@ -257,3 +257,7 @@ information_diagnostic = { text = "$status.info" } invalid_information_diagnostic = { text = "$text.3.color" } hint_diagnostic = { text = "$status.info" } invalid_hint_diagnostic = { text = "$text.3.color" } + +[project_diagnostics] +background = "$surface.1" +empty_message = "$text.0" From 29b63ae4c68123af148af47f8b07962aa6f4b0b4 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Wed, 5 Jan 2022 17:35:45 +0100 Subject: [PATCH 10/43] Remove invalid excerpts as opposed to styling them differently --- crates/diagnostics/src/diagnostics.rs | 89 ++------------------------- crates/editor/src/editor.rs | 4 -- 2 files changed, 5 insertions(+), 88 deletions(-) diff --git a/crates/diagnostics/src/diagnostics.rs b/crates/diagnostics/src/diagnostics.rs index da1b661333ff3855aff4b1103c0564af53f38a12..8820c1097d9c2c48077974348ace443ada4b9f95 100644 --- a/crates/diagnostics/src/diagnostics.rs +++ b/crates/diagnostics/src/diagnostics.rs @@ -46,7 +46,6 @@ struct DiagnosticGroupState { excerpts: Vec, blocks: HashMap, block_count: usize, - is_valid: bool, } enum DiagnosticBlock { @@ -207,18 +206,10 @@ impl ProjectDiagnosticsEditor { let mut groups_to_add = Vec::new(); let mut group_ixs_to_remove = Vec::new(); let mut blocks_to_add = Vec::new(); - let mut blocks_to_restyle = HashMap::default(); let mut blocks_to_remove = HashSet::default(); - let selected_excerpts = self - .editor - .read(cx) - .local_anchor_selections() - .iter() - .flat_map(|s| [s.start.excerpt_id().clone(), s.end.excerpt_id().clone()]) - .collect::>(); let mut diagnostic_blocks = Vec::new(); let excerpts_snapshot = self.excerpts.update(cx, |excerpts, excerpts_cx| { - let mut old_groups = groups.iter_mut().enumerate().peekable(); + let mut old_groups = groups.iter().enumerate().peekable(); let mut new_groups = snapshot .diagnostic_groups() .into_iter() @@ -228,7 +219,6 @@ impl ProjectDiagnosticsEditor { loop { let mut to_insert = None; let mut to_invalidate = None; - let mut to_validate = None; match (old_groups.peek(), new_groups.peek()) { (None, None) => break, (None, Some(_)) => to_insert = new_groups.next(), @@ -239,7 +229,7 @@ impl ProjectDiagnosticsEditor { match compare_diagnostics(old_primary, new_primary, &snapshot) { Ordering::Less => to_invalidate = old_groups.next(), Ordering::Equal => { - to_validate = old_groups.next(); + old_groups.next(); new_groups.next(); } Ordering::Greater => to_insert = new_groups.next(), @@ -253,7 +243,6 @@ impl ProjectDiagnosticsEditor { excerpts: Default::default(), blocks: Default::default(), block_count: 0, - is_valid: true, }; let mut pending_range: Option<(Range, usize)> = None; let mut is_first_excerpt_for_group = true; @@ -345,76 +334,9 @@ impl ProjectDiagnosticsEditor { groups_to_add.push(group_state); } else if let Some((group_ix, group_state)) = to_invalidate { - if group_state - .excerpts - .iter() - .any(|excerpt_id| selected_excerpts.contains(excerpt_id)) - { - for (block_id, block) in &group_state.blocks { - match block { - DiagnosticBlock::Header(diagnostic) => { - blocks_to_restyle.insert( - *block_id, - diagnostic_header_renderer( - buffer.clone(), - diagnostic.clone(), - false, - self.build_settings.clone(), - ), - ); - } - DiagnosticBlock::Inline(diagnostic) => { - blocks_to_restyle.insert( - *block_id, - diagnostic_block_renderer( - diagnostic.clone(), - false, - self.build_settings.clone(), - ), - ); - } - DiagnosticBlock::Context => {} - } - } - - group_state.is_valid = false; - prev_excerpt_id = group_state.excerpts.last().unwrap().clone(); - } else { - excerpts.remove_excerpts(group_state.excerpts.iter(), excerpts_cx); - group_ixs_to_remove.push(group_ix); - blocks_to_remove.extend(group_state.blocks.keys().copied()); - } - } else if let Some((_, group_state)) = to_validate { - for (block_id, block) in &group_state.blocks { - match block { - DiagnosticBlock::Header(diagnostic) => { - blocks_to_restyle.insert( - *block_id, - diagnostic_header_renderer( - buffer.clone(), - diagnostic.clone(), - true, - self.build_settings.clone(), - ), - ); - } - DiagnosticBlock::Inline(diagnostic) => { - blocks_to_restyle.insert( - *block_id, - diagnostic_block_renderer( - diagnostic.clone(), - true, - self.build_settings.clone(), - ), - ); - } - DiagnosticBlock::Context => {} - } - } - group_state.is_valid = true; - prev_excerpt_id = group_state.excerpts.last().unwrap().clone(); - } else { - unreachable!(); + excerpts.remove_excerpts(group_state.excerpts.iter(), excerpts_cx); + group_ixs_to_remove.push(group_ix); + blocks_to_remove.extend(group_state.blocks.keys().copied()); } } @@ -423,7 +345,6 @@ impl ProjectDiagnosticsEditor { self.editor.update(cx, |editor, cx| { editor.remove_blocks(blocks_to_remove, cx); - editor.replace_blocks(blocks_to_restyle, cx); let mut block_ids = editor .insert_blocks( blocks_to_add.into_iter().map(|block| { diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index f418efd8d00908bb51984ffe8d2d9c891822fcd3..ddf3fbd37f626aeaab7307fe309959b30dd78b08 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -3098,10 +3098,6 @@ impl Editor { .collect() } - pub fn local_anchor_selections(&self) -> &Arc<[Selection]> { - &self.selections - } - fn resolve_selections<'a, D, I>( &self, selections: I, From 85a13fa4772ee6f72db29098120f2ae12a48baa8 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Wed, 5 Jan 2022 11:28:49 -0800 Subject: [PATCH 11/43] Fix panic when resolving anchors after an excerpt id has been recycled Co-Authored-By: Nathan Sobo --- crates/editor/src/multi_buffer.rs | 57 +++++++++++++++++++++++- crates/editor/src/multi_buffer/anchor.rs | 5 +++ crates/text/src/text.rs | 4 +- 3 files changed, 62 insertions(+), 4 deletions(-) diff --git a/crates/editor/src/multi_buffer.rs b/crates/editor/src/multi_buffer.rs index 7e718552366b03fb4979fc7a363176cba99078c3..4d39b921401a24432e126ca8f7d0f7e3b3959d3a 100644 --- a/crates/editor/src/multi_buffer.rs +++ b/crates/editor/src/multi_buffer.rs @@ -1291,7 +1291,7 @@ impl MultiBufferSnapshot { let mut position = D::from_text_summary(&cursor.start().text); if let Some(excerpt) = cursor.item() { - if excerpt.id == anchor.excerpt_id { + if excerpt.id == anchor.excerpt_id && excerpt.buffer_id == anchor.buffer_id { let excerpt_buffer_start = excerpt.range.start.summary::(&excerpt.buffer); let buffer_position = anchor.text_anchor.summary::(&excerpt.buffer); if buffer_position > excerpt_buffer_start { @@ -1312,6 +1312,7 @@ impl MultiBufferSnapshot { let mut summaries = Vec::new(); while let Some(anchor) = anchors.peek() { let excerpt_id = &anchor.excerpt_id; + let buffer_id = anchor.buffer_id; let excerpt_anchors = iter::from_fn(|| { let anchor = anchors.peek()?; if anchor.excerpt_id == *excerpt_id { @@ -1328,7 +1329,7 @@ impl MultiBufferSnapshot { let position = D::from_text_summary(&cursor.start().text); if let Some(excerpt) = cursor.item() { - if excerpt.id == *excerpt_id { + if excerpt.id == *excerpt_id && excerpt.buffer_id == buffer_id { let excerpt_buffer_start = excerpt.range.start.summary::(&excerpt.buffer); summaries.extend( excerpt @@ -1379,6 +1380,7 @@ impl MultiBufferSnapshot { let text_anchor = excerpt.clip_anchor(excerpt.buffer.anchor_at(buffer_start + overshoot, bias)); Anchor { + buffer_id: excerpt.buffer_id, excerpt_id: excerpt.id.clone(), text_anchor, } @@ -1397,6 +1399,7 @@ impl MultiBufferSnapshot { let text_anchor = excerpt.clip_anchor(text_anchor); drop(cursor); return Anchor { + buffer_id: excerpt.buffer_id, excerpt_id, text_anchor, }; @@ -1595,10 +1598,12 @@ impl MultiBufferSnapshot { .flat_map(move |(replica_id, selections)| { selections.map(move |selection| { let mut start = Anchor { + buffer_id: excerpt.buffer_id, excerpt_id: excerpt.id.clone(), text_anchor: selection.start.clone(), }; let mut end = Anchor { + buffer_id: excerpt.buffer_id, excerpt_id: excerpt.id.clone(), text_anchor: selection.end.clone(), }; @@ -2349,6 +2354,54 @@ mod tests { assert_eq!(old_snapshot.anchor_after(10).to_offset(&new_snapshot), 14); } + #[gpui::test] + fn test_multibuffer_anchors_after_replacing_excerpts(cx: &mut MutableAppContext) { + let buffer_1 = cx.add_model(|cx| Buffer::new(0, "abcd", cx)); + let buffer_2 = cx.add_model(|cx| Buffer::new(0, "efghi", cx)); + + // Create an insertion id in buffer 1 that doesn't exist in buffer 2 + buffer_1.update(cx, |buffer, cx| { + buffer.edit([4..4], "123", cx); + }); + + let multibuffer = cx.add_model(|_| MultiBuffer::new(0)); + let excerpt_id = multibuffer.update(cx, |multibuffer, cx| { + multibuffer.push_excerpt( + ExcerptProperties { + buffer: &buffer_1, + range: 0..7, + }, + cx, + ) + }); + + // Create an anchor in the second insertion of buffer 1 + let anchor = multibuffer.read(cx).read(cx).anchor_before(7); + + multibuffer.update(cx, |multibuffer, cx| { + multibuffer.remove_excerpts([&excerpt_id], cx); + let new_excerpt_id = multibuffer.push_excerpt( + ExcerptProperties { + buffer: &buffer_2, + range: 0..5, + }, + cx, + ); + + // The same id is reused for an excerpt in a different buffer. + assert_eq!(new_excerpt_id, excerpt_id); + + // We don't attempt to resolve the text anchor from buffer 1 + // in buffer 2. + let snapshot = multibuffer.snapshot(cx); + assert_eq!(snapshot.summary_for_anchor::(&anchor), 0); + assert_eq!( + snapshot.summaries_for_anchors::(&[anchor]), + vec![0] + ); + }); + } + #[gpui::test(iterations = 100)] fn test_random_excerpts(cx: &mut MutableAppContext, mut rng: StdRng) { let operations = env::var("OPERATIONS") diff --git a/crates/editor/src/multi_buffer/anchor.rs b/crates/editor/src/multi_buffer/anchor.rs index 758a62526bf1be8825dc08083f668fc3308e45f4..2e1e1a924634f8b1b734b539a16805fbb888fd92 100644 --- a/crates/editor/src/multi_buffer/anchor.rs +++ b/crates/editor/src/multi_buffer/anchor.rs @@ -9,6 +9,7 @@ use text::{rope::TextDimension, Point}; #[derive(Clone, Eq, PartialEq, Debug, Hash)] pub struct Anchor { + pub(crate) buffer_id: usize, pub(crate) excerpt_id: ExcerptId, pub(crate) text_anchor: text::Anchor, } @@ -16,6 +17,7 @@ pub struct Anchor { impl Anchor { pub fn min() -> Self { Self { + buffer_id: 0, excerpt_id: ExcerptId::min(), text_anchor: text::Anchor::min(), } @@ -23,6 +25,7 @@ impl Anchor { pub fn max() -> Self { Self { + buffer_id: 0, excerpt_id: ExcerptId::max(), text_anchor: text::Anchor::max(), } @@ -54,6 +57,7 @@ impl Anchor { if self.text_anchor.bias != Bias::Left { if let Some(buffer_snapshot) = snapshot.buffer_snapshot_for_excerpt(&self.excerpt_id) { return Self { + buffer_id: self.buffer_id, excerpt_id: self.excerpt_id.clone(), text_anchor: self.text_anchor.bias_left(buffer_snapshot), }; @@ -66,6 +70,7 @@ impl Anchor { if self.text_anchor.bias != Bias::Right { if let Some(buffer_snapshot) = snapshot.buffer_snapshot_for_excerpt(&self.excerpt_id) { return Self { + buffer_id: self.buffer_id, excerpt_id: self.excerpt_id.clone(), text_anchor: self.text_anchor.bias_right(buffer_snapshot), }; diff --git a/crates/text/src/text.rs b/crates/text/src/text.rs index 9db7591f22a5dcdf4a7bbfaee6883a29767bc98e..402cbfecfac34382d28b8fd9e2b01a11d46203ea 100644 --- a/crates/text/src/text.rs +++ b/crates/text/src/text.rs @@ -1495,7 +1495,7 @@ impl BufferSnapshot { insertion_cursor.prev(&()); } let insertion = insertion_cursor.item().expect("invalid insertion"); - debug_assert_eq!(insertion.timestamp, anchor.timestamp, "invalid insertion"); + assert_eq!(insertion.timestamp, anchor.timestamp, "invalid insertion"); fragment_cursor.seek_forward(&Some(&insertion.fragment_id), Bias::Left, &None); let fragment = fragment_cursor.item().unwrap(); @@ -1537,7 +1537,7 @@ impl BufferSnapshot { insertion_cursor.prev(&()); } let insertion = insertion_cursor.item().expect("invalid insertion"); - debug_assert_eq!(insertion.timestamp, anchor.timestamp, "invalid insertion"); + assert_eq!(insertion.timestamp, anchor.timestamp, "invalid insertion"); let mut fragment_cursor = self.fragments.cursor::<(Option<&Locator>, usize)>(); fragment_cursor.seek(&Some(&insertion.fragment_id), Bias::Left, &None); From 5a53eeef63859ff3e2faa6fa6879c5a19d0713e6 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Wed, 5 Jan 2022 12:31:00 -0800 Subject: [PATCH 12/43] Don't scroll editors away from the top of their buffer when content changes Co-Authored-By: Nathan Sobo --- crates/editor/src/editor.rs | 41 +++++++++++++++++++++++-------------- 1 file changed, 26 insertions(+), 15 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index ddf3fbd37f626aeaab7307fe309959b30dd78b08..b2b76d2ffa5ba474ba23722c52188ecec900aa85 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -365,7 +365,7 @@ pub struct Editor { select_larger_syntax_node_stack: Vec]>>, active_diagnostics: Option, scroll_position: Vector2F, - scroll_top_anchor: Anchor, + scroll_top_anchor: Option, autoscroll_request: Option, build_settings: BuildSettings, focused: bool, @@ -383,7 +383,7 @@ pub struct EditorSnapshot { pub placeholder_text: Option>, is_focused: bool, scroll_position: Vector2F, - scroll_top_anchor: Anchor, + scroll_top_anchor: Option, } struct PendingSelection { @@ -495,7 +495,7 @@ impl Editor { active_diagnostics: None, build_settings, scroll_position: Vector2F::zero(), - scroll_top_anchor: Anchor::min(), + scroll_top_anchor: None, autoscroll_request: None, focused: false, show_local_cursors: false, @@ -565,15 +565,22 @@ impl Editor { pub fn set_scroll_position(&mut self, scroll_position: Vector2F, cx: &mut ViewContext) { let map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); - let scroll_top_buffer_offset = - DisplayPoint::new(scroll_position.y() as u32, 0).to_offset(&map, Bias::Right); - self.scroll_top_anchor = map - .buffer_snapshot - .anchor_at(scroll_top_buffer_offset, Bias::Right); - self.scroll_position = vec2f( - scroll_position.x(), - scroll_position.y() - self.scroll_top_anchor.to_display_point(&map).row() as f32, - ); + + if scroll_position.y() == 0. { + self.scroll_top_anchor = None; + self.scroll_position = scroll_position; + } else { + let scroll_top_buffer_offset = + DisplayPoint::new(scroll_position.y() as u32, 0).to_offset(&map, Bias::Right); + let anchor = map + .buffer_snapshot + .anchor_at(scroll_top_buffer_offset, Bias::Right); + self.scroll_position = vec2f( + scroll_position.x(), + scroll_position.y() - anchor.to_display_point(&map).row() as f32, + ); + self.scroll_top_anchor = Some(anchor); + } cx.notify(); } @@ -3631,10 +3638,14 @@ impl EditorSettings { fn compute_scroll_position( snapshot: &DisplaySnapshot, mut scroll_position: Vector2F, - scroll_top_anchor: &Anchor, + scroll_top_anchor: &Option, ) -> Vector2F { - let scroll_top = scroll_top_anchor.to_display_point(snapshot).row() as f32; - scroll_position.set_y(scroll_top + scroll_position.y()); + if let Some(anchor) = scroll_top_anchor { + let scroll_top = anchor.to_display_point(snapshot).row() as f32; + scroll_position.set_y(scroll_top + scroll_position.y()); + } else { + scroll_position.set_y(0.); + } scroll_position } From 7340e8305902eb5bce9efd28a5d60f8280bf1cd2 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Wed, 5 Jan 2022 21:12:49 -0800 Subject: [PATCH 13/43] WIP - MultiBuffer::refresh_anchors --- crates/editor/src/editor.rs | 13 +++ crates/editor/src/multi_buffer.rs | 173 +++++++++++++++++++++++++----- 2 files changed, 159 insertions(+), 27 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index b2b76d2ffa5ba474ba23722c52188ecec900aa85..84735c3aa03f325c1d2df5c9eaa87d5527c630e1 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -3258,6 +3258,19 @@ impl Editor { ); } + pub fn refresh_selections(&mut self, cx: &mut ViewContext) { + let anchors = self.buffer.update(cx, |buffer, cx| { + let snapshot = buffer.read(cx); + snapshot.refresh_anchors( + self.selections + .iter() + .flat_map(|selection| [&selection.start, &selection.end]), + ) + }); + + todo!(); + } + fn set_selections(&mut self, selections: Arc<[Selection]>, cx: &mut ViewContext) { self.selections = selections; self.buffer.update(cx, |buffer, cx| { diff --git a/crates/editor/src/multi_buffer.rs b/crates/editor/src/multi_buffer.rs index 236aa1d0b79830966c50c049686ff193fc8fe516..7f56239f83fadefde5f3fde9c94dbd419a2b0875 100644 --- a/crates/editor/src/multi_buffer.rs +++ b/crates/editor/src/multi_buffer.rs @@ -1302,6 +1302,61 @@ impl MultiBufferSnapshot { position } + pub fn refresh_anchors<'a, I>(&'a self, anchors: I) -> Vec + where + I: 'a + IntoIterator, + { + let mut anchors = anchors.into_iter().peekable(); + let mut cursor = self.excerpts.cursor::>(); + let mut result = Vec::new(); + while let Some(anchor) = anchors.peek() { + let old_excerpt_id = &anchor.excerpt_id; + + // Find the location where this anchor's excerpt should be, + cursor.seek_forward(&Some(old_excerpt_id), Bias::Left, &()); + if cursor.item().is_none() { + cursor.next(&()); + } + + let next_excerpt = cursor.item(); + let prev_excerpt = cursor.prev_item(); + + // Process all of the anchors for this excerpt. + while let Some(&anchor) = anchors.peek() { + if anchor.excerpt_id != *old_excerpt_id { + break; + } + let mut anchor = anchors.next().unwrap().clone(); + + // If the old excerpt no longer exists at this location, then attempt to + // find an equivalent position for this anchor in an adjacent excerpt. + if next_excerpt.map_or(true, |e| e.id != *old_excerpt_id) { + for excerpt in [next_excerpt, prev_excerpt].iter().filter_map(|e| *e) { + if excerpt.buffer_id == anchor.buffer_id + && excerpt + .range + .start + .cmp(&anchor.text_anchor, &excerpt.buffer) + .unwrap() + .is_le() + && excerpt + .range + .end + .cmp(&anchor.text_anchor, &excerpt.buffer) + .unwrap() + .is_ge() + { + anchor.excerpt_id = excerpt.id.clone(); + } + } + } + + result.push(anchor); + } + } + result + } + pub fn summaries_for_anchors<'a, D, I>(&'a self, anchors: I) -> Vec where D: TextDimension + Ord + Sub, @@ -2375,17 +2430,17 @@ mod tests { } #[gpui::test] - fn test_multibuffer_anchors_after_replacing_excerpts(cx: &mut MutableAppContext) { + fn test_multibuffer_resolving_anchors_after_replacing_their_excerpts( + cx: &mut MutableAppContext, + ) { let buffer_1 = cx.add_model(|cx| Buffer::new(0, "abcd", cx)); - let buffer_2 = cx.add_model(|cx| Buffer::new(0, "efghi", cx)); - - // Create an insertion id in buffer 1 that doesn't exist in buffer 2 - buffer_1.update(cx, |buffer, cx| { - buffer.edit([4..4], "123", cx); - }); - + let buffer_2 = cx.add_model(|cx| Buffer::new(0, "ABCDEFGHIJKLMNOP", cx)); let multibuffer = cx.add_model(|_| MultiBuffer::new(0)); - let excerpt_id = multibuffer.update(cx, |multibuffer, cx| { + + // Create an insertion id in buffer 1 that doesn't exist in buffer 2. + // Add an excerpt from buffer 1 that spans this new insertion. + buffer_1.update(cx, |buffer, cx| buffer.edit([4..4], "123", cx)); + let excerpt_id_1 = multibuffer.update(cx, |multibuffer, cx| { multibuffer.push_excerpt( ExcerptProperties { buffer: &buffer_1, @@ -2395,31 +2450,95 @@ mod tests { ) }); - // Create an anchor in the second insertion of buffer 1 - let anchor = multibuffer.read(cx).read(cx).anchor_before(7); + let snapshot_1 = multibuffer.read(cx).snapshot(cx); + assert_eq!(snapshot_1.text(), "abcd123"); + + // Replace the buffer 1 excerpt with new excerpts from buffer 2. + let (excerpt_id_2, excerpt_id_3, excerpt_id_4) = + multibuffer.update(cx, |multibuffer, cx| { + multibuffer.remove_excerpts([&excerpt_id_1], cx); + ( + multibuffer.push_excerpt( + ExcerptProperties { + buffer: &buffer_2, + range: 0..4, + }, + cx, + ), + multibuffer.push_excerpt( + ExcerptProperties { + buffer: &buffer_2, + range: 6..10, + }, + cx, + ), + multibuffer.push_excerpt( + ExcerptProperties { + buffer: &buffer_2, + range: 12..16, + }, + cx, + ), + ) + }); + let snapshot_2 = multibuffer.read(cx).snapshot(cx); + assert_eq!(snapshot_2.text(), "ABCD\nGHIJ\nMNOP"); - multibuffer.update(cx, |multibuffer, cx| { - multibuffer.remove_excerpts([&excerpt_id], cx); - let new_excerpt_id = multibuffer.push_excerpt( + // And excerpt id has been reused. + assert_eq!(excerpt_id_2, excerpt_id_1); + + // Resolve some anchors from the previous snapshot in the new snapshot. + // Although there is still an excerpt with the same id, it is for + // a different buffer, so we don't attempt to resolve the old text + // anchor in the new buffer. + assert_eq!( + snapshot_2.summary_for_anchor::(&snapshot_1.anchor_before(2)), + 0 + ); + assert_eq!( + snapshot_2.summaries_for_anchors::(&[ + snapshot_1.anchor_before(2), + snapshot_1.anchor_after(3) + ]), + vec![0, 0] + ); + + // Replace the middle excerpt with a smaller excerpt in buffer 2, + // that intersects the old excerpt. + let excerpt_id_5 = multibuffer.update(cx, |multibuffer, cx| { + multibuffer.remove_excerpts([&excerpt_id_3], cx); + multibuffer.insert_excerpt_after( + &excerpt_id_3, ExcerptProperties { buffer: &buffer_2, - range: 0..5, + range: 5..8, }, cx, - ); + ) + }); - // The same id is reused for an excerpt in a different buffer. - assert_eq!(new_excerpt_id, excerpt_id); + let snapshot_3 = multibuffer.read(cx).snapshot(cx); + assert_eq!(snapshot_3.text(), "ABCD\nFGH\nMNOP"); + assert_ne!(excerpt_id_5, excerpt_id_3); + + // Resolve some anchors from the previous snapshot in the new snapshot. + // The anchor in the middle excerpt snaps to the beginning of the + // excerpt, since it is not + let anchors = [ + snapshot_2.anchor_after(2), + snapshot_2.anchor_after(6), + snapshot_2.anchor_after(14), + ]; + assert_eq!( + snapshot_3.summaries_for_anchors::(&anchors), + &[2, 9, 13] + ); - // We don't attempt to resolve the text anchor from buffer 1 - // in buffer 2. - let snapshot = multibuffer.snapshot(cx); - assert_eq!(snapshot.summary_for_anchor::(&anchor), 0); - assert_eq!( - snapshot.summaries_for_anchors::(&[anchor]), - vec![0] - ); - }); + let new_anchors = snapshot_3.refresh_anchors(&anchors); + assert_eq!( + snapshot_3.summaries_for_anchors::(&new_anchors), + &[2, 7, 13] + ); } #[gpui::test(iterations = 100)] From f37f8393302c9f67a0272ba475eccbd784681e65 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Thu, 6 Jan 2022 15:32:37 +0100 Subject: [PATCH 14/43] Wire up `refresh_anchors` in `Editor::refresh_selections` and call it --- crates/diagnostics/src/diagnostics.rs | 2 ++ crates/editor/src/editor.rs | 13 +++++++++++-- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/crates/diagnostics/src/diagnostics.rs b/crates/diagnostics/src/diagnostics.rs index 8820c1097d9c2c48077974348ace443ada4b9f95..28f82d7fe3c1f4149964b7e2ed1cbff6076a2ae2 100644 --- a/crates/diagnostics/src/diagnostics.rs +++ b/crates/diagnostics/src/diagnostics.rs @@ -364,6 +364,8 @@ impl ProjectDiagnosticsEditor { for group_state in &mut groups_to_add { group_state.blocks = block_ids.by_ref().take(group_state.block_count).collect(); } + + editor.refresh_selections(cx); }); for ix in group_ixs_to_remove.into_iter().rev() { diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 84735c3aa03f325c1d2df5c9eaa87d5527c630e1..a75e13972482aeaaf97686db236b1911c391fb1c 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -3267,8 +3267,17 @@ impl Editor { .flat_map(|selection| [&selection.start, &selection.end]), ) }); - - todo!(); + self.selections = self + .selections + .iter() + .cloned() + .zip(anchors.chunks(2)) + .map(|(mut selection, anchors)| { + selection.start = anchors[0].clone(); + selection.end = anchors[1].clone(); + selection + }) + .collect(); } fn set_selections(&mut self, selections: Arc<[Selection]>, cx: &mut ViewContext) { From d0f7e5f075b85b25b09b76868da4d144385734b5 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Thu, 6 Jan 2022 15:33:02 +0100 Subject: [PATCH 15/43] Maintain excerpt ordering correctly when some errors don't change --- crates/diagnostics/src/diagnostics.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/crates/diagnostics/src/diagnostics.rs b/crates/diagnostics/src/diagnostics.rs index 28f82d7fe3c1f4149964b7e2ed1cbff6076a2ae2..5c7f4f01bd5cbde9f5f0d3b13d37a3ff768b8dd7 100644 --- a/crates/diagnostics/src/diagnostics.rs +++ b/crates/diagnostics/src/diagnostics.rs @@ -219,6 +219,7 @@ impl ProjectDiagnosticsEditor { loop { let mut to_insert = None; let mut to_invalidate = None; + let mut to_keep = None; match (old_groups.peek(), new_groups.peek()) { (None, None) => break, (None, Some(_)) => to_insert = new_groups.next(), @@ -229,7 +230,7 @@ impl ProjectDiagnosticsEditor { match compare_diagnostics(old_primary, new_primary, &snapshot) { Ordering::Less => to_invalidate = old_groups.next(), Ordering::Equal => { - old_groups.next(); + to_keep = old_groups.next(); new_groups.next(); } Ordering::Greater => to_insert = new_groups.next(), @@ -337,6 +338,8 @@ impl ProjectDiagnosticsEditor { excerpts.remove_excerpts(group_state.excerpts.iter(), excerpts_cx); group_ixs_to_remove.push(group_ix); blocks_to_remove.extend(group_state.blocks.keys().copied()); + } else if let Some((_, group)) = to_keep { + prev_excerpt_id = group.excerpts.last().unwrap().clone(); } } From 1875a0e349b955e802d46a9c444532d34da01b4f Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Thu, 6 Jan 2022 16:17:53 +0100 Subject: [PATCH 16/43] Polish rendering of inline errors - Don't soft-wrap - Render multiple lines Co-Authored-By: Nathan Sobo --- crates/diagnostics/src/diagnostics.rs | 2 +- crates/editor/src/editor.rs | 7 ++++++- crates/gpui/src/elements/text.rs | 22 ++++++++++++++++++---- 3 files changed, 25 insertions(+), 6 deletions(-) diff --git a/crates/diagnostics/src/diagnostics.rs b/crates/diagnostics/src/diagnostics.rs index 5c7f4f01bd5cbde9f5f0d3b13d37a3ff768b8dd7..3b495b414cc6f1ef7add654cee369e9780eccaf9 100644 --- a/crates/diagnostics/src/diagnostics.rs +++ b/crates/diagnostics/src/diagnostics.rs @@ -285,7 +285,7 @@ impl ProjectDiagnosticsEditor { diagnostic_blocks.push(DiagnosticBlock::Header(primary.clone())); blocks_to_add.push(BlockProperties { position: header_position, - height: 2, + height: primary.message.matches('\n').count() as u8 + 2, render: diagnostic_header_renderer( buffer.clone(), primary.clone(), diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index a75e13972482aeaaf97686db236b1911c391fb1c..c8d95da3eb425322ff86e5f030829031f9cef97f 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -3800,6 +3800,7 @@ pub fn diagnostic_block_renderer( let mut text_style = settings.style.text.clone(); text_style.color = diagnostic_style(diagnostic.severity, is_valid, &settings.style).text; Text::new(diagnostic.message.clone(), text_style) + .with_soft_wrap(false) .contained() .with_margin_left(cx.anchor_x) .boxed() @@ -3823,7 +3824,11 @@ pub fn diagnostic_header_renderer( }; Flex::column() - .with_child(Label::new(diagnostic.message.clone(), text_style).boxed()) + .with_child( + Text::new(diagnostic.message.clone(), text_style) + .with_soft_wrap(false) + .boxed(), + ) .with_child(Label::new(file_path, settings.style.text.clone()).boxed()) .boxed() }) diff --git a/crates/gpui/src/elements/text.rs b/crates/gpui/src/elements/text.rs index 623af72af6444892b30c75448eea17e379b14a02..2f20b77d566a8b14ab303d787bfffa89a1e2d007 100644 --- a/crates/gpui/src/elements/text.rs +++ b/crates/gpui/src/elements/text.rs @@ -14,6 +14,7 @@ use serde_json::json; pub struct Text { text: String, style: TextStyle, + soft_wrap: bool, } pub struct LayoutState { @@ -23,13 +24,22 @@ pub struct LayoutState { impl Text { pub fn new(text: String, style: TextStyle) -> Self { - Self { text, style } + Self { + text, + style, + soft_wrap: true, + } } pub fn with_default_color(mut self, color: Color) -> Self { self.style.color = color; self } + + pub fn with_soft_wrap(mut self, soft_wrap: bool) -> Self { + self.soft_wrap = soft_wrap; + self + } } impl Element for Text { @@ -54,9 +64,13 @@ impl Element for Text { self.style.font_size, &[(line.len(), self.style.to_run())], ); - let wrap_boundaries = wrapper - .wrap_shaped_line(line, &shaped_line, constraint.max.x()) - .collect::>(); + let wrap_boundaries = if self.soft_wrap { + wrapper + .wrap_shaped_line(line, &shaped_line, constraint.max.x()) + .collect::>() + } else { + Vec::new() + }; max_line_width = max_line_width.max(shaped_line.width()); line_count += wrap_boundaries.len() + 1; From 571d0386e25b77bd207ef078855f9eca63ab715d Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Thu, 6 Jan 2022 08:35:31 -0700 Subject: [PATCH 17/43] Re-focus diagnostics editor when transitioning from an empty to a populated state Co-Authored-By: Antonio Scandurra --- crates/diagnostics/src/diagnostics.rs | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/crates/diagnostics/src/diagnostics.rs b/crates/diagnostics/src/diagnostics.rs index 3b495b414cc6f1ef7add654cee369e9780eccaf9..2aa062af91d41023ac25a2c29857bd1c2f06d219 100644 --- a/crates/diagnostics/src/diagnostics.rs +++ b/crates/diagnostics/src/diagnostics.rs @@ -389,8 +389,14 @@ impl ProjectDiagnosticsEditor { self.path_states.remove(path_ix); } - if self.editor.is_focused(cx) && self.path_states.is_empty() { - cx.focus_self(); + if self.path_states.is_empty() { + if self.editor.is_focused(cx) { + cx.focus_self(); + } + } else { + if cx.handle().is_focused(cx) { + cx.focus(&self.editor); + } } cx.notify(); } From d7a78e14ac6d40ea7f51c2a98a8a48967d7e2c82 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Thu, 6 Jan 2022 09:32:08 -0700 Subject: [PATCH 18/43] Allow disk-based diagnostic progress begin/end events to interleave When multiple saves occur, we can have multiple start events followed by multiple end events. We don't want to update our project diagnostics view until all pending progress is finished. Co-Authored-By: Antonio Scandurra --- crates/editor/src/display_map.rs | 9 ++-- crates/editor/src/display_map/wrap_map.rs | 11 ++--- crates/editor/src/test.rs | 27 ------------ crates/gpui/src/app.rs | 2 +- crates/gpui/src/test.rs | 52 ++++++++++++++++++++++- crates/language/src/language.rs | 1 + crates/lsp/src/lsp.rs | 16 +++++++ crates/project/src/worktree.rs | 36 +++++++++++++++- 8 files changed, 114 insertions(+), 40 deletions(-) diff --git a/crates/editor/src/display_map.rs b/crates/editor/src/display_map.rs index cf436971a53102047b2c1b43b1b7d8514051e255..342ef90b3845022db2e7fdfdee91b5d807d485c6 100644 --- a/crates/editor/src/display_map.rs +++ b/crates/editor/src/display_map.rs @@ -446,10 +446,11 @@ impl ToDisplayPoint for Anchor { #[cfg(test)] mod tests { use super::*; - use crate::{movement, test::*}; - use gpui::{color::Color, elements::*, MutableAppContext}; + use crate::movement; + use gpui::{color::Color, elements::*, test::observe, MutableAppContext}; use language::{Buffer, Language, LanguageConfig, RandomCharIter, SelectionGoal}; use rand::{prelude::*, Rng}; + use smol::stream::StreamExt; use std::{env, sync::Arc}; use theme::SyntaxTheme; use util::test::sample_text; @@ -493,7 +494,7 @@ mod tests { let map = cx.add_model(|cx| { DisplayMap::new(buffer.clone(), tab_size, font_id, font_size, wrap_width, cx) }); - let (_observer, notifications) = Observer::new(&map, &mut cx); + let mut notifications = observe(&map, &mut cx); let mut fold_count = 0; let mut blocks = Vec::new(); @@ -589,7 +590,7 @@ mod tests { } if map.read_with(&cx, |map, cx| map.is_rewrapping(cx)) { - notifications.recv().await.unwrap(); + notifications.next().await.unwrap(); } let snapshot = map.update(&mut cx, |map, cx| map.snapshot(cx)); diff --git a/crates/editor/src/display_map/wrap_map.rs b/crates/editor/src/display_map/wrap_map.rs index b7e96c490634d92572a8cf9530d6617b1a2d10bf..8b02dbbd15c72297ee28ba02632d097dde8ec8e8 100644 --- a/crates/editor/src/display_map/wrap_map.rs +++ b/crates/editor/src/display_map/wrap_map.rs @@ -1014,11 +1014,12 @@ mod tests { use super::*; use crate::{ display_map::{fold_map::FoldMap, tab_map::TabMap}, - test::Observer, MultiBuffer, }; + use gpui::test::observe; use language::RandomCharIter; use rand::prelude::*; + use smol::stream::StreamExt; use std::{cmp, env}; use text::Rope; @@ -1072,10 +1073,10 @@ mod tests { let (wrap_map, _) = cx.update(|cx| WrapMap::new(tabs_snapshot.clone(), font_id, font_size, wrap_width, cx)); - let (_observer, notifications) = Observer::new(&wrap_map, &mut cx); + let mut notifications = observe(&wrap_map, &mut cx); if wrap_map.read_with(&cx, |map, _| map.is_rewrapping()) { - notifications.recv().await.unwrap(); + notifications.next().await.unwrap(); } let (initial_snapshot, _) = wrap_map.update(&mut cx, |map, cx| { @@ -1148,7 +1149,7 @@ mod tests { if wrap_map.read_with(&cx, |map, _| map.is_rewrapping()) && rng.gen_bool(0.4) { log::info!("Waiting for wrapping to finish"); while wrap_map.read_with(&cx, |map, _| map.is_rewrapping()) { - notifications.recv().await.unwrap(); + notifications.next().await.unwrap(); } wrap_map.read_with(&cx, |map, _| assert!(map.pending_edits.is_empty())); } @@ -1236,7 +1237,7 @@ mod tests { if wrap_map.read_with(&cx, |map, _| map.is_rewrapping()) { log::info!("Waiting for wrapping to finish"); while wrap_map.read_with(&cx, |map, _| map.is_rewrapping()) { - notifications.recv().await.unwrap(); + notifications.next().await.unwrap(); } } wrap_map.read_with(&cx, |map, _| assert!(map.pending_edits.is_empty())); diff --git a/crates/editor/src/test.rs b/crates/editor/src/test.rs index 3fb538dfbd55e518aeb3358ed047a879bfd8df1f..f4622d1f6e1a7d6bc34d724a4b5f704a144a11ac 100644 --- a/crates/editor/src/test.rs +++ b/crates/editor/src/test.rs @@ -1,33 +1,6 @@ -use gpui::{Entity, ModelHandle}; -use smol::channel; -use std::marker::PhantomData; - #[cfg(test)] #[ctor::ctor] fn init_logger() { // std::env::set_var("RUST_LOG", "info"); env_logger::init(); } - -pub struct Observer(PhantomData); - -impl Entity for Observer { - type Event = (); -} - -impl Observer { - pub fn new( - handle: &ModelHandle, - cx: &mut gpui::TestAppContext, - ) -> (ModelHandle, channel::Receiver<()>) { - let (notify_tx, notify_rx) = channel::unbounded(); - let observer = cx.add_model(|cx| { - cx.observe(handle, move |_, _, _| { - let _ = notify_tx.try_send(()); - }) - .detach(); - Observer(PhantomData) - }); - (observer, notify_rx) - } -} diff --git a/crates/gpui/src/app.rs b/crates/gpui/src/app.rs index 2e7e08d0a5d13c706ca3e2760cab353a794f4d48..e42e8894966939202508575c9031a60e6f930c2b 100644 --- a/crates/gpui/src/app.rs +++ b/crates/gpui/src/app.rs @@ -992,7 +992,7 @@ impl MutableAppContext { }) } - fn observe(&mut self, handle: &H, mut callback: F) -> Subscription + pub fn observe(&mut self, handle: &H, mut callback: F) -> Subscription where E: Entity, E::Event: 'static, diff --git a/crates/gpui/src/test.rs b/crates/gpui/src/test.rs index 59d49cac8df8f45adcbe6bdd3c2496e474892ace..ef95ea435ac34ea31d53d37d61e169094b3efde5 100644 --- a/crates/gpui/src/test.rs +++ b/crates/gpui/src/test.rs @@ -7,7 +7,13 @@ use std::{ }, }; -use crate::{executor, platform, FontCache, MutableAppContext, Platform, TestAppContext}; +use futures::StreamExt; +use smol::channel; + +use crate::{ + executor, platform, Entity, FontCache, Handle, MutableAppContext, Platform, Subscription, + TestAppContext, +}; #[cfg(test)] #[ctor::ctor] @@ -87,3 +93,47 @@ pub fn run_test( } } } + +pub struct Observation { + rx: channel::Receiver, + _subscription: Subscription, +} + +impl futures::Stream for Observation { + type Item = T; + + fn poll_next( + mut self: std::pin::Pin<&mut Self>, + cx: &mut std::task::Context<'_>, + ) -> std::task::Poll> { + self.rx.poll_next_unpin(cx) + } +} + +pub fn observe(entity: &impl Handle, cx: &mut TestAppContext) -> Observation<()> { + let (tx, rx) = smol::channel::unbounded(); + let _subscription = cx.update(|cx| { + cx.observe(entity, move |_, _| { + let _ = smol::block_on(tx.send(())); + }) + }); + + Observation { rx, _subscription } +} + +pub fn subscribe( + entity: &impl Handle, + cx: &mut TestAppContext, +) -> Observation +where + T::Event: Clone, +{ + let (tx, rx) = smol::channel::unbounded(); + let _subscription = cx.update(|cx| { + cx.subscribe(entity, move |_, event, _| { + let _ = smol::block_on(tx.send(event.clone())); + }) + }); + + Observation { rx, _subscription } +} diff --git a/crates/language/src/language.rs b/crates/language/src/language.rs index bd5f91b792176ae70d8abfed7eec84bb0ea1f429..9f7f9f75ac4d6b190210b940b2cec422308d6685 100644 --- a/crates/language/src/language.rs +++ b/crates/language/src/language.rs @@ -237,6 +237,7 @@ impl LanguageServerConfig { ( Self { fake_server: Some((server, started)), + disk_based_diagnostics_progress_token: Some("fakeServer/check".to_string()), ..Default::default() }, fake, diff --git a/crates/lsp/src/lsp.rs b/crates/lsp/src/lsp.rs index d0ce93b97365e4cecbbe3adc1bc3247678b44b37..c3d264e8a99f227156378c07e25a4dca726204fa 100644 --- a/crates/lsp/src/lsp.rs +++ b/crates/lsp/src/lsp.rs @@ -514,6 +514,22 @@ impl FakeLanguageServer { notification.params } + pub async fn start_progress(&mut self, token: impl Into) { + self.notify::(ProgressParams { + token: NumberOrString::String(token.into()), + value: ProgressParamsValue::WorkDone(WorkDoneProgress::Begin(Default::default())), + }) + .await; + } + + pub async fn end_progress(&mut self, token: impl Into) { + self.notify::(ProgressParams { + token: NumberOrString::String(token.into()), + value: ProgressParamsValue::WorkDone(WorkDoneProgress::End(Default::default())), + }) + .await; + } + async fn send(&mut self, message: Vec) { self.stdout .write_all(CONTENT_LEN_HEADER.as_bytes()) diff --git a/crates/project/src/worktree.rs b/crates/project/src/worktree.rs index f7538f629463be2a98e6ac62d857cae69f0521e4..3e76151fdfa115bdf3d1046c8a639e016bb8404f 100644 --- a/crates/project/src/worktree.rs +++ b/crates/project/src/worktree.rs @@ -67,7 +67,7 @@ pub enum Worktree { Remote(RemoteWorktree), } -#[derive(Debug)] +#[derive(Clone, Debug, Eq, PartialEq)] pub enum Event { DiskBasedDiagnosticsUpdated, DiagnosticsUpdated(Arc), @@ -1120,6 +1120,7 @@ impl LocalWorktree { }) .detach(); + let mut pending_disk_based_diagnostics: i32 = 0; language_server .on_notification::(move |params| { let token = match params.token { @@ -1130,8 +1131,15 @@ impl LocalWorktree { if token == disk_based_diagnostics_progress_token { match params.value { lsp::ProgressParamsValue::WorkDone(progress) => match progress { + lsp::WorkDoneProgress::Begin(_) => { + pending_disk_based_diagnostics += 1; + } lsp::WorkDoneProgress::End(_) => { - smol::block_on(disk_based_diagnostics_done_tx.send(())).ok(); + pending_disk_based_diagnostics -= 1; + if pending_disk_based_diagnostics == 0 { + smol::block_on(disk_based_diagnostics_done_tx.send(())) + .ok(); + } } _ => {} }, @@ -3107,6 +3115,7 @@ mod tests { use anyhow::Result; use client::test::{FakeHttpClient, FakeServer}; use fs::RealFs; + use gpui::test::subscribe; use language::{tree_sitter_rust, DiagnosticEntry, LanguageServerConfig}; use language::{Diagnostic, LanguageConfig}; use lsp::Url; @@ -3756,6 +3765,10 @@ mod tests { async fn test_language_server_diagnostics(mut cx: gpui::TestAppContext) { let (language_server_config, mut fake_server) = LanguageServerConfig::fake(cx.background()).await; + let progress_token = language_server_config + .disk_based_diagnostics_progress_token + .clone() + .unwrap(); let mut languages = LanguageRegistry::new(); languages.add(Arc::new(Language::new( LanguageConfig { @@ -3795,6 +3808,13 @@ mod tests { .await .unwrap(); + let mut events = subscribe(&tree, &mut cx); + + fake_server.start_progress(&progress_token).await; + fake_server.start_progress(&progress_token).await; + fake_server.end_progress(&progress_token).await; + fake_server.start_progress(&progress_token).await; + fake_server .notify::(lsp::PublishDiagnosticsParams { uri: Url::from_file_path(dir.path().join("a.rs")).unwrap(), @@ -3808,6 +3828,18 @@ mod tests { }) .await; + let event = events.next().await.unwrap(); + assert_eq!( + event, + Event::DiagnosticsUpdated(Arc::from(Path::new("a.rs"))) + ); + + fake_server.end_progress(&progress_token).await; + fake_server.end_progress(&progress_token).await; + + let event = events.next().await.unwrap(); + assert_eq!(event, Event::DiskBasedDiagnosticsUpdated); + let buffer = tree .update(&mut cx, |tree, cx| tree.open_buffer("a.rs", cx)) .await From 2dbee1d9140396d3fa12b788f91491043f1c6856 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Thu, 6 Jan 2022 12:11:06 -0700 Subject: [PATCH 19/43] Send diagnostic summaries to guests when they join the project Co-Authored-By: Max Brunsfeld --- crates/editor/src/multi_buffer.rs | 53 ++++++++++--------- crates/project/src/project.rs | 10 ++++ crates/project/src/worktree.rs | 82 ++++++++++++++++++----------- crates/rpc/proto/zed.proto | 5 ++ crates/server/src/rpc.rs | 86 ++++++++++++++++++++++++++++--- crates/server/src/rpc/store.rs | 37 +++++++++++-- crates/sum_tree/src/tree_map.rs | 10 ++++ 7 files changed, 217 insertions(+), 66 deletions(-) diff --git a/crates/editor/src/multi_buffer.rs b/crates/editor/src/multi_buffer.rs index 7f56239f83fadefde5f3fde9c94dbd419a2b0875..f399a53412fa3db6e81b88002739199d7ac8b392 100644 --- a/crates/editor/src/multi_buffer.rs +++ b/crates/editor/src/multi_buffer.rs @@ -2454,33 +2454,32 @@ mod tests { assert_eq!(snapshot_1.text(), "abcd123"); // Replace the buffer 1 excerpt with new excerpts from buffer 2. - let (excerpt_id_2, excerpt_id_3, excerpt_id_4) = - multibuffer.update(cx, |multibuffer, cx| { - multibuffer.remove_excerpts([&excerpt_id_1], cx); - ( - multibuffer.push_excerpt( - ExcerptProperties { - buffer: &buffer_2, - range: 0..4, - }, - cx, - ), - multibuffer.push_excerpt( - ExcerptProperties { - buffer: &buffer_2, - range: 6..10, - }, - cx, - ), - multibuffer.push_excerpt( - ExcerptProperties { - buffer: &buffer_2, - range: 12..16, - }, - cx, - ), - ) - }); + let (excerpt_id_2, excerpt_id_3, _) = multibuffer.update(cx, |multibuffer, cx| { + multibuffer.remove_excerpts([&excerpt_id_1], cx); + ( + multibuffer.push_excerpt( + ExcerptProperties { + buffer: &buffer_2, + range: 0..4, + }, + cx, + ), + multibuffer.push_excerpt( + ExcerptProperties { + buffer: &buffer_2, + range: 6..10, + }, + cx, + ), + multibuffer.push_excerpt( + ExcerptProperties { + buffer: &buffer_2, + range: 12..16, + }, + cx, + ), + ) + }); let snapshot_2 = multibuffer.read(cx).snapshot(cx); assert_eq!(snapshot_2.text(), "ABCD\nGHIJ\nMNOP"); diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index a0ed7b811d24753cdb67aa7e5aa6fc961ea38ee6..325ac682ebfabcef2c291d3ac31862fceb798347 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -101,6 +101,16 @@ impl DiagnosticSummary { this } + + pub fn to_proto(&self, path: Arc) -> proto::DiagnosticSummary { + proto::DiagnosticSummary { + path: path.to_string_lossy().to_string(), + error_count: self.error_count as u32, + warning_count: self.warning_count as u32, + info_count: self.info_count as u32, + hint_count: self.hint_count as u32, + } + } } #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] diff --git a/crates/project/src/worktree.rs b/crates/project/src/worktree.rs index 3e76151fdfa115bdf3d1046c8a639e016bb8404f..3533714c04bb24170ba52047c06d7bb2c5e13fe5 100644 --- a/crates/project/src/worktree.rs +++ b/crates/project/src/worktree.rs @@ -7,8 +7,7 @@ use ::ignore::gitignore::{Gitignore, GitignoreBuilder}; use anyhow::{anyhow, Context, Result}; use client::{proto, Client, PeerId, TypedEnvelope, UserStore}; use clock::ReplicaId; -use collections::{hash_map, HashMap}; -use collections::{BTreeMap, HashSet}; +use collections::{hash_map, HashMap, HashSet}; use futures::{Stream, StreamExt}; use fuzzy::CharBag; use gpui::{ @@ -44,7 +43,7 @@ use std::{ }, time::{Duration, SystemTime}, }; -use sum_tree::Bias; +use sum_tree::{Bias, TreeMap}; use sum_tree::{Edit, SeekTarget, SumTree}; use util::{post_inc, ResultExt, TryFutureExt}; @@ -142,7 +141,7 @@ impl Worktree { .map(|c| c.to_ascii_lowercase()) .collect(); let root_name = worktree.root_name.clone(); - let (entries_by_path, entries_by_id) = cx + let (entries_by_path, entries_by_id, diagnostic_summaries) = cx .background() .spawn(async move { let mut entries_by_path_edits = Vec::new(); @@ -166,7 +165,22 @@ impl Worktree { let mut entries_by_id = SumTree::new(); entries_by_path.edit(entries_by_path_edits, &()); entries_by_id.edit(entries_by_id_edits, &()); - (entries_by_path, entries_by_id) + + let diagnostic_summaries = TreeMap::from_ordered_entries( + worktree.diagnostic_summaries.into_iter().map(|summary| { + ( + PathKey(PathBuf::from(summary.path).into()), + DiagnosticSummary { + error_count: summary.error_count as usize, + warning_count: summary.warning_count as usize, + info_count: summary.info_count as usize, + hint_count: summary.hint_count as usize, + }, + ) + }), + ); + + (entries_by_path, entries_by_id, diagnostic_summaries) }) .await; @@ -183,6 +197,7 @@ impl Worktree { entries_by_id, removed_entry_ids: Default::default(), next_entry_id: Default::default(), + diagnostic_summaries, }; let (updates_tx, mut updates_rx) = postage::mpsc::channel(64); @@ -223,7 +238,6 @@ impl Worktree { client: client.clone(), loading_buffers: Default::default(), open_buffers: Default::default(), - diagnostic_summaries: Default::default(), queued_operations: Default::default(), languages, user_store, @@ -351,7 +365,7 @@ impl Worktree { Worktree::Remote(worktree) => &worktree.diagnostic_summaries, } .iter() - .map(|(path, summary)| (path.clone(), summary.clone())) + .map(|(path, summary)| (path.0.clone(), summary.clone())) } pub fn loading_buffers<'a>(&'a mut self) -> &'a mut LoadingBuffers { @@ -778,8 +792,9 @@ impl Worktree { let this = self.as_local_mut().unwrap(); let summary = DiagnosticSummary::new(&diagnostics); - this.diagnostic_summaries - .insert(worktree_path.clone(), summary.clone()); + this.snapshot + .diagnostic_summaries + .insert(PathKey(worktree_path.clone()), summary.clone()); this.diagnostics.insert(worktree_path.clone(), diagnostics); cx.emit(Event::DiagnosticsUpdated(worktree_path.clone())); @@ -796,11 +811,13 @@ impl Worktree { .send(proto::UpdateDiagnosticSummary { project_id, worktree_id, - path, - error_count: summary.error_count as u32, - warning_count: summary.warning_count as u32, - info_count: summary.info_count as u32, - hint_count: summary.hint_count as u32, + summary: Some(proto::DiagnosticSummary { + path, + error_count: summary.error_count as u32, + warning_count: summary.warning_count as u32, + info_count: summary.info_count as u32, + hint_count: summary.hint_count as u32, + }), }) .await .log_err() @@ -890,6 +907,7 @@ pub struct Snapshot { entries_by_id: SumTree, removed_entry_ids: HashMap, next_entry_id: Arc, + diagnostic_summaries: TreeMap, } pub struct LocalWorktree { @@ -904,7 +922,6 @@ pub struct LocalWorktree { open_buffers: HashMap>, shared_buffers: HashMap>>, diagnostics: HashMap, Vec>>, - diagnostic_summaries: BTreeMap, DiagnosticSummary>, queued_operations: Vec<(u64, Operation)>, language_registry: Arc, client: Arc, @@ -928,7 +945,6 @@ pub struct RemoteWorktree { replica_id: ReplicaId, loading_buffers: LoadingBuffers, open_buffers: HashMap, - diagnostic_summaries: BTreeMap, DiagnosticSummary>, languages: Arc, user_store: ModelHandle, queued_operations: Vec<(u64, Operation)>, @@ -986,6 +1002,7 @@ impl LocalWorktree { entries_by_id: Default::default(), removed_entry_ids: Default::default(), next_entry_id: Arc::new(next_entry_id), + diagnostic_summaries: Default::default(), }; if let Some(metadata) = metadata { snapshot.insert_entry( @@ -1011,7 +1028,6 @@ impl LocalWorktree { open_buffers: Default::default(), shared_buffers: Default::default(), diagnostics: Default::default(), - diagnostic_summaries: Default::default(), queued_operations: Default::default(), language_registry: languages, client, @@ -1626,17 +1642,19 @@ impl RemoteWorktree { envelope: TypedEnvelope, cx: &mut ModelContext, ) { - let path: Arc = Path::new(&envelope.payload.path).into(); - self.diagnostic_summaries.insert( - path.clone(), - DiagnosticSummary { - error_count: envelope.payload.error_count as usize, - warning_count: envelope.payload.warning_count as usize, - info_count: envelope.payload.info_count as usize, - hint_count: envelope.payload.hint_count as usize, - }, - ); - cx.emit(Event::DiagnosticsUpdated(path)); + if let Some(summary) = envelope.payload.summary { + let path: Arc = Path::new(&summary.path).into(); + self.snapshot.diagnostic_summaries.insert( + PathKey(path.clone()), + DiagnosticSummary { + error_count: summary.error_count as usize, + warning_count: summary.warning_count as usize, + info_count: summary.info_count as usize, + hint_count: summary.hint_count as usize, + }, + ); + cx.emit(Event::DiagnosticsUpdated(path)); + } } pub fn disk_based_diagnostics_updated(&self, cx: &mut ModelContext) { @@ -1679,10 +1697,15 @@ impl Snapshot { root_name, entries: self .entries_by_path - .cursor::<()>() + .iter() .filter(|e| !e.is_ignored) .map(Into::into) .collect(), + diagnostic_summaries: self + .diagnostic_summaries + .iter() + .map(|(path, summary)| summary.to_proto(path.0.clone())) + .collect(), } } @@ -4160,6 +4183,7 @@ mod tests { root_name: Default::default(), root_char_bag: Default::default(), next_entry_id: next_entry_id.clone(), + diagnostic_summaries: Default::default(), }; initial_snapshot.insert_entry( Entry::new( diff --git a/crates/rpc/proto/zed.proto b/crates/rpc/proto/zed.proto index 71ceb4d9ac5cfce73d62bf27357202dcb9c1241a..669ffdbd7e50ae6f5a3579ee0db06cd55c29c312 100644 --- a/crates/rpc/proto/zed.proto +++ b/crates/rpc/proto/zed.proto @@ -170,6 +170,10 @@ message BufferSaved { message UpdateDiagnosticSummary { uint64 project_id = 1; uint64 worktree_id = 2; + DiagnosticSummary summary = 3; +} + +message DiagnosticSummary { string path = 3; uint32 error_count = 4; uint32 warning_count = 5; @@ -256,6 +260,7 @@ message Worktree { uint64 id = 1; string root_name = 2; repeated Entry entries = 3; + repeated DiagnosticSummary diagnostic_summaries = 4; } message Entry { diff --git a/crates/server/src/rpc.rs b/crates/server/src/rpc.rs index 0d84a89e1435168c2b805d20806c303ec223c69f..19792caace189d5652be903c4020112e6c63fbad 100644 --- a/crates/server/src/rpc.rs +++ b/crates/server/src/rpc.rs @@ -17,7 +17,7 @@ use rpc::{ Connection, ConnectionId, Peer, TypedEnvelope, }; use sha1::{Digest as _, Sha1}; -use std::{any::TypeId, future::Future, mem, sync::Arc, time::Instant}; +use std::{any::TypeId, future::Future, mem, path::PathBuf, sync::Arc, time::Instant}; use store::{Store, Worktree}; use surf::StatusCode; use tide::log; @@ -302,6 +302,11 @@ impl Server { id: *id, root_name: worktree.root_name.clone(), entries: share.entries.values().cloned().collect(), + diagnostic_summaries: share + .diagnostic_summaries + .values() + .cloned() + .collect(), }) }) .collect(); @@ -473,11 +478,17 @@ impl Server { .map(|entry| (entry.id, entry)) .collect(); + let diagnostic_summaries = mem::take(&mut worktree.diagnostic_summaries) + .into_iter() + .map(|summary| (PathBuf::from(summary.path.clone()), summary)) + .collect(); + let contact_user_ids = self.state_mut().share_worktree( request.payload.project_id, worktree.id, request.sender_id, entries, + diagnostic_summaries, ); if let Some(contact_user_ids) = contact_user_ids { self.peer.respond(request.receipt(), proto::Ack {}).await?; @@ -520,13 +531,23 @@ impl Server { } async fn update_diagnostic_summary( - self: Arc, + mut self: Arc, request: TypedEnvelope, ) -> tide::Result<()> { - let receiver_ids = self - .state() - .project_connection_ids(request.payload.project_id, request.sender_id) + let receiver_ids = request + .payload + .summary + .clone() + .and_then(|summary| { + self.state_mut().update_diagnostic_summary( + request.payload.project_id, + request.payload.worktree_id, + request.sender_id, + summary, + ) + }) .ok_or_else(|| anyhow!(NO_SUCH_PROJECT))?; + broadcast(request.sender_id, receiver_ids, |connection_id| { self.peer .forward_send(request.sender_id, connection_id, request.payload.clone()) @@ -1816,6 +1837,39 @@ mod tests { .await .unwrap(); + // Simulate a language server reporting errors for a file. + fake_language_server + .notify::(lsp::PublishDiagnosticsParams { + uri: lsp::Url::from_file_path("/a/a.rs").unwrap(), + version: None, + diagnostics: vec![lsp::Diagnostic { + severity: Some(lsp::DiagnosticSeverity::ERROR), + range: lsp::Range::new(lsp::Position::new(0, 4), lsp::Position::new(0, 7)), + message: "message 1".to_string(), + ..Default::default() + }], + }) + .await; + + // Wait for server to see the diagnostics update. + server + .condition(|store| { + let worktree = store + .project(project_id) + .unwrap() + .worktrees + .get(&worktree_id.to_proto()) + .unwrap(); + + !worktree + .share + .as_ref() + .unwrap() + .diagnostic_summaries + .is_empty() + }) + .await; + // Join the worktree as client B. let project_b = Project::remote( project_id, @@ -1828,7 +1882,24 @@ mod tests { .await .unwrap(); - // Simulate a language server reporting errors for a file. + project_b.read_with(&cx_b, |project, cx| { + assert_eq!( + project.diagnostic_summaries(cx).collect::>(), + &[( + ProjectPath { + worktree_id, + path: Arc::from(Path::new("a.rs")), + }, + DiagnosticSummary { + error_count: 1, + warning_count: 0, + ..Default::default() + }, + )] + ) + }); + + // Simulate a language server reporting more errors for a file. fake_language_server .notify::(lsp::PublishDiagnosticsParams { uri: lsp::Url::from_file_path("/a/a.rs").unwrap(), @@ -1853,6 +1924,7 @@ mod tests { }) .await; + // Client b gets the updated summaries project_b .condition(&cx_b, |project, cx| { project.diagnostic_summaries(cx).collect::>() @@ -1870,7 +1942,7 @@ mod tests { }) .await; - // Open the file with the errors. + // Open the file with the errors on client B. They should be present. let worktree_b = project_b.update(&mut cx_b, |p, _| p.worktrees()[0].clone()); let buffer_b = cx_b .background() diff --git a/crates/server/src/rpc/store.rs b/crates/server/src/rpc/store.rs index e4d740629fbb4816f892c6cfa589596036116795..7e8523b06cec33c416820d3d797b69e238feceed 100644 --- a/crates/server/src/rpc/store.rs +++ b/crates/server/src/rpc/store.rs @@ -1,8 +1,8 @@ use crate::db::{ChannelId, UserId}; use anyhow::anyhow; -use collections::{HashMap, HashSet}; +use collections::{BTreeMap, HashMap, HashSet}; use rpc::{proto, ConnectionId}; -use std::collections::hash_map; +use std::{collections::hash_map, path::PathBuf}; #[derive(Default)] pub struct Store { @@ -41,6 +41,7 @@ pub struct ProjectShare { pub struct WorktreeShare { pub entries: HashMap, + pub diagnostic_summaries: BTreeMap, } #[derive(Default)] @@ -385,17 +386,42 @@ impl Store { worktree_id: u64, connection_id: ConnectionId, entries: HashMap, + diagnostic_summaries: BTreeMap, ) -> Option> { let project = self.projects.get_mut(&project_id)?; let worktree = project.worktrees.get_mut(&worktree_id)?; if project.host_connection_id == connection_id && project.share.is_some() { - worktree.share = Some(WorktreeShare { entries }); + worktree.share = Some(WorktreeShare { + entries, + diagnostic_summaries, + }); Some(project.authorized_user_ids()) } else { None } } + pub fn update_diagnostic_summary( + &mut self, + project_id: u64, + worktree_id: u64, + connection_id: ConnectionId, + summary: proto::DiagnosticSummary, + ) -> Option> { + let project = self.projects.get_mut(&project_id)?; + let worktree = project.worktrees.get_mut(&worktree_id)?; + if project.host_connection_id == connection_id { + if let Some(share) = worktree.share.as_mut() { + share + .diagnostic_summaries + .insert(summary.path.clone().into(), summary); + return Some(project.connection_ids()); + } + } + + None + } + pub fn join_project( &mut self, connection_id: ConnectionId, @@ -497,6 +523,11 @@ impl Store { Some(self.channels.get(&channel_id)?.connection_ids()) } + #[cfg(test)] + pub fn project(&self, project_id: u64) -> Option<&Project> { + self.projects.get(&project_id) + } + pub fn read_project(&self, project_id: u64, connection_id: ConnectionId) -> Option<&Project> { let project = self.projects.get(&project_id)?; if project.host_connection_id == connection_id diff --git a/crates/sum_tree/src/tree_map.rs b/crates/sum_tree/src/tree_map.rs index f50c233d050dd72c7af6c9532247f42a8c010865..1de6b2f589470e11fbd436d76c77604ca1219a34 100644 --- a/crates/sum_tree/src/tree_map.rs +++ b/crates/sum_tree/src/tree_map.rs @@ -21,6 +21,16 @@ pub struct MapKey(K); pub struct MapKeyRef<'a, K>(Option<&'a K>); impl TreeMap { + pub fn from_ordered_entries(entries: impl IntoIterator) -> Self { + let tree = SumTree::from_iter( + entries + .into_iter() + .map(|(key, value)| MapEntry { key, value }), + &(), + ); + Self(tree) + } + pub fn get<'a>(&self, key: &'a K) -> Option<&V> { let mut cursor = self.0.cursor::>(); cursor.seek(&MapKeyRef(Some(key)), Bias::Left, &()); From 943571af2ab6bb80837948227eea819455fb2dc9 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Thu, 6 Jan 2022 13:33:55 -0700 Subject: [PATCH 20/43] Report backtraces of pending conditions when deterministic executor illegally parks Co-Authored-By: Max Brunsfeld --- crates/gpui/src/app.rs | 10 ++- crates/gpui/src/executor.rs | 125 +++++++++++++++++++++++++----------- crates/server/src/rpc.rs | 33 ++++++---- 3 files changed, 109 insertions(+), 59 deletions(-) diff --git a/crates/gpui/src/app.rs b/crates/gpui/src/app.rs index e42e8894966939202508575c9031a60e6f930c2b..e57bcbc764292876e200e512e2e3d44dccc2483a 100644 --- a/crates/gpui/src/app.rs +++ b/crates/gpui/src/app.rs @@ -2660,8 +2660,6 @@ impl ModelHandle { loop { { let cx = cx.borrow(); - let executor = cx.foreground(); - let cx = cx.as_ref(); if predicate( handle @@ -2672,15 +2670,13 @@ impl ModelHandle { ) { break; } - - if executor.parking_forbidden() && executor.would_park() { - panic!("parked while waiting on condition"); - } } + cx.borrow().foreground().start_waiting(); rx.recv() .await .expect("model dropped with pending condition"); + cx.borrow().foreground().finish_waiting(); } }) .await @@ -2920,9 +2916,11 @@ impl ViewHandle { } } + cx.borrow().foreground().start_waiting(); rx.recv() .await .expect("view dropped with pending condition"); + cx.borrow().foreground().finish_waiting(); } }) .await diff --git a/crates/gpui/src/executor.rs b/crates/gpui/src/executor.rs index 6b20f56f9e275a54055366582f970b818f4be9a0..596b1be18424fd73d4a05b940349995801321d09 100644 --- a/crates/gpui/src/executor.rs +++ b/crates/gpui/src/executor.rs @@ -77,6 +77,7 @@ struct DeterministicState { block_on_ticks: RangeInclusive, now: Instant, pending_timers: Vec<(Instant, barrier::Sender)>, + waiting_backtrace: Option, } pub struct Deterministic { @@ -97,6 +98,7 @@ impl Deterministic { block_on_ticks: 0..=1000, now: Instant::now(), pending_timers: Default::default(), + waiting_backtrace: None, })), parker: Default::default(), } @@ -143,8 +145,8 @@ impl Deterministic { return result; } - if !woken.load(SeqCst) && self.state.lock().forbid_parking { - panic!("deterministic executor parked after a call to forbid_parking"); + if !woken.load(SeqCst) { + self.state.lock().will_park(); } woken.store(false, SeqCst); @@ -206,7 +208,11 @@ impl Deterministic { } let state = self.state.lock(); - if state.would_park() { + + if state.scheduled_from_foreground.is_empty() + && state.scheduled_from_background.is_empty() + && state.spawned_from_foreground.is_empty() + { return None; } } @@ -241,11 +247,9 @@ impl Deterministic { if let Poll::Ready(result) = future.as_mut().poll(&mut cx) { return Some(result); } - let state = self.state.lock(); + let mut state = self.state.lock(); if state.scheduled_from_background.is_empty() { - if state.forbid_parking { - panic!("deterministic executor parked after a call to forbid_parking"); - } + state.will_park(); drop(state); self.parker.lock().park(); } @@ -259,11 +263,22 @@ impl Deterministic { } impl DeterministicState { - fn would_park(&self) -> bool { - self.forbid_parking - && self.scheduled_from_foreground.is_empty() - && self.scheduled_from_background.is_empty() - && self.spawned_from_foreground.is_empty() + fn will_park(&mut self) { + if self.forbid_parking { + let mut backtrace_message = String::new(); + if let Some(backtrace) = self.waiting_backtrace.as_mut() { + backtrace.resolve(); + backtrace_message = format!( + "\nbacktrace of waiting future:\n{:?}", + CwdBacktrace::new(backtrace) + ); + } + + panic!( + "deterministic executor parked after a call to forbid_parking{}", + backtrace_message + ); + } } } @@ -312,32 +327,53 @@ impl Trace { } } -impl Debug for Trace { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - struct FirstCwdFrameInBacktrace<'a>(&'a Backtrace); - - impl<'a> Debug for FirstCwdFrameInBacktrace<'a> { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> std::fmt::Result { - let cwd = std::env::current_dir().unwrap(); - let mut print_path = |fmt: &mut fmt::Formatter<'_>, path: BytesOrWideString<'_>| { - fmt::Display::fmt(&path, fmt) - }; - let mut fmt = BacktraceFmt::new(f, backtrace::PrintFmt::Full, &mut print_path); - for frame in self.0.frames() { - let mut formatted_frame = fmt.frame(); - if frame - .symbols() - .iter() - .any(|s| s.filename().map_or(false, |f| f.starts_with(&cwd))) - { - formatted_frame.backtrace_frame(frame)?; - break; - } +struct CwdBacktrace<'a> { + backtrace: &'a Backtrace, + first_frame_only: bool, +} + +impl<'a> CwdBacktrace<'a> { + fn new(backtrace: &'a Backtrace) -> Self { + Self { + backtrace, + first_frame_only: false, + } + } + + fn first_frame(backtrace: &'a Backtrace) -> Self { + Self { + backtrace, + first_frame_only: true, + } + } +} + +impl<'a> Debug for CwdBacktrace<'a> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> std::fmt::Result { + let cwd = std::env::current_dir().unwrap(); + let mut print_path = |fmt: &mut fmt::Formatter<'_>, path: BytesOrWideString<'_>| { + fmt::Display::fmt(&path, fmt) + }; + let mut fmt = BacktraceFmt::new(f, backtrace::PrintFmt::Full, &mut print_path); + for frame in self.backtrace.frames() { + let mut formatted_frame = fmt.frame(); + if frame + .symbols() + .iter() + .any(|s| s.filename().map_or(false, |f| f.starts_with(&cwd))) + { + formatted_frame.backtrace_frame(frame)?; + if self.first_frame_only { + break; } - fmt.finish() } } + fmt.finish() + } +} +impl Debug for Trace { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { for ((backtrace, scheduled), spawned_from_foreground) in self .executed .iter() @@ -346,7 +382,7 @@ impl Debug for Trace { { writeln!(f, "Scheduled")?; for backtrace in scheduled { - writeln!(f, "- {:?}", FirstCwdFrameInBacktrace(backtrace))?; + writeln!(f, "- {:?}", CwdBacktrace::first_frame(backtrace))?; } if scheduled.is_empty() { writeln!(f, "None")?; @@ -355,14 +391,14 @@ impl Debug for Trace { writeln!(f, "Spawned from foreground")?; for backtrace in spawned_from_foreground { - writeln!(f, "- {:?}", FirstCwdFrameInBacktrace(backtrace))?; + writeln!(f, "- {:?}", CwdBacktrace::first_frame(backtrace))?; } if spawned_from_foreground.is_empty() { writeln!(f, "None")?; } writeln!(f, "==========")?; - writeln!(f, "Run: {:?}", FirstCwdFrameInBacktrace(backtrace))?; + writeln!(f, "Run: {:?}", CwdBacktrace::first_frame(backtrace))?; writeln!(f, "+++++++++++++++++++")?; } @@ -446,9 +482,20 @@ impl Foreground { } } - pub fn would_park(&self) -> bool { + pub fn start_waiting(&self) { match self { - Self::Deterministic(executor) => executor.state.lock().would_park(), + Self::Deterministic(executor) => { + executor.state.lock().waiting_backtrace = Some(Backtrace::new_unresolved()); + } + _ => panic!("this method can only be called on a deterministic executor"), + } + } + + pub fn finish_waiting(&self) { + match self { + Self::Deterministic(executor) => { + executor.state.lock().waiting_backtrace.take(); + } _ => panic!("this method can only be called on a deterministic executor"), } } diff --git a/crates/server/src/rpc.rs b/crates/server/src/rpc.rs index 19792caace189d5652be903c4020112e6c63fbad..510b701ddd5b5ef5f41e4915703206633e7a2ef3 100644 --- a/crates/server/src/rpc.rs +++ b/crates/server/src/rpc.rs @@ -1054,7 +1054,7 @@ mod tests { }; use ::rpc::Peer; use async_std::task; - use gpui::{ModelHandle, TestAppContext}; + use gpui::{executor, ModelHandle, TestAppContext}; use parking_lot::Mutex; use postage::{mpsc, watch}; use rpc::PeerId; @@ -1063,6 +1063,7 @@ mod tests { use std::{ ops::Deref, path::Path, + rc::Rc, sync::{ atomic::{AtomicBool, Ordering::SeqCst}, Arc, @@ -1092,7 +1093,7 @@ mod tests { cx_a.foreground().forbid_parking(); // Connect to a server as 2 clients. - let mut server = TestServer::start().await; + let mut server = TestServer::start(cx_a.foreground()).await; let client_a = server.create_client(&mut cx_a, "user_a").await; let client_b = server.create_client(&mut cx_b, "user_b").await; @@ -1225,7 +1226,7 @@ mod tests { cx_a.foreground().forbid_parking(); // Connect to a server as 2 clients. - let mut server = TestServer::start().await; + let mut server = TestServer::start(cx_a.foreground()).await; let client_a = server.create_client(&mut cx_a, "user_a").await; let client_b = server.create_client(&mut cx_b, "user_b").await; @@ -1301,7 +1302,7 @@ mod tests { cx_a.foreground().forbid_parking(); // Connect to a server as 3 clients. - let mut server = TestServer::start().await; + let mut server = TestServer::start(cx_a.foreground()).await; let client_a = server.create_client(&mut cx_a, "user_a").await; let client_b = server.create_client(&mut cx_b, "user_b").await; let client_c = server.create_client(&mut cx_c, "user_c").await; @@ -1451,7 +1452,7 @@ mod tests { let fs = Arc::new(FakeFs::new()); // Connect to a server as 2 clients. - let mut server = TestServer::start().await; + let mut server = TestServer::start(cx_a.foreground()).await; let client_a = server.create_client(&mut cx_a, "user_a").await; let client_b = server.create_client(&mut cx_b, "user_b").await; @@ -1547,7 +1548,7 @@ mod tests { let fs = Arc::new(FakeFs::new()); // Connect to a server as 2 clients. - let mut server = TestServer::start().await; + let mut server = TestServer::start(cx_a.foreground()).await; let client_a = server.create_client(&mut cx_a, "user_a").await; let client_b = server.create_client(&mut cx_b, "user_b").await; @@ -1627,7 +1628,7 @@ mod tests { let fs = Arc::new(FakeFs::new()); // Connect to a server as 2 clients. - let mut server = TestServer::start().await; + let mut server = TestServer::start(cx_a.foreground()).await; let client_a = server.create_client(&mut cx_a, "user_a").await; let client_b = server.create_client(&mut cx_b, "user_b").await; @@ -1702,7 +1703,7 @@ mod tests { let fs = Arc::new(FakeFs::new()); // Connect to a server as 2 clients. - let mut server = TestServer::start().await; + let mut server = TestServer::start(cx_a.foreground()).await; let client_a = server.create_client(&mut cx_a, "user_a").await; let client_b = server.create_client(&mut cx_b, "user_b").await; @@ -1789,7 +1790,7 @@ mod tests { ))); // Connect to a server as 2 clients. - let mut server = TestServer::start().await; + let mut server = TestServer::start(cx_a.foreground()).await; let client_a = server.create_client(&mut cx_a, "user_a").await; let client_b = server.create_client(&mut cx_b, "user_b").await; @@ -1988,7 +1989,7 @@ mod tests { cx_a.foreground().forbid_parking(); // Connect to a server as 2 clients. - let mut server = TestServer::start().await; + let mut server = TestServer::start(cx_a.foreground()).await; let client_a = server.create_client(&mut cx_a, "user_a").await; let client_b = server.create_client(&mut cx_b, "user_b").await; @@ -2127,7 +2128,7 @@ mod tests { async fn test_chat_message_validation(mut cx_a: TestAppContext) { cx_a.foreground().forbid_parking(); - let mut server = TestServer::start().await; + let mut server = TestServer::start(cx_a.foreground()).await; let client_a = server.create_client(&mut cx_a, "user_a").await; let db = &server.app_state.db; @@ -2188,7 +2189,7 @@ mod tests { cx_a.foreground().forbid_parking(); // Connect to a server as 2 clients. - let mut server = TestServer::start().await; + let mut server = TestServer::start(cx_a.foreground()).await; let client_a = server.create_client(&mut cx_a, "user_a").await; let client_b = server.create_client(&mut cx_b, "user_b").await; let mut status_b = client_b.status(); @@ -2406,7 +2407,7 @@ mod tests { let fs = Arc::new(FakeFs::new()); // Connect to a server as 3 clients. - let mut server = TestServer::start().await; + let mut server = TestServer::start(cx_a.foreground()).await; let client_a = server.create_client(&mut cx_a, "user_a").await; let client_b = server.create_client(&mut cx_b, "user_b").await; let client_c = server.create_client(&mut cx_c, "user_c").await; @@ -2539,6 +2540,7 @@ mod tests { peer: Arc, app_state: Arc, server: Arc, + foreground: Rc, notifications: mpsc::Receiver<()>, connection_killers: Arc>>>>, forbid_connections: Arc, @@ -2546,7 +2548,7 @@ mod tests { } impl TestServer { - async fn start() -> Self { + async fn start(foreground: Rc) -> Self { let test_db = TestDb::new(); let app_state = Self::build_app_state(&test_db).await; let peer = Peer::new(); @@ -2556,6 +2558,7 @@ mod tests { peer, app_state, server, + foreground, notifications: notifications.1, connection_killers: Default::default(), forbid_connections: Default::default(), @@ -2671,7 +2674,9 @@ mod tests { { async_std::future::timeout(Duration::from_millis(500), async { while !(predicate)(&*self.server.store.read()) { + self.foreground.start_waiting(); self.notifications.recv().await; + self.foreground.finish_waiting(); } }) .await From 10548c2038427a293005ebe6875dd2a9e9299bd3 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Thu, 6 Jan 2022 14:22:28 -0800 Subject: [PATCH 21/43] Always group diagnostics the way they're grouped in the LSP message Co-Authored-By: Nathan Sobo --- crates/project/src/worktree.rs | 147 ++++++++++++++++++--------------- 1 file changed, 79 insertions(+), 68 deletions(-) diff --git a/crates/project/src/worktree.rs b/crates/project/src/worktree.rs index 3533714c04bb24170ba52047c06d7bb2c5e13fe5..c3bd5886842df7caea6fccb3db4f1d02432db450 100644 --- a/crates/project/src/worktree.rs +++ b/crates/project/src/worktree.rs @@ -34,7 +34,6 @@ use std::{ ffi::{OsStr, OsString}, fmt, future::Future, - mem, ops::{Deref, Range}, path::{Path, PathBuf}, sync::{ @@ -691,7 +690,7 @@ impl Worktree { pub fn update_diagnostics( &mut self, - mut params: lsp::PublishDiagnosticsParams, + params: lsp::PublishDiagnosticsParams, disk_based_sources: &HashSet, cx: &mut ModelContext, ) -> Result<()> { @@ -706,58 +705,92 @@ impl Worktree { .context("path is not within worktree")?, ); - let mut group_ids_by_diagnostic_range = HashMap::default(); - let mut diagnostics_by_group_id = HashMap::default(); let mut next_group_id = 0; - for diagnostic in &mut params.diagnostics { + let mut diagnostics = Vec::default(); + let mut primary_diagnostic_group_ids = HashMap::default(); + let mut sources_by_group_id = HashMap::default(); + let mut supporting_diagnostic_severities = HashMap::default(); + for diagnostic in ¶ms.diagnostics { let source = diagnostic.source.as_ref(); - let code = diagnostic.code.as_ref(); - let group_id = diagnostic_ranges(&diagnostic, &abs_path) - .find_map(|range| group_ids_by_diagnostic_range.get(&(source, code, range))) - .copied() - .unwrap_or_else(|| { - let group_id = post_inc(&mut next_group_id); - for range in diagnostic_ranges(&diagnostic, &abs_path) { - group_ids_by_diagnostic_range.insert((source, code, range), group_id); - } - group_id + let code = diagnostic.code.as_ref().map(|code| match code { + lsp::NumberOrString::Number(code) => code.to_string(), + lsp::NumberOrString::String(code) => code.clone(), + }); + let range = range_from_lsp(diagnostic.range); + let is_supporting = diagnostic + .related_information + .as_ref() + .map_or(false, |infos| { + infos.iter().any(|info| { + primary_diagnostic_group_ids.contains_key(&( + source, + code.clone(), + range_from_lsp(info.location.range), + )) + }) }); - diagnostics_by_group_id - .entry(group_id) - .or_insert(Vec::new()) - .push(DiagnosticEntry { - range: diagnostic.range.start.to_point_utf16() - ..diagnostic.range.end.to_point_utf16(), + if is_supporting { + if let Some(severity) = diagnostic.severity { + supporting_diagnostic_severities + .insert((source, code.clone(), range), severity); + } + } else { + let group_id = post_inc(&mut next_group_id); + let is_disk_based = + source.map_or(false, |source| disk_based_sources.contains(source)); + + sources_by_group_id.insert(group_id, source); + primary_diagnostic_group_ids + .insert((source, code.clone(), range.clone()), group_id); + + diagnostics.push(DiagnosticEntry { + range, diagnostic: Diagnostic { - code: diagnostic.code.clone().map(|code| match code { - lsp::NumberOrString::Number(code) => code.to_string(), - lsp::NumberOrString::String(code) => code, - }), + code: code.clone(), severity: diagnostic.severity.unwrap_or(DiagnosticSeverity::ERROR), - message: mem::take(&mut diagnostic.message), + message: diagnostic.message.clone(), group_id, - is_primary: false, + is_primary: true, is_valid: true, - is_disk_based: diagnostic - .source - .as_ref() - .map_or(false, |source| disk_based_sources.contains(source)), + is_disk_based, }, }); + if let Some(infos) = &diagnostic.related_information { + for info in infos { + if info.location.uri == params.uri { + let range = range_from_lsp(info.location.range); + diagnostics.push(DiagnosticEntry { + range, + diagnostic: Diagnostic { + code: code.clone(), + severity: DiagnosticSeverity::INFORMATION, + message: info.message.clone(), + group_id, + is_primary: false, + is_valid: true, + is_disk_based, + }, + }); + } + } + } + } } - let diagnostics = diagnostics_by_group_id - .into_values() - .flat_map(|mut diagnostics| { - let primary = diagnostics - .iter_mut() - .min_by_key(|entry| entry.diagnostic.severity) - .unwrap(); - primary.diagnostic.is_primary = true; - diagnostics - }) - .collect::>(); + for entry in &mut diagnostics { + let diagnostic = &mut entry.diagnostic; + if !diagnostic.is_primary { + let source = *sources_by_group_id.get(&diagnostic.group_id).unwrap(); + if let Some(&severity) = supporting_diagnostic_severities.get(&( + source, + diagnostic.code.clone(), + entry.range.clone(), + )) { + diagnostic.severity = severity; + } + } + } self.update_diagnostic_entries(worktree_path, params.version, diagnostics, cx)?; Ok(()) @@ -3103,32 +3136,10 @@ impl ToPointUtf16 for lsp::Position { } } -fn diagnostic_ranges<'a>( - diagnostic: &'a lsp::Diagnostic, - abs_path: &'a Path, -) -> impl 'a + Iterator> { - diagnostic - .related_information - .iter() - .flatten() - .filter_map(move |info| { - if info.location.uri.to_file_path().ok()? == abs_path { - let info_start = PointUtf16::new( - info.location.range.start.line, - info.location.range.start.character, - ); - let info_end = PointUtf16::new( - info.location.range.end.line, - info.location.range.end.character, - ); - Some(info_start..info_end) - } else { - None - } - }) - .chain(Some( - diagnostic.range.start.to_point_utf16()..diagnostic.range.end.to_point_utf16(), - )) +fn range_from_lsp(range: lsp::Range) -> Range { + let start = PointUtf16::new(range.start.line, range.start.character); + let end = PointUtf16::new(range.end.line, range.end.character); + start..end } #[cfg(test)] From 7357b3ff2a2468c4b8f414855b34253c9957f3dc Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Thu, 6 Jan 2022 14:38:13 -0800 Subject: [PATCH 22/43] Revert "Remove special handling of multi-line primary diagnostic messages and fix tests" This reverts commit ce4142eab39118a2ecd43a0473a3e7035087191e. --- crates/diagnostics/src/diagnostics.rs | 84 +++++++++------------------ 1 file changed, 28 insertions(+), 56 deletions(-) diff --git a/crates/diagnostics/src/diagnostics.rs b/crates/diagnostics/src/diagnostics.rs index 2aa062af91d41023ac25a2c29857bd1c2f06d219..8b54fa78131091f4d7fddebc1dd4d81e75a06653 100644 --- a/crates/diagnostics/src/diagnostics.rs +++ b/crates/diagnostics/src/diagnostics.rs @@ -281,14 +281,17 @@ impl ProjectDiagnosticsEditor { if is_first_excerpt_for_group { is_first_excerpt_for_group = false; let primary = &group.entries[group.primary_ix].diagnostic; + let mut header = primary.clone(); + header.message = + primary.message.split('\n').next().unwrap().to_string(); group_state.block_count += 1; - diagnostic_blocks.push(DiagnosticBlock::Header(primary.clone())); + diagnostic_blocks.push(DiagnosticBlock::Header(header.clone())); blocks_to_add.push(BlockProperties { position: header_position, - height: primary.message.matches('\n').count() as u8 + 2, + height: 2, render: diagnostic_header_renderer( buffer.clone(), - primary.clone(), + header, true, self.build_settings.clone(), ), @@ -306,17 +309,21 @@ impl ProjectDiagnosticsEditor { } for entry in &group.entries[*start_ix..ix] { - if !entry.diagnostic.is_primary { + let mut diagnostic = entry.diagnostic.clone(); + if diagnostic.is_primary { + diagnostic.message = + entry.diagnostic.message.split('\n').skip(1).collect(); + } + + if !diagnostic.message.is_empty() { group_state.block_count += 1; diagnostic_blocks - .push(DiagnosticBlock::Inline(entry.diagnostic.clone())); + .push(DiagnosticBlock::Inline(diagnostic.clone())); blocks_to_add.push(BlockProperties { position: (excerpt_id.clone(), entry.range.start.clone()), - height: entry.diagnostic.message.matches('\n').count() - as u8 - + 1, + height: diagnostic.message.matches('\n').count() as u8 + 1, render: diagnostic_block_renderer( - entry.diagnostic.clone(), + diagnostic, true, self.build_settings.clone(), ), @@ -601,7 +608,7 @@ mod tests { DiagnosticEntry { range: PointUtf16::new(7, 6)..PointUtf16::new(7, 7), diagnostic: Diagnostic { - message: "use of moved value".to_string(), + message: "use of moved value\nvalue used here after move".to_string(), severity: DiagnosticSeverity::ERROR, is_primary: true, is_disk_based: true, @@ -609,21 +616,10 @@ mod tests { ..Default::default() }, }, - DiagnosticEntry { - range: PointUtf16::new(7, 6)..PointUtf16::new(7, 7), - diagnostic: Diagnostic { - message: "value used here after move".to_string(), - severity: DiagnosticSeverity::INFORMATION, - is_primary: false, - is_disk_based: true, - group_id: 0, - ..Default::default() - }, - }, DiagnosticEntry { range: PointUtf16::new(8, 6)..PointUtf16::new(8, 7), diagnostic: Diagnostic { - message: "use of moved value".to_string(), + message: "use of moved value\nvalue used here after move".to_string(), severity: DiagnosticSeverity::ERROR, is_primary: true, is_disk_based: true, @@ -631,17 +627,6 @@ mod tests { ..Default::default() }, }, - DiagnosticEntry { - range: PointUtf16::new(8, 6)..PointUtf16::new(8, 7), - diagnostic: Diagnostic { - message: "value used here after move".to_string(), - severity: DiagnosticSeverity::INFORMATION, - is_primary: false, - is_disk_based: true, - group_id: 1, - ..Default::default() - }, - }, ], cx, ) @@ -703,30 +688,17 @@ mod tests { .update_diagnostic_entries( Arc::from("/test/a.rs".as_ref()), None, - vec![ - DiagnosticEntry { - range: PointUtf16::new(0, 15)..PointUtf16::new(0, 15), - diagnostic: Diagnostic { - message: "mismatched types".to_string(), - severity: DiagnosticSeverity::ERROR, - is_primary: true, - is_disk_based: true, - group_id: 0, - ..Default::default() - }, + vec![DiagnosticEntry { + range: PointUtf16::new(0, 15)..PointUtf16::new(0, 15), + diagnostic: Diagnostic { + message: "mismatched types\nexpected `usize`, found `char`".to_string(), + severity: DiagnosticSeverity::ERROR, + is_primary: true, + is_disk_based: true, + group_id: 0, + ..Default::default() }, - DiagnosticEntry { - range: PointUtf16::new(0, 15)..PointUtf16::new(0, 15), - diagnostic: Diagnostic { - message: "expected `usize`, found `char`".to_string(), - severity: DiagnosticSeverity::INFORMATION, - is_primary: false, - is_disk_based: true, - group_id: 0, - ..Default::default() - }, - }, - ], + }], cx, ) .unwrap(); From 9bbe67f0ea643a3ef019f8eb86791faf6b287cd7 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Thu, 6 Jan 2022 15:04:06 -0800 Subject: [PATCH 23/43] Don't clobber diagnostics when getting new snapshot from background scanner Co-Authored-By: Nathan Sobo --- crates/project/src/worktree.rs | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/crates/project/src/worktree.rs b/crates/project/src/worktree.rs index c3bd5886842df7caea6fccb3db4f1d02432db450..6108e9eb108a0f170e5056702b00db78a1f0c166 100644 --- a/crates/project/src/worktree.rs +++ b/crates/project/src/worktree.rs @@ -34,6 +34,7 @@ use std::{ ffi::{OsStr, OsString}, fmt, future::Future, + mem, ops::{Deref, Range}, path::{Path, PathBuf}, sync::{ @@ -583,7 +584,9 @@ impl Worktree { match self { Self::Local(worktree) => { let is_fake_fs = worktree.fs.is_fake(); - worktree.snapshot = worktree.background_snapshot.lock().clone(); + worktree + .snapshot + .assign(worktree.background_snapshot.lock().clone()); if worktree.is_scanning() { if worktree.poll_task.is_none() { worktree.poll_task = Some(cx.spawn(|this, mut cx| async move { @@ -1842,6 +1845,14 @@ impl Snapshot { Ok(()) } + fn assign(&mut self, mut other: Self) { + mem::swap( + &mut self.diagnostic_summaries, + &mut other.diagnostic_summaries, + ); + *self = other; + } + pub fn file_count(&self) -> usize { self.entries_by_path.summary().file_count } From b19d92e918b8d799712e5e225f9aac54d02e1705 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Thu, 6 Jan 2022 17:01:13 -0800 Subject: [PATCH 24/43] Keep selections at the top of the project diagnostics view when it is first populated Co-Authored-By: Nathan Sobo --- crates/diagnostics/src/diagnostics.rs | 23 +++++++- crates/editor/src/editor.rs | 78 +++++++++++++-------------- 2 files changed, 60 insertions(+), 41 deletions(-) diff --git a/crates/diagnostics/src/diagnostics.rs b/crates/diagnostics/src/diagnostics.rs index 8b54fa78131091f4d7fddebc1dd4d81e75a06653..dfaac9f150e02bcdaea36db7ed610f7d418bb8bc 100644 --- a/crates/diagnostics/src/diagnostics.rs +++ b/crates/diagnostics/src/diagnostics.rs @@ -9,7 +9,7 @@ use gpui::{ action, elements::*, keymap::Binding, AppContext, Entity, ModelHandle, MutableAppContext, RenderContext, Task, View, ViewContext, ViewHandle, }; -use language::{Bias, Buffer, Diagnostic, DiagnosticEntry, Point}; +use language::{Bias, Buffer, Diagnostic, DiagnosticEntry, Point, Selection, SelectionGoal}; use postage::watch; use project::{Project, ProjectPath, WorktreeId}; use std::{cmp::Ordering, ops::Range, path::Path, sync::Arc}; @@ -183,6 +183,7 @@ impl ProjectDiagnosticsEditor { } } + let was_empty = self.path_states.is_empty(); let path_ix = match self .path_states .binary_search_by_key(&path.as_ref(), |e| e.0.as_ref()) @@ -375,7 +376,21 @@ impl ProjectDiagnosticsEditor { group_state.blocks = block_ids.by_ref().take(group_state.block_count).collect(); } - editor.refresh_selections(cx); + if was_empty { + editor.update_selections( + vec![Selection { + id: 0, + start: 0, + end: 0, + reversed: false, + goal: SelectionGoal::None, + }], + None, + cx, + ); + } else { + editor.refresh_selections(cx); + } }); for ix in group_ixs_to_remove.into_iter().rev() { @@ -681,6 +696,10 @@ mod tests { "}" ) ); + + view.editor.update(cx, |editor, cx| { + assert_eq!(editor.selected_ranges::(cx), [0..0]); + }); }); worktree.update(&mut cx, |worktree, cx| { diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index c8d95da3eb425322ff86e5f030829031f9cef97f..4d3aa058be64589e46164b7480ba6104c7234ccc 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -1056,6 +1056,45 @@ impl Editor { } } + #[cfg(any(test, feature = "test-support"))] + pub fn selected_ranges>( + &self, + cx: &mut MutableAppContext, + ) -> Vec> { + self.local_selections::(cx) + .iter() + .map(|s| { + if s.reversed { + s.end.clone()..s.start.clone() + } else { + s.start.clone()..s.end.clone() + } + }) + .collect() + } + + #[cfg(any(test, feature = "test-support"))] + pub fn selected_display_ranges(&self, cx: &mut MutableAppContext) -> Vec> { + let display_map = self + .display_map + .update(cx, |display_map, cx| display_map.snapshot(cx)); + self.selections + .iter() + .chain( + self.pending_selection + .as_ref() + .map(|pending| &pending.selection), + ) + .map(|s| { + if s.reversed { + s.end.to_display_point(&display_map)..s.start.to_display_point(&display_map) + } else { + s.start.to_display_point(&display_map)..s.end.to_display_point(&display_map) + } + }) + .collect() + } + pub fn select_ranges( &mut self, ranges: I, @@ -6179,45 +6218,6 @@ mod tests { }); } - impl Editor { - fn selected_ranges>( - &self, - cx: &mut MutableAppContext, - ) -> Vec> { - self.local_selections::(cx) - .iter() - .map(|s| { - if s.reversed { - s.end.clone()..s.start.clone() - } else { - s.start.clone()..s.end.clone() - } - }) - .collect() - } - - fn selected_display_ranges(&self, cx: &mut MutableAppContext) -> Vec> { - let display_map = self - .display_map - .update(cx, |display_map, cx| display_map.snapshot(cx)); - self.selections - .iter() - .chain( - self.pending_selection - .as_ref() - .map(|pending| &pending.selection), - ) - .map(|s| { - if s.reversed { - s.end.to_display_point(&display_map)..s.start.to_display_point(&display_map) - } else { - s.start.to_display_point(&display_map)..s.end.to_display_point(&display_map) - } - }) - .collect() - } - } - fn empty_range(row: usize, column: usize) -> Range { let point = DisplayPoint::new(row as u32, column as u32); point..point From 1f762e482d5e35c362643d4171fdb546133100ca Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Thu, 6 Jan 2022 17:29:34 -0800 Subject: [PATCH 25/43] Unify Flexible and Expanded elements We'll use the name Expanded for something else now. Co-Authored-By: Nathan Sobo --- crates/chat_panel/src/chat_panel.rs | 2 +- crates/contacts_panel/src/contacts_panel.rs | 2 +- crates/file_finder/src/file_finder.rs | 3 +- crates/gpui/src/elements.rs | 4 +- crates/gpui/src/elements/flex.rs | 77 +-------------------- crates/theme_selector/src/theme_selector.rs | 2 +- crates/workspace/src/pane.rs | 14 ++-- crates/workspace/src/pane_group.rs | 2 +- crates/workspace/src/sidebar.rs | 21 +++--- crates/workspace/src/status_bar.rs | 2 +- crates/workspace/src/workspace.rs | 74 +++++++++----------- 11 files changed, 58 insertions(+), 145 deletions(-) diff --git a/crates/chat_panel/src/chat_panel.rs b/crates/chat_panel/src/chat_panel.rs index 0ec3884a8569854aef07a31c5642ea8e36fd5d5a..993a0cec0e6e6b923d2141195f47674267f7d3a6 100644 --- a/crates/chat_panel/src/chat_panel.rs +++ b/crates/chat_panel/src/chat_panel.rs @@ -233,7 +233,7 @@ impl ChatPanel { Empty::new().boxed() }; - Expanded::new(1., messages).boxed() + Flexible::new(1., true, messages).boxed() } fn render_message(&self, message: &ChannelMessage) -> ElementBox { diff --git a/crates/contacts_panel/src/contacts_panel.rs b/crates/contacts_panel/src/contacts_panel.rs index 2fcdf79bceb5ad4466e219f4d5292592906e4946..fc875889aa0ed1b502e7b0dcec93bc23d27e09e7 100644 --- a/crates/contacts_panel/src/contacts_panel.rs +++ b/crates/contacts_panel/src/contacts_panel.rs @@ -214,7 +214,7 @@ impl ContactsPanel { })); } }) - .expanded(1.0) + .flexible(1., true) .boxed() }) .constrained() diff --git a/crates/file_finder/src/file_finder.rs b/crates/file_finder/src/file_finder.rs index c973551f8df465e16cbaa2313c72a851c3bacd85..f317a189167f16eeddab18f25b47a35e5f8ee3e5 100644 --- a/crates/file_finder/src/file_finder.rs +++ b/crates/file_finder/src/file_finder.rs @@ -83,7 +83,7 @@ impl View for FileFinder { .with_style(settings.theme.selector.input_editor.container) .boxed(), ) - .with_child(Flexible::new(1.0, self.render_matches()).boxed()) + .with_child(Flexible::new(1.0, false, self.render_matches()).boxed()) .boxed(), ) .with_style(settings.theme.selector.container) @@ -175,6 +175,7 @@ impl FileFinder { .with_child( Flexible::new( 1.0, + false, Flex::column() .with_child( Label::new(file_name.to_string(), style.label.clone()) diff --git a/crates/gpui/src/elements.rs b/crates/gpui/src/elements.rs index c0b6cdd14391e6e8e6c7a628f04efc962e553e12..8e14cd89ce2a81225f73416360a917dfac36a3ed 100644 --- a/crates/gpui/src/elements.rs +++ b/crates/gpui/src/elements.rs @@ -130,11 +130,11 @@ pub trait Element { Container::new(self.boxed()) } - fn expanded(self, flex: f32) -> Expanded + fn flexible(self, flex: f32, expanded: bool) -> Flexible where Self: 'static + Sized, { - Expanded::new(flex, self.boxed()) + Flexible::new(flex, expanded, self.boxed()) } } diff --git a/crates/gpui/src/elements/flex.rs b/crates/gpui/src/elements/flex.rs index e2bd7eb1c97f8838d5acaf348fda08a748b15e54..6b884289a2a91bdca69af6724e585fe3f17a8918 100644 --- a/crates/gpui/src/elements/flex.rs +++ b/crates/gpui/src/elements/flex.rs @@ -228,88 +228,15 @@ struct FlexParentData { expanded: bool, } -pub struct Expanded { - metadata: FlexParentData, - child: ElementBox, -} - -impl Expanded { - pub fn new(flex: f32, child: ElementBox) -> Self { - Expanded { - metadata: FlexParentData { - flex, - expanded: true, - }, - child, - } - } -} - -impl Element for Expanded { - type LayoutState = (); - type PaintState = (); - - fn layout( - &mut self, - constraint: SizeConstraint, - cx: &mut LayoutContext, - ) -> (Vector2F, Self::LayoutState) { - let size = self.child.layout(constraint, cx); - (size, ()) - } - - fn paint( - &mut self, - bounds: RectF, - visible_bounds: RectF, - _: &mut Self::LayoutState, - cx: &mut PaintContext, - ) -> Self::PaintState { - self.child.paint(bounds.origin(), visible_bounds, cx) - } - - fn dispatch_event( - &mut self, - event: &Event, - _: RectF, - _: &mut Self::LayoutState, - _: &mut Self::PaintState, - cx: &mut EventContext, - ) -> bool { - self.child.dispatch_event(event, cx) - } - - fn metadata(&self) -> Option<&dyn Any> { - Some(&self.metadata) - } - - fn debug( - &self, - _: RectF, - _: &Self::LayoutState, - _: &Self::PaintState, - cx: &DebugContext, - ) -> Value { - json!({ - "type": "Expanded", - "flex": self.metadata.flex, - "child": self.child.debug(cx) - }) - } -} - pub struct Flexible { metadata: FlexParentData, child: ElementBox, } impl Flexible { - pub fn new(flex: f32, child: ElementBox) -> Self { + pub fn new(flex: f32, expanded: bool, child: ElementBox) -> Self { Flexible { - metadata: FlexParentData { - flex, - expanded: false, - }, + metadata: FlexParentData { flex, expanded }, child, } } diff --git a/crates/theme_selector/src/theme_selector.rs b/crates/theme_selector/src/theme_selector.rs index 4d226c3880b152f7b20f7ef39297120de1a1d665..df7713ad1fa947427d55d356b00f7347f7ef4276 100644 --- a/crates/theme_selector/src/theme_selector.rs +++ b/crates/theme_selector/src/theme_selector.rs @@ -293,7 +293,7 @@ impl View for ThemeSelector { Container::new( Flex::new(Axis::Vertical) .with_child(ChildView::new(self.query_editor.id()).boxed()) - .with_child(Flexible::new(1.0, self.render_matches(cx)).boxed()) + .with_child(Flexible::new(1.0, false, self.render_matches(cx)).boxed()) .boxed(), ) .with_style(settings.theme.selector.container) diff --git a/crates/workspace/src/pane.rs b/crates/workspace/src/pane.rs index 5a3411c9268c254f811075cf11397f9e73079a8b..2ef586a8fb31e316327f465471b2f17a8063e36f 100644 --- a/crates/workspace/src/pane.rs +++ b/crates/workspace/src/pane.rs @@ -314,13 +314,11 @@ impl Pane { } row.add_child( - Expanded::new( - 0.0, - Container::new(Empty::new().boxed()) - .with_border(theme.workspace.tab.container.border) - .boxed(), - ) - .named("filler"), + Empty::new() + .contained() + .with_border(theme.workspace.tab.container.border) + .flexible(0., true) + .named("filler"), ); row.boxed() @@ -345,7 +343,7 @@ impl View for Pane { if let Some(active_item) = self.active_item() { Flex::column() .with_child(self.render_tabs(cx)) - .with_child(Expanded::new(1.0, ChildView::new(active_item.id()).boxed()).boxed()) + .with_child(ChildView::new(active_item.id()).flexible(1., true).boxed()) .named("pane") } else { Empty::new().named("pane") diff --git a/crates/workspace/src/pane_group.rs b/crates/workspace/src/pane_group.rs index f61fd6f1af77c3c46cfe9d30e92db1062997b896..a2b3803b85b5354d6e4266a368f42db936b0c5eb 100644 --- a/crates/workspace/src/pane_group.rs +++ b/crates/workspace/src/pane_group.rs @@ -183,7 +183,7 @@ impl PaneAxis { member = Container::new(member).with_border(border).boxed(); } - Expanded::new(1.0, member).boxed() + Flexible::new(1.0, true, member).boxed() })) .boxed() } diff --git a/crates/workspace/src/sidebar.rs b/crates/workspace/src/sidebar.rs index ea3b3cb920b0c6acf7384bab97cd8b58ec072770..8d3d0f63709b895d720db31a921602bae01a95ca 100644 --- a/crates/workspace/src/sidebar.rs +++ b/crates/workspace/src/sidebar.rs @@ -135,19 +135,16 @@ impl Sidebar { } container.add_child( - Flexible::new( - 1., - Hook::new( - ConstrainedBox::new(ChildView::new(active_item.id()).boxed()) - .with_max_width(*self.width.borrow()) - .boxed(), - ) - .on_after_layout({ - let width = self.width.clone(); - move |size, _| *width.borrow_mut() = size.x() - }) - .boxed(), + Hook::new( + ConstrainedBox::new(ChildView::new(active_item.id()).boxed()) + .with_max_width(*self.width.borrow()) + .boxed(), ) + .on_after_layout({ + let width = self.width.clone(); + move |size, _| *width.borrow_mut() = size.x() + }) + .flexible(1., false) .boxed(), ); if matches!(self.side, Side::Left) { diff --git a/crates/workspace/src/status_bar.rs b/crates/workspace/src/status_bar.rs index 970078dcb50dfa78a2913107a8029dd492230957..162394ed00edd9740b90641bb060ecb471c33491 100644 --- a/crates/workspace/src/status_bar.rs +++ b/crates/workspace/src/status_bar.rs @@ -47,7 +47,7 @@ impl View for StatusBar { .iter() .map(|i| ChildView::new(i.id()).aligned().boxed()), ) - .with_child(Empty::new().expanded(1.).boxed()) + .with_child(Empty::new().flexible(1., true).boxed()) .with_children( self.right_items .iter() diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 69a7004c443b79e4c2e91523437a52e3f9e445d8..6bb6da215778bd69eb3c98e7e1d35be776d14e06 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -1191,50 +1191,40 @@ impl View for Workspace { fn render(&mut self, cx: &mut RenderContext) -> ElementBox { let settings = self.settings.borrow(); let theme = &settings.theme; - Container::new( - Flex::column() - .with_child(self.render_titlebar(&theme, cx)) - .with_child( - Expanded::new( - 1.0, - Stack::new() - .with_child({ - let mut content = Flex::row(); - content.add_child(self.left_sidebar.render(&settings, cx)); - if let Some(element) = - self.left_sidebar.render_active_item(&settings, cx) - { - content.add_child(Flexible::new(0.8, element).boxed()); - } - content.add_child( - Flex::column() - .with_child( - Expanded::new(1.0, self.center.render(&settings.theme)) - .boxed(), - ) - .with_child(ChildView::new(self.status_bar.id()).boxed()) - .expanded(1.) + Flex::column() + .with_child(self.render_titlebar(&theme, cx)) + .with_child( + Stack::new() + .with_child({ + let mut content = Flex::row(); + content.add_child(self.left_sidebar.render(&settings, cx)); + if let Some(element) = self.left_sidebar.render_active_item(&settings, cx) { + content.add_child(Flexible::new(0.8, false, element).boxed()); + } + content.add_child( + Flex::column() + .with_child( + Flexible::new(1., true, self.center.render(&settings.theme)) .boxed(), - ); - if let Some(element) = - self.right_sidebar.render_active_item(&settings, cx) - { - content.add_child(Flexible::new(0.8, element).boxed()); - } - content.add_child(self.right_sidebar.render(&settings, cx)); - content.boxed() - }) - .with_children( - self.modal.as_ref().map(|m| ChildView::new(m.id()).boxed()), - ) - .boxed(), - ) + ) + .with_child(ChildView::new(self.status_bar.id()).boxed()) + .flexible(1., true) + .boxed(), + ); + if let Some(element) = self.right_sidebar.render_active_item(&settings, cx) + { + content.add_child(Flexible::new(0.8, false, element).boxed()); + } + content.add_child(self.right_sidebar.render(&settings, cx)); + content.boxed() + }) + .with_children(self.modal.as_ref().map(|m| ChildView::new(m.id()).boxed())) + .flexible(1.0, true) .boxed(), - ) - .boxed(), - ) - .with_background_color(settings.theme.workspace.background) - .named("workspace") + ) + .contained() + .with_background_color(settings.theme.workspace.background) + .named("workspace") } fn on_focus(&mut self, cx: &mut ViewContext) { From 2b36ab0de7a69a382b85d74d7641e377b5dace9e Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Thu, 6 Jan 2022 17:35:45 -0800 Subject: [PATCH 26/43] Introduce Expanded element Co-Authored-By: Nathan Sobo --- crates/gpui/src/elements.rs | 9 +++ crates/gpui/src/elements/expanded.rs | 90 ++++++++++++++++++++++++++++ 2 files changed, 99 insertions(+) create mode 100644 crates/gpui/src/elements/expanded.rs diff --git a/crates/gpui/src/elements.rs b/crates/gpui/src/elements.rs index 8e14cd89ce2a81225f73416360a917dfac36a3ed..68309239533a338db501b389ab67a881b5f3e9e9 100644 --- a/crates/gpui/src/elements.rs +++ b/crates/gpui/src/elements.rs @@ -4,6 +4,7 @@ mod constrained_box; mod container; mod empty; mod event_handler; +mod expanded; mod flex; mod hook; mod image; @@ -16,6 +17,7 @@ mod svg; mod text; mod uniform_list; +use self::expanded::Expanded; pub use self::{ align::*, canvas::*, constrained_box::*, container::*, empty::*, event_handler::*, flex::*, hook::*, image::*, label::*, list::*, mouse_event_handler::*, overlay::*, stack::*, svg::*, @@ -130,6 +132,13 @@ pub trait Element { Container::new(self.boxed()) } + fn expanded(self) -> Expanded + where + Self: 'static + Sized, + { + Expanded::new(self.boxed()) + } + fn flexible(self, flex: f32, expanded: bool) -> Flexible where Self: 'static + Sized, diff --git a/crates/gpui/src/elements/expanded.rs b/crates/gpui/src/elements/expanded.rs new file mode 100644 index 0000000000000000000000000000000000000000..cbeef598da649cd397625c482ab3cfb80a68cb9c --- /dev/null +++ b/crates/gpui/src/elements/expanded.rs @@ -0,0 +1,90 @@ +use crate::{ + geometry::{rect::RectF, vector::Vector2F}, + json, DebugContext, Element, ElementBox, Event, EventContext, LayoutContext, PaintContext, + SizeConstraint, +}; +use serde_json::json; + +pub struct Expanded { + child: ElementBox, + full_width: bool, + full_height: bool, +} + +impl Expanded { + pub fn new(child: ElementBox) -> Self { + Self { + child, + full_width: true, + full_height: true, + } + } + + pub fn to_full_width(mut self) -> Self { + self.full_width = true; + self.full_height = false; + self + } + + pub fn to_full_height(mut self) -> Self { + self.full_width = false; + self.full_height = true; + self + } +} + +impl Element for Expanded { + type LayoutState = (); + type PaintState = (); + + fn layout( + &mut self, + mut constraint: SizeConstraint, + cx: &mut LayoutContext, + ) -> (Vector2F, Self::LayoutState) { + if self.full_width { + constraint.min.set_x(constraint.max.x()); + } + if self.full_height { + constraint.min.set_y(constraint.max.y()); + } + let size = self.child.layout(constraint, cx); + (size, ()) + } + + fn paint( + &mut self, + bounds: RectF, + visible_bounds: RectF, + _: &mut Self::LayoutState, + cx: &mut PaintContext, + ) -> Self::PaintState { + self.child.paint(bounds.origin(), visible_bounds, cx); + } + + fn dispatch_event( + &mut self, + event: &Event, + _: RectF, + _: &mut Self::LayoutState, + _: &mut Self::PaintState, + cx: &mut EventContext, + ) -> bool { + self.child.dispatch_event(event, cx) + } + + fn debug( + &self, + _: RectF, + _: &Self::LayoutState, + _: &Self::PaintState, + cx: &DebugContext, + ) -> json::Value { + json!({ + "type": "Expanded", + "full_width": self.full_width, + "full_height": self.full_height, + "child": self.child.debug(cx) + }) + } +} From 94e9c7fd5b25599876bd665985b173a8b4c26544 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Thu, 6 Jan 2022 17:55:56 -0800 Subject: [PATCH 27/43] Give a full-width background to the diagnostic headers --- crates/diagnostics/src/diagnostics.rs | 2 +- crates/editor/src/editor.rs | 8 +++++++- crates/theme/src/theme.rs | 2 ++ crates/zed/assets/themes/_base.toml | 22 +++++++++++++++++----- 4 files changed, 27 insertions(+), 7 deletions(-) diff --git a/crates/diagnostics/src/diagnostics.rs b/crates/diagnostics/src/diagnostics.rs index dfaac9f150e02bcdaea36db7ed610f7d418bb8bc..f6cc65981800cdc46af4aa5663adfabafaa13ea7 100644 --- a/crates/diagnostics/src/diagnostics.rs +++ b/crates/diagnostics/src/diagnostics.rs @@ -289,7 +289,7 @@ impl ProjectDiagnosticsEditor { diagnostic_blocks.push(DiagnosticBlock::Header(header.clone())); blocks_to_add.push(BlockProperties { position: header_position, - height: 2, + height: 3, render: diagnostic_header_renderer( buffer.clone(), header, diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 4d3aa058be64589e46164b7480ba6104c7234ccc..b9c57de1308b25ec0cf9999a5d29a240179371fb 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -3855,7 +3855,8 @@ pub fn diagnostic_header_renderer( Arc::new(move |cx| { let settings = build_settings(cx); let mut text_style = settings.style.text.clone(); - text_style.color = diagnostic_style(diagnostic.severity, is_valid, &settings.style).text; + let diagnostic_style = diagnostic_style(diagnostic.severity, is_valid, &settings.style); + text_style.color = diagnostic_style.text; let file_path = if let Some(file) = buffer.read(&**cx).file() { file.path().to_string_lossy().to_string() } else { @@ -3869,6 +3870,11 @@ pub fn diagnostic_header_renderer( .boxed(), ) .with_child(Label::new(file_path, settings.style.text.clone()).boxed()) + .aligned() + .left() + .contained() + .with_style(diagnostic_style.header) + .expanded() .boxed() }) } diff --git a/crates/theme/src/theme.rs b/crates/theme/src/theme.rs index 6a67789afe114e2b19c288259ba08ed5c7a099f4..e91f0addb7836b4f950bae9707b2781b8033567f 100644 --- a/crates/theme/src/theme.rs +++ b/crates/theme/src/theme.rs @@ -261,6 +261,8 @@ pub struct EditorStyle { #[derive(Copy, Clone, Deserialize, Default)] pub struct DiagnosticStyle { pub text: Color, + #[serde(default)] + pub header: ContainerStyle, } #[derive(Clone, Copy, Default, Deserialize)] diff --git a/crates/zed/assets/themes/_base.toml b/crates/zed/assets/themes/_base.toml index 4063fc92f95a7c95351af9eafb69ab78c0dc7d85..2fa2bde5454d61e8446702744d6d86947c9c9ff9 100644 --- a/crates/zed/assets/themes/_base.toml +++ b/crates/zed/assets/themes/_base.toml @@ -185,7 +185,7 @@ corner_radius = 6 [project_panel] extends = "$panel" -padding.top = 6 # ($workspace.tab.height - $project_panel.entry.height) / 2 +padding.top = 6 # ($workspace.tab.height - $project_panel.entry.height) / 2 [project_panel.entry] text = "$text.1" @@ -249,15 +249,27 @@ line_number_active = "$text.0.color" selection = "$selection.host" guest_selections = "$selection.guests" error_color = "$status.bad" -error_diagnostic = { text = "$status.bad" } invalid_error_diagnostic = { text = "$text.3.color" } -warning_diagnostic = { text = "$status.warn" } invalid_warning_diagnostic = { text = "$text.3.color" } -information_diagnostic = { text = "$status.info" } invalid_information_diagnostic = { text = "$text.3.color" } -hint_diagnostic = { text = "$status.info" } invalid_hint_diagnostic = { text = "$text.3.color" } +[editor.error_diagnostic] +text = "$status.bad" +header = { padding = { left = 10 }, background = "#ffffff08" } + +[editor.warning_diagnostic] +text = "$status.warn" +header = { padding = { left = 10 }, background = "#ffffff08" } + +[editor.information_diagnostic] +text = "$status.info" +header = { padding = { left = 10 }, background = "#ffffff08" } + +[editor.hint_diagnostic] +text = "$status.info" +header = { padding = { left = 10 }, background = "#ffffff08" } + [project_diagnostics] background = "$surface.1" empty_message = "$text.0" From 67f672d0cc2209903d5a34c5877fc0a2f65d3633 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Fri, 7 Jan 2022 10:19:28 +0100 Subject: [PATCH 28/43] Clear selections on other excerpted buffers when setting active selections --- crates/editor/src/multi_buffer.rs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/crates/editor/src/multi_buffer.rs b/crates/editor/src/multi_buffer.rs index f399a53412fa3db6e81b88002739199d7ac8b392..ed67491e24b4e7ba82dd270272920aa07a38d828 100644 --- a/crates/editor/src/multi_buffer.rs +++ b/crates/editor/src/multi_buffer.rs @@ -496,6 +496,14 @@ impl MultiBuffer { } } + for (buffer_id, buffer_state) in self.buffers.borrow().iter() { + if !selections_by_buffer.contains_key(buffer_id) { + buffer_state + .buffer + .update(cx, |buffer, cx| buffer.remove_active_selections(cx)); + } + } + for (buffer_id, mut selections) in selections_by_buffer { self.buffers.borrow()[&buffer_id] .buffer From 089542c6f4c8132fb8f095c21c5a735b745c5355 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Fri, 7 Jan 2022 10:33:21 +0100 Subject: [PATCH 29/43] Avoid removing diagnostics from `Worktree` after opening a buffer This allows re-opening the same buffer and supplying the previous diagnostics. --- crates/project/src/worktree.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/project/src/worktree.rs b/crates/project/src/worktree.rs index 6108e9eb108a0f170e5056702b00db78a1f0c166..843a775993ccc1b4f9a9486ba2f4953c6a2c4cac 100644 --- a/crates/project/src/worktree.rs +++ b/crates/project/src/worktree.rs @@ -1267,7 +1267,7 @@ impl LocalWorktree { let (diagnostics, language, language_server) = this.update(&mut cx, |this, cx| { let this = this.as_local_mut().unwrap(); - let diagnostics = this.diagnostics.remove(&path); + let diagnostics = this.diagnostics.get(&path).cloned(); let language = this .language_registry .select_language(file.full_path()) From 56496c25853184a45dc1b4453552115617557660 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Fri, 7 Jan 2022 13:38:20 +0100 Subject: [PATCH 30/43] Move back `diagnostic_summaries` into `Worktree` This fixes an issue where updating the snapshot's entries would override the diagnostic summaries received on the remote side. Co-Authored-By: Nathan Sobo --- crates/project/src/worktree.rs | 39 +++++++++++++--------------------- 1 file changed, 15 insertions(+), 24 deletions(-) diff --git a/crates/project/src/worktree.rs b/crates/project/src/worktree.rs index 843a775993ccc1b4f9a9486ba2f4953c6a2c4cac..350908d0d53677c5bcb5b89cbe25a4344b49715d 100644 --- a/crates/project/src/worktree.rs +++ b/crates/project/src/worktree.rs @@ -34,7 +34,6 @@ use std::{ ffi::{OsStr, OsString}, fmt, future::Future, - mem, ops::{Deref, Range}, path::{Path, PathBuf}, sync::{ @@ -197,7 +196,6 @@ impl Worktree { entries_by_id, removed_entry_ids: Default::default(), next_entry_id: Default::default(), - diagnostic_summaries, }; let (updates_tx, mut updates_rx) = postage::mpsc::channel(64); @@ -241,6 +239,7 @@ impl Worktree { queued_operations: Default::default(), languages, user_store, + diagnostic_summaries, }) }) }); @@ -584,9 +583,7 @@ impl Worktree { match self { Self::Local(worktree) => { let is_fake_fs = worktree.fs.is_fake(); - worktree - .snapshot - .assign(worktree.background_snapshot.lock().clone()); + worktree.snapshot = worktree.background_snapshot.lock().clone(); if worktree.is_scanning() { if worktree.poll_task.is_none() { worktree.poll_task = Some(cx.spawn(|this, mut cx| async move { @@ -828,8 +825,7 @@ impl Worktree { let this = self.as_local_mut().unwrap(); let summary = DiagnosticSummary::new(&diagnostics); - this.snapshot - .diagnostic_summaries + this.diagnostic_summaries .insert(PathKey(worktree_path.clone()), summary.clone()); this.diagnostics.insert(worktree_path.clone(), diagnostics); @@ -943,7 +939,6 @@ pub struct Snapshot { entries_by_id: SumTree, removed_entry_ids: HashMap, next_entry_id: Arc, - diagnostic_summaries: TreeMap, } pub struct LocalWorktree { @@ -958,6 +953,7 @@ pub struct LocalWorktree { open_buffers: HashMap>, shared_buffers: HashMap>>, diagnostics: HashMap, Vec>>, + diagnostic_summaries: TreeMap, queued_operations: Vec<(u64, Operation)>, language_registry: Arc, client: Arc, @@ -984,6 +980,7 @@ pub struct RemoteWorktree { languages: Arc, user_store: ModelHandle, queued_operations: Vec<(u64, Operation)>, + diagnostic_summaries: TreeMap, } type LoadingBuffers = HashMap< @@ -1038,7 +1035,6 @@ impl LocalWorktree { entries_by_id: Default::default(), removed_entry_ids: Default::default(), next_entry_id: Arc::new(next_entry_id), - diagnostic_summaries: Default::default(), }; if let Some(metadata) = metadata { snapshot.insert_entry( @@ -1064,6 +1060,7 @@ impl LocalWorktree { open_buffers: Default::default(), shared_buffers: Default::default(), diagnostics: Default::default(), + diagnostic_summaries: Default::default(), queued_operations: Default::default(), language_registry: languages, client, @@ -1502,10 +1499,11 @@ impl LocalWorktree { }) .detach(); + let diagnostic_summaries = self.diagnostic_summaries.clone(); let share_message = cx.background().spawn(async move { proto::ShareWorktree { project_id, - worktree: Some(snapshot.to_proto()), + worktree: Some(snapshot.to_proto(&diagnostic_summaries)), } }); @@ -1680,7 +1678,7 @@ impl RemoteWorktree { ) { if let Some(summary) = envelope.payload.summary { let path: Arc = Path::new(&summary.path).into(); - self.snapshot.diagnostic_summaries.insert( + self.diagnostic_summaries.insert( PathKey(path.clone()), DiagnosticSummary { error_count: summary.error_count as usize, @@ -1726,7 +1724,10 @@ impl Snapshot { self.id } - pub fn to_proto(&self) -> proto::Worktree { + pub fn to_proto( + &self, + diagnostic_summaries: &TreeMap, + ) -> proto::Worktree { let root_name = self.root_name.clone(); proto::Worktree { id: self.id.0 as u64, @@ -1737,8 +1738,7 @@ impl Snapshot { .filter(|e| !e.is_ignored) .map(Into::into) .collect(), - diagnostic_summaries: self - .diagnostic_summaries + diagnostic_summaries: diagnostic_summaries .iter() .map(|(path, summary)| summary.to_proto(path.0.clone())) .collect(), @@ -1845,14 +1845,6 @@ impl Snapshot { Ok(()) } - fn assign(&mut self, mut other: Self) { - mem::swap( - &mut self.diagnostic_summaries, - &mut other.diagnostic_summaries, - ); - *self = other; - } - pub fn file_count(&self) -> usize { self.entries_by_path.summary().file_count } @@ -3358,7 +3350,7 @@ mod tests { let remote = Worktree::remote( 1, 1, - initial_snapshot.to_proto(), + initial_snapshot.to_proto(&Default::default()), Client::new(http_client.clone()), user_store, Default::default(), @@ -4205,7 +4197,6 @@ mod tests { root_name: Default::default(), root_char_bag: Default::default(), next_entry_id: next_entry_id.clone(), - diagnostic_summaries: Default::default(), }; initial_snapshot.insert_entry( Entry::new( From e39be35e174a8449e1a5d0ebf20150fce8160afe Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Fri, 7 Jan 2022 14:06:05 +0100 Subject: [PATCH 31/43] Show status bar item for project diagnostic summary Co-Authored-By: Nathan Sobo --- crates/diagnostics/src/diagnostics.rs | 10 ++-- crates/diagnostics/src/items.rs | 71 +++++++++++++++++++++++++++ crates/project/src/project.rs | 11 +++++ crates/theme/src/theme.rs | 1 + crates/zed/assets/themes/_base.toml | 3 +- crates/zed/src/zed.rs | 12 ++++- 6 files changed, 101 insertions(+), 7 deletions(-) create mode 100644 crates/diagnostics/src/items.rs diff --git a/crates/diagnostics/src/diagnostics.rs b/crates/diagnostics/src/diagnostics.rs index f6cc65981800cdc46af4aa5663adfabafaa13ea7..c7f1f9b86871d499a7f90861b6d9fb1c9aef67a2 100644 --- a/crates/diagnostics/src/diagnostics.rs +++ b/crates/diagnostics/src/diagnostics.rs @@ -1,3 +1,5 @@ +pub mod items; + use anyhow::Result; use collections::{HashMap, HashSet}; use editor::{ @@ -16,13 +18,13 @@ use std::{cmp::Ordering, ops::Range, path::Path, sync::Arc}; use util::TryFutureExt; use workspace::Workspace; -action!(Toggle); +action!(Deploy); const CONTEXT_LINE_COUNT: u32 = 1; pub fn init(cx: &mut MutableAppContext) { - cx.add_bindings([Binding::new("alt-shift-D", Toggle, None)]); - cx.add_action(ProjectDiagnosticsEditor::toggle); + cx.add_bindings([Binding::new("alt-shift-D", Deploy, None)]); + cx.add_action(ProjectDiagnosticsEditor::deploy); } type Event = editor::Event; @@ -148,7 +150,7 @@ impl ProjectDiagnosticsEditor { self.editor.read(cx).text(cx) } - fn toggle(workspace: &mut Workspace, _: &Toggle, cx: &mut ViewContext) { + fn deploy(workspace: &mut Workspace, _: &Deploy, cx: &mut ViewContext) { let diagnostics = cx.add_model(|_| ProjectDiagnostics::new(workspace.project().clone())); workspace.add_item(diagnostics, cx); } diff --git a/crates/diagnostics/src/items.rs b/crates/diagnostics/src/items.rs new file mode 100644 index 0000000000000000000000000000000000000000..ae54b9879b7cba9e16b38772592a1e603df88088 --- /dev/null +++ b/crates/diagnostics/src/items.rs @@ -0,0 +1,71 @@ +use gpui::{ + elements::*, platform::CursorStyle, Entity, ModelHandle, RenderContext, View, ViewContext, +}; +use postage::watch; +use project::Project; +use workspace::{Settings, StatusItemView}; + +pub struct DiagnosticSummary { + settings: watch::Receiver, + summary: project::DiagnosticSummary, +} + +impl DiagnosticSummary { + pub fn new( + project: &ModelHandle, + settings: watch::Receiver, + cx: &mut ViewContext, + ) -> Self { + cx.subscribe(project, |this, project, event, cx| { + if let project::Event::DiskBasedDiagnosticsUpdated { .. } = event { + this.summary = project.read(cx).diagnostic_summary(cx); + cx.notify(); + } + }) + .detach(); + Self { + settings, + summary: project.read(cx).diagnostic_summary(cx), + } + } +} + +impl Entity for DiagnosticSummary { + type Event = (); +} + +impl View for DiagnosticSummary { + fn ui_name() -> &'static str { + "DiagnosticSummary" + } + + fn render(&mut self, cx: &mut RenderContext) -> ElementBox { + enum Tag {} + + let theme = &self.settings.borrow().theme.project_diagnostics; + MouseEventHandler::new::(0, cx, |_, _| { + Label::new( + format!( + "Errors: {}, Warnings: {}", + self.summary.error_count, self.summary.warning_count + ), + theme.status_bar_item.text.clone(), + ) + .contained() + .with_style(theme.status_bar_item.container) + .boxed() + }) + .with_cursor_style(CursorStyle::PointingHand) + .on_click(|cx| cx.dispatch_action(crate::Deploy)) + .boxed() + } +} + +impl StatusItemView for DiagnosticSummary { + fn set_active_pane_item( + &mut self, + _: Option<&dyn workspace::ItemViewHandle>, + _: &mut ViewContext, + ) { + } +} diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 325ac682ebfabcef2c291d3ac31862fceb798347..c1da5aa8424aac28c0a57b1b4a16741782051507 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -539,6 +539,17 @@ impl Project { } } + pub fn diagnostic_summary(&self, cx: &AppContext) -> DiagnosticSummary { + let mut summary = DiagnosticSummary::default(); + for (_, path_summary) in self.diagnostic_summaries(cx) { + summary.error_count += path_summary.error_count; + summary.warning_count += path_summary.warning_count; + summary.info_count += path_summary.info_count; + summary.hint_count += path_summary.hint_count; + } + summary + } + pub fn diagnostic_summaries<'a>( &'a self, cx: &'a AppContext, diff --git a/crates/theme/src/theme.rs b/crates/theme/src/theme.rs index e91f0addb7836b4f950bae9707b2781b8033567f..1ba2fc8faf85e1f83fbe088d8f66ac693ec8c635 100644 --- a/crates/theme/src/theme.rs +++ b/crates/theme/src/theme.rs @@ -232,6 +232,7 @@ pub struct ProjectDiagnostics { #[serde(flatten)] pub container: ContainerStyle, pub empty_message: TextStyle, + pub status_bar_item: ContainedText, } #[derive(Clone, Deserialize, Default)] diff --git a/crates/zed/assets/themes/_base.toml b/crates/zed/assets/themes/_base.toml index 2fa2bde5454d61e8446702744d6d86947c9c9ff9..a8a5694978aca9d9207f1bd2a84f9b11fb4cf695 100644 --- a/crates/zed/assets/themes/_base.toml +++ b/crates/zed/assets/themes/_base.toml @@ -185,7 +185,7 @@ corner_radius = 6 [project_panel] extends = "$panel" -padding.top = 6 # ($workspace.tab.height - $project_panel.entry.height) / 2 +padding.top = 6 # ($workspace.tab.height - $project_panel.entry.height) / 2 [project_panel.entry] text = "$text.1" @@ -273,3 +273,4 @@ header = { padding = { left = 10 }, background = "#ffffff08" } [project_diagnostics] background = "$surface.1" empty_message = "$text.0" +status_bar_item = { extends = "$text.2", margin.right = 10 } diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index 1483982b601ed2bbc1f45becf5df756c9e177204..00046689398091174b771cfb297dfc72531fedb6 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -88,12 +88,20 @@ pub fn build_workspace( .into(), ); - let diagnostic = + let diagnostic_message = cx.add_view(|_| editor::items::DiagnosticMessage::new(app_state.settings.clone())); + let diagnostic_summary = cx.add_view(|cx| { + diagnostics::items::DiagnosticSummary::new( + workspace.project(), + app_state.settings.clone(), + cx, + ) + }); let cursor_position = cx.add_view(|_| editor::items::CursorPosition::new(app_state.settings.clone())); workspace.status_bar().update(cx, |status_bar, cx| { - status_bar.add_left_item(diagnostic, cx); + status_bar.add_left_item(diagnostic_summary, cx); + status_bar.add_left_item(diagnostic_message, cx); status_bar.add_right_item(cursor_position, cx); }); From cf62d26ed8a3bbd9a0abeb534cf1c10ee674d01b Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Fri, 7 Jan 2022 15:03:19 +0100 Subject: [PATCH 32/43] Display a "Checking..." message when running disk-based diagnostics Co-Authored-By: Nathan Sobo --- crates/diagnostics/src/items.rs | 40 +++++++++---- crates/project/src/project.rs | 46 ++++++++++++++- crates/project/src/worktree.rs | 101 ++++++++++++++++++++++++-------- crates/rpc/proto/zed.proto | 54 +++++++++-------- crates/rpc/src/proto.rs | 2 + crates/server/src/rpc.rs | 17 ++++++ 6 files changed, 200 insertions(+), 60 deletions(-) diff --git a/crates/diagnostics/src/items.rs b/crates/diagnostics/src/items.rs index ae54b9879b7cba9e16b38772592a1e603df88088..072738fa77417542fbc7cdfa1036f8d55418c6e8 100644 --- a/crates/diagnostics/src/items.rs +++ b/crates/diagnostics/src/items.rs @@ -3,11 +3,13 @@ use gpui::{ }; use postage::watch; use project::Project; +use std::fmt::Write; use workspace::{Settings, StatusItemView}; pub struct DiagnosticSummary { settings: watch::Receiver, summary: project::DiagnosticSummary, + in_progress: bool, } impl DiagnosticSummary { @@ -16,16 +18,26 @@ impl DiagnosticSummary { settings: watch::Receiver, cx: &mut ViewContext, ) -> Self { - cx.subscribe(project, |this, project, event, cx| { - if let project::Event::DiskBasedDiagnosticsUpdated { .. } = event { + cx.subscribe(project, |this, project, event, cx| match event { + project::Event::DiskBasedDiagnosticsUpdated { .. } => { this.summary = project.read(cx).diagnostic_summary(cx); cx.notify(); } + project::Event::DiskBasedDiagnosticsStarted => { + this.in_progress = true; + cx.notify(); + } + project::Event::DiskBasedDiagnosticsFinished => { + this.in_progress = false; + cx.notify(); + } + _ => {} }) .detach(); Self { settings, summary: project.read(cx).diagnostic_summary(cx), + in_progress: project.read(cx).is_running_disk_based_diagnostics(), } } } @@ -43,17 +55,21 @@ impl View for DiagnosticSummary { enum Tag {} let theme = &self.settings.borrow().theme.project_diagnostics; + let mut message = String::new(); + if self.in_progress { + message.push_str("Checking... "); + } + write!( + message, + "Errors: {}, Warnings: {}", + self.summary.error_count, self.summary.warning_count + ) + .unwrap(); MouseEventHandler::new::(0, cx, |_, _| { - Label::new( - format!( - "Errors: {}, Warnings: {}", - self.summary.error_count, self.summary.warning_count - ), - theme.status_bar_item.text.clone(), - ) - .contained() - .with_style(theme.status_bar_item.container) - .boxed() + Label::new(message, theme.status_bar_item.text.clone()) + .contained() + .with_style(theme.status_bar_item.container) + .boxed() }) .with_cursor_style(CursorStyle::PointingHand) .on_click(|cx| cx.dispatch_action(crate::Deploy)) diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index c1da5aa8424aac28c0a57b1b4a16741782051507..62ecf94b7edff08c012c27bfd6de21ad4fc7eb77 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -33,6 +33,7 @@ pub struct Project { client_state: ProjectClientState, collaborators: HashMap, subscriptions: Vec, + pending_disk_based_diagnostics: isize, } enum ProjectClientState { @@ -60,7 +61,9 @@ pub struct Collaborator { pub enum Event { ActiveEntryChanged(Option), WorktreeRemoved(WorktreeId), + DiskBasedDiagnosticsStarted, DiskBasedDiagnosticsUpdated { worktree_id: WorktreeId }, + DiskBasedDiagnosticsFinished, DiagnosticsUpdated(ProjectPath), } @@ -187,6 +190,7 @@ impl Project { client, user_store, fs, + pending_disk_based_diagnostics: 0, } }) } @@ -259,6 +263,11 @@ impl Project { cx, Self::handle_update_diagnostic_summary, ), + client.subscribe_to_entity( + remote_id, + cx, + Self::handle_disk_based_diagnostics_updating, + ), client.subscribe_to_entity( remote_id, cx, @@ -273,6 +282,7 @@ impl Project { remote_id, replica_id, }, + pending_disk_based_diagnostics: 0, }; for worktree in worktrees { this.add_worktree(worktree, cx); @@ -506,17 +516,29 @@ impl Project { fn add_worktree(&mut self, worktree: ModelHandle, cx: &mut ModelContext) { cx.observe(&worktree, |_, _, cx| cx.notify()).detach(); - cx.subscribe(&worktree, |_, worktree, event, cx| match event { + cx.subscribe(&worktree, move |this, worktree, event, cx| match event { worktree::Event::DiagnosticsUpdated(path) => { cx.emit(Event::DiagnosticsUpdated(ProjectPath { worktree_id: worktree.read(cx).id(), path: path.clone(), })); } + worktree::Event::DiskBasedDiagnosticsUpdating => { + if this.pending_disk_based_diagnostics == 0 { + cx.emit(Event::DiskBasedDiagnosticsStarted); + } + this.pending_disk_based_diagnostics += 1; + } worktree::Event::DiskBasedDiagnosticsUpdated => { + this.pending_disk_based_diagnostics -= 1; cx.emit(Event::DiskBasedDiagnosticsUpdated { worktree_id: worktree.read(cx).id(), }); + if this.pending_disk_based_diagnostics == 0 { + if this.pending_disk_based_diagnostics == 0 { + cx.emit(Event::DiskBasedDiagnosticsFinished); + } + } } }) .detach(); @@ -539,6 +561,10 @@ impl Project { } } + pub fn is_running_disk_based_diagnostics(&self) -> bool { + self.pending_disk_based_diagnostics > 0 + } + pub fn diagnostic_summary(&self, cx: &AppContext) -> DiagnosticSummary { let mut summary = DiagnosticSummary::default(); for (_, path_summary) in self.diagnostic_summaries(cx) { @@ -716,6 +742,24 @@ impl Project { Ok(()) } + fn handle_disk_based_diagnostics_updating( + &mut self, + envelope: TypedEnvelope, + _: Arc, + cx: &mut ModelContext, + ) -> Result<()> { + let worktree_id = WorktreeId::from_proto(envelope.payload.worktree_id); + if let Some(worktree) = self.worktree_for_id(worktree_id, cx) { + worktree.update(cx, |worktree, cx| { + worktree + .as_remote() + .unwrap() + .disk_based_diagnostics_updating(cx); + }); + } + Ok(()) + } + fn handle_disk_based_diagnostics_updated( &mut self, envelope: TypedEnvelope, diff --git a/crates/project/src/worktree.rs b/crates/project/src/worktree.rs index 350908d0d53677c5bcb5b89cbe25a4344b49715d..46caf8cd93609718a605876348baf2a251551dc0 100644 --- a/crates/project/src/worktree.rs +++ b/crates/project/src/worktree.rs @@ -67,6 +67,7 @@ pub enum Worktree { #[derive(Clone, Debug, Eq, PartialEq)] pub enum Event { + DiskBasedDiagnosticsUpdating, DiskBasedDiagnosticsUpdated, DiagnosticsUpdated(Arc), } @@ -1133,6 +1134,11 @@ impl LocalWorktree { .log_err() .flatten() { + enum DiagnosticProgress { + Updating, + Updated, + } + let disk_based_sources = language .disk_based_diagnostic_sources() .cloned() @@ -1155,10 +1161,21 @@ impl LocalWorktree { while let Ok(diagnostics) = diagnostics_rx.recv().await { if let Some(handle) = cx.read(|cx| this.upgrade(cx)) { handle.update(&mut cx, |this, cx| { + if !has_disk_based_diagnostic_progress_token { + smol::block_on( + disk_based_diagnostics_done_tx + .send(DiagnosticProgress::Updating), + ) + .ok(); + } this.update_diagnostics(diagnostics, &disk_based_sources, cx) .log_err(); if !has_disk_based_diagnostic_progress_token { - smol::block_on(disk_based_diagnostics_done_tx.send(())).ok(); + smol::block_on( + disk_based_diagnostics_done_tx + .send(DiagnosticProgress::Updated), + ) + .ok(); } }) } else { @@ -1181,13 +1198,23 @@ impl LocalWorktree { match params.value { lsp::ProgressParamsValue::WorkDone(progress) => match progress { lsp::WorkDoneProgress::Begin(_) => { + if pending_disk_based_diagnostics == 0 { + smol::block_on( + disk_based_diagnostics_done_tx + .send(DiagnosticProgress::Updating), + ) + .ok(); + } pending_disk_based_diagnostics += 1; } lsp::WorkDoneProgress::End(_) => { pending_disk_based_diagnostics -= 1; if pending_disk_based_diagnostics == 0 { - smol::block_on(disk_based_diagnostics_done_tx.send(())) - .ok(); + smol::block_on( + disk_based_diagnostics_done_tx + .send(DiagnosticProgress::Updated), + ) + .ok(); } } _ => {} @@ -1198,21 +1225,41 @@ impl LocalWorktree { .detach(); let rpc = self.client.clone(); cx.spawn_weak(|this, mut cx| async move { - while let Ok(()) = disk_based_diagnostics_done_rx.recv().await { + while let Ok(progress) = disk_based_diagnostics_done_rx.recv().await { if let Some(handle) = cx.read(|cx| this.upgrade(cx)) { - let message = handle.update(&mut cx, |this, cx| { - cx.emit(Event::DiskBasedDiagnosticsUpdated); - let this = this.as_local().unwrap(); - this.share - .as_ref() - .map(|share| proto::DiskBasedDiagnosticsUpdated { - project_id: share.project_id, - worktree_id: this.id().to_proto(), - }) - }); - - if let Some(message) = message { - rpc.send(message).await.log_err(); + match progress { + DiagnosticProgress::Updating => { + let message = handle.update(&mut cx, |this, cx| { + cx.emit(Event::DiskBasedDiagnosticsUpdating); + let this = this.as_local().unwrap(); + this.share.as_ref().map(|share| { + proto::DiskBasedDiagnosticsUpdating { + project_id: share.project_id, + worktree_id: this.id().to_proto(), + } + }) + }); + + if let Some(message) = message { + rpc.send(message).await.log_err(); + } + } + DiagnosticProgress::Updated => { + let message = handle.update(&mut cx, |this, cx| { + cx.emit(Event::DiskBasedDiagnosticsUpdated); + let this = this.as_local().unwrap(); + this.share.as_ref().map(|share| { + proto::DiskBasedDiagnosticsUpdated { + project_id: share.project_id, + worktree_id: this.id().to_proto(), + } + }) + }); + + if let Some(message) = message { + rpc.send(message).await.log_err(); + } + } } } else { break; @@ -1691,6 +1738,10 @@ impl RemoteWorktree { } } + pub fn disk_based_diagnostics_updating(&self, cx: &mut ModelContext) { + cx.emit(Event::DiskBasedDiagnosticsUpdating); + } + pub fn disk_based_diagnostics_updated(&self, cx: &mut ModelContext) { cx.emit(Event::DiskBasedDiagnosticsUpdated); } @@ -3848,6 +3899,11 @@ mod tests { let mut events = subscribe(&tree, &mut cx); fake_server.start_progress(&progress_token).await; + assert_eq!( + events.next().await.unwrap(), + Event::DiskBasedDiagnosticsUpdating + ); + fake_server.start_progress(&progress_token).await; fake_server.end_progress(&progress_token).await; fake_server.start_progress(&progress_token).await; @@ -3864,18 +3920,17 @@ mod tests { }], }) .await; - - let event = events.next().await.unwrap(); assert_eq!( - event, + events.next().await.unwrap(), Event::DiagnosticsUpdated(Arc::from(Path::new("a.rs"))) ); fake_server.end_progress(&progress_token).await; fake_server.end_progress(&progress_token).await; - - let event = events.next().await.unwrap(); - assert_eq!(event, Event::DiskBasedDiagnosticsUpdated); + assert_eq!( + events.next().await.unwrap(), + Event::DiskBasedDiagnosticsUpdated + ); let buffer = tree .update(&mut cx, |tree, cx| tree.open_buffer("a.rs", cx)) diff --git a/crates/rpc/proto/zed.proto b/crates/rpc/proto/zed.proto index 669ffdbd7e50ae6f5a3579ee0db06cd55c29c312..f6300c44952a0935d3eb1f3d90ccc15062b8d9e9 100644 --- a/crates/rpc/proto/zed.proto +++ b/crates/rpc/proto/zed.proto @@ -26,30 +26,31 @@ message Envelope { ShareWorktree share_worktree = 19; UpdateWorktree update_worktree = 20; UpdateDiagnosticSummary update_diagnostic_summary = 21; - DiskBasedDiagnosticsUpdated disk_based_diagnostics_updated = 22; - - OpenBuffer open_buffer = 23; - OpenBufferResponse open_buffer_response = 24; - CloseBuffer close_buffer = 25; - UpdateBuffer update_buffer = 26; - SaveBuffer save_buffer = 27; - BufferSaved buffer_saved = 28; - - GetChannels get_channels = 29; - GetChannelsResponse get_channels_response = 30; - JoinChannel join_channel = 31; - JoinChannelResponse join_channel_response = 32; - LeaveChannel leave_channel = 33; - SendChannelMessage send_channel_message = 34; - SendChannelMessageResponse send_channel_message_response = 35; - ChannelMessageSent channel_message_sent = 36; - GetChannelMessages get_channel_messages = 37; - GetChannelMessagesResponse get_channel_messages_response = 38; - - UpdateContacts update_contacts = 39; - - GetUsers get_users = 40; - GetUsersResponse get_users_response = 41; + DiskBasedDiagnosticsUpdating disk_based_diagnostics_updating = 22; + DiskBasedDiagnosticsUpdated disk_based_diagnostics_updated = 23; + + OpenBuffer open_buffer = 24; + OpenBufferResponse open_buffer_response = 25; + CloseBuffer close_buffer = 26; + UpdateBuffer update_buffer = 27; + SaveBuffer save_buffer = 28; + BufferSaved buffer_saved = 29; + + GetChannels get_channels = 30; + GetChannelsResponse get_channels_response = 31; + JoinChannel join_channel = 32; + JoinChannelResponse join_channel_response = 33; + LeaveChannel leave_channel = 34; + SendChannelMessage send_channel_message = 35; + SendChannelMessageResponse send_channel_message_response = 36; + ChannelMessageSent channel_message_sent = 37; + GetChannelMessages get_channel_messages = 38; + GetChannelMessagesResponse get_channel_messages_response = 39; + + UpdateContacts update_contacts = 40; + + GetUsers get_users = 41; + GetUsersResponse get_users_response = 42; } } @@ -181,6 +182,11 @@ message DiagnosticSummary { uint32 hint_count = 7; } +message DiskBasedDiagnosticsUpdating { + uint64 project_id = 1; + uint64 worktree_id = 2; +} + message DiskBasedDiagnosticsUpdated { uint64 project_id = 1; uint64 worktree_id = 2; diff --git a/crates/rpc/src/proto.rs b/crates/rpc/src/proto.rs index 2cabdc921859c9a90cc218cde37a1f0f60720107..91abc2523d3c026567b0a3c4f83fa00115ab3cdd 100644 --- a/crates/rpc/src/proto.rs +++ b/crates/rpc/src/proto.rs @@ -126,6 +126,7 @@ messages!( ChannelMessageSent, CloseBuffer, DiskBasedDiagnosticsUpdated, + DiskBasedDiagnosticsUpdating, Error, GetChannelMessages, GetChannelMessagesResponse, @@ -183,6 +184,7 @@ entity_messages!( BufferSaved, CloseBuffer, DiskBasedDiagnosticsUpdated, + DiskBasedDiagnosticsUpdating, JoinProject, LeaveProject, OpenBuffer, diff --git a/crates/server/src/rpc.rs b/crates/server/src/rpc.rs index 510b701ddd5b5ef5f41e4915703206633e7a2ef3..76698c3a19171cb34a76b196db5dbb95ed805f7b 100644 --- a/crates/server/src/rpc.rs +++ b/crates/server/src/rpc.rs @@ -72,6 +72,7 @@ impl Server { .add_handler(Server::share_worktree) .add_handler(Server::update_worktree) .add_handler(Server::update_diagnostic_summary) + .add_handler(Server::disk_based_diagnostics_updating) .add_handler(Server::disk_based_diagnostics_updated) .add_handler(Server::open_buffer) .add_handler(Server::close_buffer) @@ -556,6 +557,22 @@ impl Server { Ok(()) } + async fn disk_based_diagnostics_updating( + self: Arc, + request: TypedEnvelope, + ) -> tide::Result<()> { + let receiver_ids = self + .state() + .project_connection_ids(request.payload.project_id, request.sender_id) + .ok_or_else(|| anyhow!(NO_SUCH_PROJECT))?; + broadcast(request.sender_id, receiver_ids, |connection_id| { + self.peer + .forward_send(request.sender_id, connection_id, request.payload.clone()) + }) + .await?; + Ok(()) + } + async fn disk_based_diagnostics_updated( self: Arc, request: TypedEnvelope, From 3cab32d2010841b8f691a5bd543025cdaef4d8a6 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Fri, 7 Jan 2022 16:12:50 +0100 Subject: [PATCH 33/43] WIP: Add keybinding to open buffers under cursors Co-Authored-By: Nathan Sobo --- crates/diagnostics/src/diagnostics.rs | 29 ++++++++++++++++++++++- crates/editor/src/editor.rs | 5 ++-- crates/editor/src/multi_buffer.rs | 27 +++++++++++++++++++++ crates/file_finder/src/file_finder.rs | 2 +- crates/project_panel/src/project_panel.rs | 2 +- crates/workspace/src/workspace.rs | 4 ++-- crates/zed/src/zed.rs | 16 ++++++------- 7 files changed, 70 insertions(+), 15 deletions(-) diff --git a/crates/diagnostics/src/diagnostics.rs b/crates/diagnostics/src/diagnostics.rs index c7f1f9b86871d499a7f90861b6d9fb1c9aef67a2..c008b10399eb94c3b34d6164fc96e20773b135e7 100644 --- a/crates/diagnostics/src/diagnostics.rs +++ b/crates/diagnostics/src/diagnostics.rs @@ -19,12 +19,21 @@ use util::TryFutureExt; use workspace::Workspace; action!(Deploy); +action!(OpenExcerpts); const CONTEXT_LINE_COUNT: u32 = 1; pub fn init(cx: &mut MutableAppContext) { - cx.add_bindings([Binding::new("alt-shift-D", Deploy, None)]); + cx.add_bindings([ + Binding::new("alt-shift-D", Deploy, Some("Workspace")), + Binding::new( + "alt-shift-D", + OpenExcerpts, + Some("ProjectDiagnosticsEditor"), + ), + ]); cx.add_action(ProjectDiagnosticsEditor::deploy); + cx.add_action(ProjectDiagnosticsEditor::open_excerpts); } type Event = editor::Event; @@ -155,6 +164,24 @@ impl ProjectDiagnosticsEditor { workspace.add_item(diagnostics, cx); } + fn open_excerpts(&mut self, _: &OpenExcerpts, cx: &mut ViewContext) { + let editor = self.editor.read(cx); + let excerpts = self.excerpts.read(cx); + let mut new_selections_by_buffer = HashMap::default(); + for selection in editor.local_selections::(cx) { + for (buffer, range) in excerpts.excerpted_buffers(selection.start..selection.end, cx) { + new_selections_by_buffer + .entry(buffer) + .or_insert(Vec::new()) + .push((range.start, range.end, selection.reversed)) + } + } + + for (buffer, selections) in new_selections_by_buffer { + // buffer.read(cx). + } + } + fn update_excerpts(&self, paths: HashSet, cx: &mut ViewContext) { let project = self.project.clone(); cx.spawn(|this, mut cx| { diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index b9c57de1308b25ec0cf9999a5d29a240179371fb..7ec1a171fed16a690b252b1459b1afda528fe93e 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -1105,7 +1105,7 @@ impl Editor { T: ToOffset, { let buffer = self.buffer.read(cx).snapshot(cx); - let selections = ranges + let mut selections = ranges .into_iter() .map(|range| { let mut start = range.start.to_offset(&buffer); @@ -1124,7 +1124,8 @@ impl Editor { goal: SelectionGoal::None, } }) - .collect(); + .collect::>(); + selections.sort_unstable_by_key(|s| s.start); self.update_selections(selections, autoscroll, cx); } diff --git a/crates/editor/src/multi_buffer.rs b/crates/editor/src/multi_buffer.rs index ed67491e24b4e7ba82dd270272920aa07a38d828..0fe155ff1138e20693643b0009c1775809f36e5b 100644 --- a/crates/editor/src/multi_buffer.rs +++ b/crates/editor/src/multi_buffer.rs @@ -689,6 +689,33 @@ impl MultiBuffer { .map_or(Vec::new(), |state| state.excerpts.clone()) } + pub fn excerpted_buffers<'a, T: ToOffset>( + &'a self, + range: Range, + cx: &AppContext, + ) -> Vec<(ModelHandle, Range)> { + let snapshot = self.snapshot(cx); + let start = range.start.to_offset(&snapshot); + let end = range.end.to_offset(&snapshot); + + let mut result = Vec::new(); + let mut cursor = snapshot.excerpts.cursor::(); + cursor.seek(&start, Bias::Right, &()); + while let Some(excerpt) = cursor.item() { + if *cursor.start() > end { + break; + } + + let excerpt_start = excerpt.range.start.to_offset(&excerpt.buffer); + let start = excerpt_start + (cmp::max(start, *cursor.start()) - *cursor.start()); + let end = excerpt_start + (cmp::min(end, cursor.end(&())) - *cursor.start()); + let buffer = self.buffers.borrow()[&excerpt.buffer_id].buffer.clone(); + result.push((buffer, start..end)); + } + + result + } + pub fn remove_excerpts<'a>( &mut self, excerpt_ids: impl IntoIterator, diff --git a/crates/file_finder/src/file_finder.rs b/crates/file_finder/src/file_finder.rs index f317a189167f16eeddab18f25b47a35e5f8ee3e5..48a1acd03bff8be8cada436380c55b8d77bd0d0c 100644 --- a/crates/file_finder/src/file_finder.rs +++ b/crates/file_finder/src/file_finder.rs @@ -250,7 +250,7 @@ impl FileFinder { match event { Event::Selected(project_path) => { workspace - .open_entry(project_path.clone(), cx) + .open_path(project_path.clone(), cx) .map(|d| d.detach()); workspace.dismiss_modal(cx); } diff --git a/crates/project_panel/src/project_panel.rs b/crates/project_panel/src/project_panel.rs index 9ced3fbeb0062c1fc4f7a667cd7bd94c8249188d..8e3f004e8d98851745c6beaed543eacfad590fd1 100644 --- a/crates/project_panel/src/project_panel.rs +++ b/crates/project_panel/src/project_panel.rs @@ -124,7 +124,7 @@ impl ProjectPanel { if let Some(worktree) = project.read(cx).worktree_for_id(worktree_id, cx) { if let Some(entry) = worktree.read(cx).entry_for_id(entry_id) { workspace - .open_entry( + .open_path( ProjectPath { worktree_id, path: entry.path.clone(), diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 6bb6da215778bd69eb3c98e7e1d35be776d14e06..3c60254b709d7f8068c6c6c51e429011eb77aa36 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -561,7 +561,7 @@ impl Workspace { let project_path = project_path.await.ok()?; if fs.is_file(&abs_path).await { if let Some(entry) = - this.update(&mut cx, |this, cx| this.open_entry(project_path, cx)) + this.update(&mut cx, |this, cx| this.open_path(project_path, cx)) { return Some(entry.await); } @@ -665,7 +665,7 @@ impl Workspace { } #[must_use] - pub fn open_entry( + pub fn open_path( &mut self, project_path: ProjectPath, cx: &mut ViewContext, diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index 00046689398091174b771cfb297dfc72531fedb6..04e21fc58cb4e5ab17e6d9e44a51f37515834d9f 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -264,7 +264,7 @@ mod tests { // Open the first entry let entry_1 = workspace - .update(&mut cx, |w, cx| w.open_entry(file1.clone(), cx)) + .update(&mut cx, |w, cx| w.open_path(file1.clone(), cx)) .unwrap() .await .unwrap(); @@ -279,7 +279,7 @@ mod tests { // Open the second entry workspace - .update(&mut cx, |w, cx| w.open_entry(file2.clone(), cx)) + .update(&mut cx, |w, cx| w.open_path(file2.clone(), cx)) .unwrap() .await .unwrap(); @@ -294,7 +294,7 @@ mod tests { // Open the first entry again. The existing pane item is activated. let entry_1b = workspace - .update(&mut cx, |w, cx| w.open_entry(file1.clone(), cx).unwrap()) + .update(&mut cx, |w, cx| w.open_path(file1.clone(), cx).unwrap()) .await .unwrap(); assert_eq!(entry_1.id(), entry_1b.id()); @@ -312,7 +312,7 @@ mod tests { workspace .update(&mut cx, |w, cx| { w.split_pane(w.active_pane().clone(), SplitDirection::Right, cx); - w.open_entry(file2.clone(), cx).unwrap() + w.open_path(file2.clone(), cx).unwrap() }) .await .unwrap(); @@ -331,8 +331,8 @@ mod tests { // Open the third entry twice concurrently. Only one pane item is added. let (t1, t2) = workspace.update(&mut cx, |w, cx| { ( - w.open_entry(file3.clone(), cx).unwrap(), - w.open_entry(file3.clone(), cx).unwrap(), + w.open_path(file3.clone(), cx).unwrap(), + w.open_path(file3.clone(), cx).unwrap(), ) }); t1.await.unwrap(); @@ -562,7 +562,7 @@ mod tests { .update(&mut cx, |workspace, cx| { workspace.split_pane(workspace.active_pane().clone(), SplitDirection::Right, cx); workspace - .open_entry( + .open_path( ProjectPath { worktree_id: worktree.read(cx).id(), path: Path::new("the-new-name.rs").into(), @@ -666,7 +666,7 @@ mod tests { let pane_1 = cx.read(|cx| workspace.read(cx).active_pane().clone()); workspace - .update(&mut cx, |w, cx| w.open_entry(file1.clone(), cx)) + .update(&mut cx, |w, cx| w.open_path(file1.clone(), cx)) .unwrap() .await .unwrap(); From 794d214eee1ff1e7018ec46c5ede54aba2c64b1d Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Fri, 7 Jan 2022 17:38:37 +0100 Subject: [PATCH 34/43] Refactor opening workspace items Co-Authored-By: Nathan Sobo --- Cargo.lock | 2 + crates/diagnostics/src/diagnostics.rs | 55 +++--- crates/editor/src/editor.rs | 6 +- crates/editor/src/items.rs | 58 +++--- crates/file_finder/src/file_finder.rs | 8 +- crates/gpui/src/app.rs | 4 + crates/gpui/src/executor.rs | 15 +- crates/project_panel/Cargo.toml | 1 + crates/project_panel/src/project_panel.rs | 2 +- crates/workspace/Cargo.toml | 1 + crates/workspace/src/pane.rs | 109 +++++------ crates/workspace/src/workspace.rs | 226 +++++++++++----------- crates/zed/src/main.rs | 6 +- crates/zed/src/test.rs | 6 +- crates/zed/src/zed.rs | 37 ++-- 15 files changed, 280 insertions(+), 256 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 7df97ed637f9e5c8916cc598ba0daea957f8616b..ac662eb09bf61afbf75b020661e3c2816dd99aa2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3502,6 +3502,7 @@ dependencies = [ "project", "serde_json", "theme", + "util", "workspace", ] @@ -5650,6 +5651,7 @@ dependencies = [ "anyhow", "client", "clock", + "collections", "gpui", "language", "log", diff --git a/crates/diagnostics/src/diagnostics.rs b/crates/diagnostics/src/diagnostics.rs index c008b10399eb94c3b34d6164fc96e20773b135e7..7576d495aa5b8d640b069217a72c36c7d8c19c84 100644 --- a/crates/diagnostics/src/diagnostics.rs +++ b/crates/diagnostics/src/diagnostics.rs @@ -43,7 +43,7 @@ struct ProjectDiagnostics { } struct ProjectDiagnosticsEditor { - project: ModelHandle, + model: ModelHandle, editor: ViewHandle, excerpts: ModelHandle, path_states: Vec<(Arc, Vec)>, @@ -109,10 +109,11 @@ impl View for ProjectDiagnosticsEditor { impl ProjectDiagnosticsEditor { fn new( - project: ModelHandle, + model: ModelHandle, settings: watch::Receiver, cx: &mut ViewContext, ) -> Self { + let project = model.read(cx).project.clone(); cx.subscribe(&project, |this, _, event, cx| match event { project::Event::DiskBasedDiagnosticsUpdated { worktree_id } => { if let Some(paths) = this.paths_to_update.remove(&worktree_id) { @@ -142,7 +143,7 @@ impl ProjectDiagnosticsEditor { .map(|e| e.0) .collect(); let this = Self { - project, + model, excerpts, editor, build_settings, @@ -161,7 +162,7 @@ impl ProjectDiagnosticsEditor { fn deploy(workspace: &mut Workspace, _: &Deploy, cx: &mut ViewContext) { let diagnostics = cx.add_model(|_| ProjectDiagnostics::new(workspace.project().clone())); - workspace.add_item(diagnostics, cx); + workspace.open_item(diagnostics, cx); } fn open_excerpts(&mut self, _: &OpenExcerpts, cx: &mut ViewContext) { @@ -183,7 +184,7 @@ impl ProjectDiagnosticsEditor { } fn update_excerpts(&self, paths: HashSet, cx: &mut ViewContext) { - let project = self.project.clone(); + let project = self.model.read(cx).project.clone(); cx.spawn(|this, mut cx| { async move { for path in paths { @@ -461,8 +462,7 @@ impl workspace::Item for ProjectDiagnostics { settings: watch::Receiver, cx: &mut ViewContext, ) -> Self::View { - let project = handle.read(cx).project.clone(); - ProjectDiagnosticsEditor::new(project, settings, cx) + ProjectDiagnosticsEditor::new(handle, settings, cx) } fn project_path(&self) -> Option { @@ -471,6 +471,12 @@ impl workspace::Item for ProjectDiagnostics { } impl workspace::ItemView for ProjectDiagnosticsEditor { + type ItemHandle = ModelHandle; + + fn item_handle(&self, _: &AppContext) -> Self::ItemHandle { + self.model.clone() + } + fn title(&self, _: &AppContext) -> String { "Project Diagnostics".to_string() } @@ -479,10 +485,26 @@ impl workspace::ItemView for ProjectDiagnosticsEditor { None } + fn is_dirty(&self, cx: &AppContext) -> bool { + self.excerpts.read(cx).read(cx).is_dirty() + } + + fn has_conflict(&self, cx: &AppContext) -> bool { + self.excerpts.read(cx).read(cx).has_conflict() + } + + fn can_save(&self, _: &AppContext) -> bool { + true + } + fn save(&mut self, cx: &mut ViewContext) -> Result>> { self.excerpts.update(cx, |excerpts, cx| excerpts.save(cx)) } + fn can_save_as(&self, _: &AppContext) -> bool { + false + } + fn save_as( &mut self, _: ModelHandle, @@ -492,28 +514,12 @@ impl workspace::ItemView for ProjectDiagnosticsEditor { unreachable!() } - fn is_dirty(&self, cx: &AppContext) -> bool { - self.excerpts.read(cx).read(cx).is_dirty() - } - - fn has_conflict(&self, cx: &AppContext) -> bool { - self.excerpts.read(cx).read(cx).has_conflict() - } - fn should_update_tab_on_event(event: &Event) -> bool { matches!( event, Event::Saved | Event::Dirtied | Event::FileHandleChanged ) } - - fn can_save(&self, _: &AppContext) -> bool { - true - } - - fn can_save_as(&self, _: &AppContext) -> bool { - false - } } fn compare_diagnostics( @@ -677,8 +683,9 @@ mod tests { .unwrap(); }); + let model = cx.add_model(|_| ProjectDiagnostics::new(project.clone())); let view = cx.add_view(Default::default(), |cx| { - ProjectDiagnosticsEditor::new(project.clone(), settings, cx) + ProjectDiagnosticsEditor::new(model, settings, cx) }); view.condition(&mut cx, |view, cx| view.text(cx).contains("fn main()")) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 7ec1a171fed16a690b252b1459b1afda528fe93e..589525fd33eff1117d75bf2b9805254313d030d5 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -111,8 +111,8 @@ action!(FoldSelectedRanges); action!(Scroll, Vector2F); action!(Select, SelectPhase); -pub fn init(cx: &mut MutableAppContext, entry_openers: &mut Vec>) { - entry_openers.push(Box::new(items::BufferOpener)); +pub fn init(cx: &mut MutableAppContext, path_openers: &mut Vec>) { + path_openers.push(Box::new(items::BufferOpener)); cx.add_bindings(vec![ Binding::new("escape", Cancel, Some("Editor")), Binding::new("backspace", Backspace, Some("Editor")), @@ -525,7 +525,7 @@ impl Editor { Buffer::new(0, "", cx).with_language(Some(language::PLAIN_TEXT.clone()), None, cx) }); let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx)); - workspace.add_item(BufferItemHandle(buffer), cx); + workspace.open_item(BufferItemHandle(buffer), cx); } pub fn replica_id(&self, cx: &AppContext) -> ReplicaId { diff --git a/crates/editor/src/items.rs b/crates/editor/src/items.rs index 29acf348a8590cdb16b1391081815be32e061e47..30c7bd80b8ef6b1f9bfdf3d3b00972389c57ffb8 100644 --- a/crates/editor/src/items.rs +++ b/crates/editor/src/items.rs @@ -71,6 +71,10 @@ impl ItemHandle for BufferItemHandle { path: f.path().clone(), }) } + + fn id(&self) -> usize { + self.0.id() + } } impl WeakItemHandle for WeakBufferItemHandle { @@ -79,22 +83,17 @@ impl WeakItemHandle for WeakBufferItemHandle { .upgrade(cx) .map(|buffer| Box::new(BufferItemHandle(buffer)) as Box) } -} -impl ItemView for Editor { - fn should_activate_item_on_event(event: &Event) -> bool { - matches!(event, Event::Activate) + fn id(&self) -> usize { + self.0.id() } +} - fn should_close_item_on_event(event: &Event) -> bool { - matches!(event, Event::Closed) - } +impl ItemView for Editor { + type ItemHandle = BufferItemHandle; - fn should_update_tab_on_event(event: &Event) -> bool { - matches!( - event, - Event::Saved | Event::Dirtied | Event::FileHandleChanged - ) + fn item_handle(&self, cx: &AppContext) -> Self::ItemHandle { + BufferItemHandle(self.buffer.clone()) } fn title(&self, cx: &AppContext) -> String { @@ -124,6 +123,18 @@ impl ItemView for Editor { Some(self.clone(cx)) } + fn is_dirty(&self, cx: &AppContext) -> bool { + self.buffer().read(cx).read(cx).is_dirty() + } + + fn has_conflict(&self, cx: &AppContext) -> bool { + self.buffer().read(cx).read(cx).has_conflict() + } + + fn can_save(&self, cx: &AppContext) -> bool { + self.project_path(cx).is_some() + } + fn save(&mut self, cx: &mut ViewContext) -> Result>> { let save = self.buffer().update(cx, |b, cx| b.save(cx))?; Ok(cx.spawn(|_, _| async move { @@ -132,6 +143,10 @@ impl ItemView for Editor { })) } + fn can_save_as(&self, _: &AppContext) -> bool { + true + } + fn save_as( &mut self, worktree: ModelHandle, @@ -180,20 +195,19 @@ impl ItemView for Editor { }) } - fn is_dirty(&self, cx: &AppContext) -> bool { - self.buffer().read(cx).read(cx).is_dirty() - } - - fn has_conflict(&self, cx: &AppContext) -> bool { - self.buffer().read(cx).read(cx).has_conflict() + fn should_activate_item_on_event(event: &Event) -> bool { + matches!(event, Event::Activate) } - fn can_save(&self, cx: &AppContext) -> bool { - self.project_path(cx).is_some() + fn should_close_item_on_event(event: &Event) -> bool { + matches!(event, Event::Closed) } - fn can_save_as(&self, _: &AppContext) -> bool { - true + fn should_update_tab_on_event(event: &Event) -> bool { + matches!( + event, + Event::Saved | Event::Dirtied | Event::FileHandleChanged + ) } } diff --git a/crates/file_finder/src/file_finder.rs b/crates/file_finder/src/file_finder.rs index 48a1acd03bff8be8cada436380c55b8d77bd0d0c..707a3bfb20602820f3be4f86681bb1605b04e0b8 100644 --- a/crates/file_finder/src/file_finder.rs +++ b/crates/file_finder/src/file_finder.rs @@ -251,7 +251,7 @@ impl FileFinder { Event::Selected(project_path) => { workspace .open_path(project_path.clone(), cx) - .map(|d| d.detach()); + .detach_and_log_err(cx); workspace.dismiss_modal(cx); } Event::Dismissed => { @@ -431,14 +431,14 @@ mod tests { #[gpui::test] async fn test_matching_paths(mut cx: gpui::TestAppContext) { - let mut entry_openers = Vec::new(); + let mut path_openers = Vec::new(); cx.update(|cx| { super::init(cx); - editor::init(cx, &mut entry_openers); + editor::init(cx, &mut path_openers); }); let mut params = cx.update(WorkspaceParams::test); - params.entry_openers = Arc::from(entry_openers); + params.path_openers = Arc::from(path_openers); params .fs .as_fake() diff --git a/crates/gpui/src/app.rs b/crates/gpui/src/app.rs index e57bcbc764292876e200e512e2e3d44dccc2483a..82ec8fbceaf6afdc98a00b38c28cd8e6952de638 100644 --- a/crates/gpui/src/app.rs +++ b/crates/gpui/src/app.rs @@ -2773,6 +2773,10 @@ impl WeakModelHandle { } } + pub fn id(&self) -> usize { + self.model_id + } + pub fn upgrade(self, cx: &impl UpgradeModelHandle) -> Option> { cx.upgrade_model_handle(self) } diff --git a/crates/gpui/src/executor.rs b/crates/gpui/src/executor.rs index 596b1be18424fd73d4a05b940349995801321d09..84efd2c6e0b6b3ee9d81736821b27f581a460e10 100644 --- a/crates/gpui/src/executor.rs +++ b/crates/gpui/src/executor.rs @@ -7,7 +7,7 @@ use rand::prelude::*; use smol::{channel, prelude::*, Executor, Timer}; use std::{ any::Any, - fmt::{self, Debug}, + fmt::{self, Debug, Display}, marker::PhantomData, mem, ops::RangeInclusive, @@ -25,7 +25,7 @@ use waker_fn::waker_fn; use crate::{ platform::{self, Dispatcher}, - util, + util, MutableAppContext, }; pub enum Foreground { @@ -682,6 +682,17 @@ impl Task { } } +impl Task> { + pub fn detach_and_log_err(self, cx: &mut MutableAppContext) { + cx.spawn(|_| async move { + if let Err(err) = self.await { + log::error!("{}", err); + } + }) + .detach(); + } +} + impl Task { fn send(any_task: AnyTask) -> Self { Self::Send { diff --git a/crates/project_panel/Cargo.toml b/crates/project_panel/Cargo.toml index eb9d76eaa98dc40ae17861e14243fba79cf5cd86..983b4b40fe35b9e945c56f62eff9de0d2eb5e5f6 100644 --- a/crates/project_panel/Cargo.toml +++ b/crates/project_panel/Cargo.toml @@ -10,6 +10,7 @@ path = "src/project_panel.rs" gpui = { path = "../gpui" } project = { path = "../project" } theme = { path = "../theme" } +util = { path = "../util" } workspace = { path = "../workspace" } postage = { version = "0.4.1", features = ["futures-traits"] } diff --git a/crates/project_panel/src/project_panel.rs b/crates/project_panel/src/project_panel.rs index 8e3f004e8d98851745c6beaed543eacfad590fd1..1ff1a33172094a25bb51b0f0c0c9d34d378b14e3 100644 --- a/crates/project_panel/src/project_panel.rs +++ b/crates/project_panel/src/project_panel.rs @@ -131,7 +131,7 @@ impl ProjectPanel { }, cx, ) - .map(|t| t.detach()); + .detach_and_log_err(cx); } } } diff --git a/crates/workspace/Cargo.toml b/crates/workspace/Cargo.toml index 759f115a358f86d297cfc00aa5f5dde4f625efd8..e85fefdabd47308fa9e1b0541d15a3fd9aa725a2 100644 --- a/crates/workspace/Cargo.toml +++ b/crates/workspace/Cargo.toml @@ -12,6 +12,7 @@ test-support = ["client/test-support", "project/test-support"] [dependencies] client = { path = "../client" } clock = { path = "../clock" } +collections = { path = "../collections" } gpui = { path = "../gpui" } language = { path = "../language" } project = { path = "../project" } diff --git a/crates/workspace/src/pane.rs b/crates/workspace/src/pane.rs index 2ef586a8fb31e316327f465471b2f17a8063e36f..c8b2c5f8687480e3ffcd18ac78b338fdb12c1265 100644 --- a/crates/workspace/src/pane.rs +++ b/crates/workspace/src/pane.rs @@ -1,15 +1,14 @@ use super::{ItemViewHandle, SplitDirection}; -use crate::Settings; +use crate::{ItemHandle, Settings}; use gpui::{ action, elements::*, geometry::{rect::RectF, vector::vec2f}, keymap::Binding, platform::CursorStyle, - Entity, MutableAppContext, Quad, RenderContext, View, ViewContext, ViewHandle, + Entity, MutableAppContext, Quad, RenderContext, View, ViewContext, }; use postage::watch; -use project::ProjectPath; use std::cmp; action!(Split, SplitDirection); @@ -70,7 +69,7 @@ pub struct TabState { } pub struct Pane { - items: Vec>, + item_views: Vec>, active_item: usize, settings: watch::Receiver, } @@ -78,7 +77,7 @@ pub struct Pane { impl Pane { pub fn new(settings: watch::Receiver) -> Self { Self { - items: Vec::new(), + item_views: Vec::new(), active_item: 0, settings, } @@ -88,43 +87,53 @@ impl Pane { cx.emit(Event::Activate); } - pub fn add_item(&mut self, item: Box, cx: &mut ViewContext) -> usize { - let item_idx = cmp::min(self.active_item + 1, self.items.len()); - self.items.insert(item_idx, item); - cx.notify(); - item_idx + pub fn open_item( + &mut self, + item_handle: T, + cx: &mut ViewContext, + ) -> Box + where + T: 'static + ItemHandle, + { + for (ix, item_view) in self.item_views.iter().enumerate() { + if item_view.item_handle(cx).id() == item_handle.id() { + let item_view = item_view.boxed_clone(); + self.activate_item(ix, cx); + return item_view; + } + } + + let item_view = item_handle.add_view(cx.window_id(), self.settings.clone(), cx); + self.add_item_view(item_view.boxed_clone(), cx); + item_view } - pub fn items(&self) -> &[Box] { - &self.items + pub fn add_item_view( + &mut self, + item_view: Box, + cx: &mut ViewContext, + ) { + item_view.added_to_pane(cx); + let item_idx = cmp::min(self.active_item + 1, self.item_views.len()); + self.item_views.insert(item_idx, item_view); + self.activate_item(item_idx, cx); + cx.notify(); } - pub fn active_item(&self) -> Option> { - self.items.get(self.active_item).cloned() + pub fn item_views(&self) -> &[Box] { + &self.item_views } - pub fn activate_entry( - &mut self, - project_path: ProjectPath, - cx: &mut ViewContext, - ) -> Option> { - if let Some(index) = self.items.iter().position(|item| { - item.project_path(cx.as_ref()) - .map_or(false, |item_path| item_path == project_path) - }) { - self.activate_item(index, cx); - self.items.get(index).map(|handle| handle.boxed_clone()) - } else { - None - } + pub fn active_item(&self) -> Option> { + self.item_views.get(self.active_item).cloned() } pub fn item_index(&self, item: &dyn ItemViewHandle) -> Option { - self.items.iter().position(|i| i.id() == item.id()) + self.item_views.iter().position(|i| i.id() == item.id()) } pub fn activate_item(&mut self, index: usize, cx: &mut ViewContext) { - if index < self.items.len() { + if index < self.item_views.len() { self.active_item = index; self.focus_active_item(cx); cx.notify(); @@ -134,15 +143,15 @@ impl Pane { pub fn activate_prev_item(&mut self, cx: &mut ViewContext) { if self.active_item > 0 { self.active_item -= 1; - } else if self.items.len() > 0 { - self.active_item = self.items.len() - 1; + } else if self.item_views.len() > 0 { + self.active_item = self.item_views.len() - 1; } self.focus_active_item(cx); cx.notify(); } pub fn activate_next_item(&mut self, cx: &mut ViewContext) { - if self.active_item + 1 < self.items.len() { + if self.active_item + 1 < self.item_views.len() { self.active_item += 1; } else { self.active_item = 0; @@ -152,15 +161,15 @@ impl Pane { } pub fn close_active_item(&mut self, cx: &mut ViewContext) { - if !self.items.is_empty() { - self.close_item(self.items[self.active_item].id(), cx) + if !self.item_views.is_empty() { + self.close_item(self.item_views[self.active_item].id(), cx) } } pub fn close_item(&mut self, item_id: usize, cx: &mut ViewContext) { - self.items.retain(|item| item.id() != item_id); - self.active_item = cmp::min(self.active_item, self.items.len().saturating_sub(1)); - if self.items.is_empty() { + self.item_views.retain(|item| item.id() != item_id); + self.active_item = cmp::min(self.active_item, self.item_views.len().saturating_sub(1)); + if self.item_views.is_empty() { cx.emit(Event::Remove); } cx.notify(); @@ -183,11 +192,11 @@ impl Pane { enum Tabs {} let tabs = MouseEventHandler::new::(cx.view_id(), cx, |mouse_state, cx| { let mut row = Flex::row(); - for (ix, item) in self.items.iter().enumerate() { + for (ix, item_view) in self.item_views.iter().enumerate() { let is_active = ix == self.active_item; row.add_child({ - let mut title = item.title(cx); + let mut title = item_view.title(cx); if title.len() > MAX_TAB_TITLE_LEN { let mut truncated_len = MAX_TAB_TITLE_LEN; while !title.is_char_boundary(truncated_len) { @@ -212,9 +221,9 @@ impl Pane { .with_child( Align::new({ let diameter = 7.0; - let icon_color = if item.has_conflict(cx) { + let icon_color = if item_view.has_conflict(cx) { Some(style.icon_conflict) - } else if item.is_dirty(cx) { + } else if item_view.is_dirty(cx) { Some(style.icon_dirty) } else { None @@ -271,7 +280,7 @@ impl Pane { .with_child( Align::new( ConstrainedBox::new(if mouse_state.hovered { - let item_id = item.id(); + let item_id = item_view.id(); enum TabCloseButton {} let icon = Svg::new("icons/x.svg"); MouseEventHandler::new::( @@ -354,17 +363,3 @@ impl View for Pane { self.focus_active_item(cx); } } - -pub trait PaneHandle { - fn add_item_view(&self, item: Box, cx: &mut MutableAppContext); -} - -impl PaneHandle for ViewHandle { - fn add_item_view(&self, item: Box, cx: &mut MutableAppContext) { - item.set_parent_pane(self, cx); - self.update(cx, |pane, cx| { - let item_idx = pane.add_item(item, cx); - pane.activate_item(item_idx, cx); - }); - } -} diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 3c60254b709d7f8068c6c6c51e429011eb77aa36..1ff9d6aa39c2b234ba3572da014147d95bfee236 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -7,6 +7,7 @@ mod status_bar; use anyhow::{anyhow, Result}; use client::{Authenticate, ChannelList, Client, User, UserStore}; use clock::ReplicaId; +use collections::HashSet; use gpui::{ action, color::Color, @@ -32,6 +33,7 @@ use status_bar::StatusBar; pub use status_bar::StatusItemView; use std::{ future::Future, + hash::{Hash, Hasher}, path::{Path, PathBuf}, sync::Arc, }; @@ -94,7 +96,7 @@ pub struct AppState { pub user_store: ModelHandle, pub fs: Arc, pub channel_list: ModelHandle, - pub entry_openers: Arc<[Box]>, + pub path_openers: Arc<[Box]>, pub build_window_options: &'static dyn Fn() -> WindowOptions<'static>, pub build_workspace: &'static dyn Fn( ModelHandle, @@ -137,6 +139,9 @@ pub trait Item: Entity + Sized { } pub trait ItemView: View { + type ItemHandle: ItemHandle; + + fn item_handle(&self, cx: &AppContext) -> Self::ItemHandle; fn title(&self, cx: &AppContext) -> String; fn project_path(&self, cx: &AppContext) -> Option; fn clone_on_split(&self, _: &mut ViewContext) -> Option @@ -172,6 +177,7 @@ pub trait ItemView: View { } pub trait ItemHandle: Send + Sync { + fn id(&self) -> usize; fn add_view( &self, window_id: usize, @@ -184,15 +190,17 @@ pub trait ItemHandle: Send + Sync { } pub trait WeakItemHandle { + fn id(&self) -> usize; fn upgrade(&self, cx: &AppContext) -> Option>; } pub trait ItemViewHandle { + fn item_handle(&self, cx: &AppContext) -> Box; fn title(&self, cx: &AppContext) -> String; fn project_path(&self, cx: &AppContext) -> Option; fn boxed_clone(&self) -> Box; fn clone_on_split(&self, cx: &mut MutableAppContext) -> Option>; - fn set_parent_pane(&self, pane: &ViewHandle, cx: &mut MutableAppContext); + fn added_to_pane(&self, cx: &mut ViewContext); fn id(&self) -> usize; fn to_any(&self) -> AnyViewHandle; fn is_dirty(&self, cx: &AppContext) -> bool; @@ -209,6 +217,10 @@ pub trait ItemViewHandle { } impl ItemHandle for ModelHandle { + fn id(&self) -> usize { + self.id() + } + fn add_view( &self, window_id: usize, @@ -232,6 +244,10 @@ impl ItemHandle for ModelHandle { } impl ItemHandle for Box { + fn id(&self) -> usize { + ItemHandle::id(self.as_ref()) + } + fn add_view( &self, window_id: usize, @@ -255,12 +271,34 @@ impl ItemHandle for Box { } impl WeakItemHandle for WeakModelHandle { + fn id(&self) -> usize { + WeakModelHandle::id(self) + } + fn upgrade(&self, cx: &AppContext) -> Option> { WeakModelHandle::::upgrade(*self, cx).map(|i| Box::new(i) as Box) } } +impl Hash for Box { + fn hash(&self, state: &mut H) { + self.id().hash(state); + } +} + +impl PartialEq for Box { + fn eq(&self, other: &Self) -> bool { + self.id() == other.id() + } +} + +impl Eq for Box {} + impl ItemViewHandle for ViewHandle { + fn item_handle(&self, cx: &AppContext) -> Box { + Box::new(self.read(cx).item_handle(cx)) + } + fn title(&self, cx: &AppContext) -> String { self.read(cx).title(cx) } @@ -280,25 +318,23 @@ impl ItemViewHandle for ViewHandle { .map(|handle| Box::new(handle) as Box) } - fn set_parent_pane(&self, pane: &ViewHandle, cx: &mut MutableAppContext) { - pane.update(cx, |_, cx| { - cx.subscribe(self, |pane, item, event, cx| { - if T::should_close_item_on_event(event) { - pane.close_item(item.id(), cx); - return; - } - if T::should_activate_item_on_event(event) { - if let Some(ix) = pane.item_index(&item) { - pane.activate_item(ix, cx); - pane.activate(cx); - } - } - if T::should_update_tab_on_event(event) { - cx.notify() + fn added_to_pane(&self, cx: &mut ViewContext) { + cx.subscribe(self, |pane, item, event, cx| { + if T::should_close_item_on_event(event) { + pane.close_item(item.id(), cx); + return; + } + if T::should_activate_item_on_event(event) { + if let Some(ix) = pane.item_index(&item) { + pane.activate_item(ix, cx); + pane.activate(cx); } - }) - .detach(); - }); + } + if T::should_update_tab_on_event(event) { + cx.notify() + } + }) + .detach(); } fn save(&self, cx: &mut MutableAppContext) -> Result>> { @@ -360,7 +396,7 @@ pub struct WorkspaceParams { pub settings: watch::Receiver, pub user_store: ModelHandle, pub channel_list: ModelHandle, - pub entry_openers: Arc<[Box]>, + pub path_openers: Arc<[Box]>, } impl WorkspaceParams { @@ -392,7 +428,7 @@ impl WorkspaceParams { languages, settings: watch::channel_with(settings).1, user_store, - entry_openers: Arc::from([]), + path_openers: Arc::from([]), } } @@ -412,7 +448,7 @@ impl WorkspaceParams { settings: app_state.settings.clone(), user_store: app_state.user_store.clone(), channel_list: app_state.channel_list.clone(), - entry_openers: app_state.entry_openers.clone(), + path_openers: app_state.path_openers.clone(), } } } @@ -430,8 +466,8 @@ pub struct Workspace { active_pane: ViewHandle, status_bar: ViewHandle, project: ModelHandle, - entry_openers: Arc<[Box]>, - items: Vec>, + path_openers: Arc<[Box]>, + items: HashSet>, _observe_current_user: Task<()>, } @@ -484,7 +520,7 @@ impl Workspace { left_sidebar: Sidebar::new(Side::Left), right_sidebar: Sidebar::new(Side::Right), project: params.project.clone(), - entry_openers: params.entry_openers.clone(), + path_openers: params.path_openers.clone(), items: Default::default(), _observe_current_user, } @@ -560,13 +596,13 @@ impl Workspace { async move { let project_path = project_path.await.ok()?; if fs.is_file(&abs_path).await { - if let Some(entry) = + Some( this.update(&mut cx, |this, cx| this.open_path(project_path, cx)) - { - return Some(entry.await); - } + .await, + ) + } else { + None } - None } }) }) @@ -667,102 +703,51 @@ impl Workspace { #[must_use] pub fn open_path( &mut self, - project_path: ProjectPath, + path: ProjectPath, cx: &mut ViewContext, - ) -> Option, Arc>>> { - let pane = self.active_pane().clone(); - if let Some(existing_item) = - self.activate_or_open_existing_entry(project_path.clone(), &pane, cx) - { - return Some(cx.foreground().spawn(async move { Ok(existing_item) })); + ) -> Task, Arc>> { + if let Some(existing_item) = self.item_for_path(&path, cx) { + return Task::ready(Ok(self.open_item(existing_item, cx))); } - let worktree = match self - .project - .read(cx) - .worktree_for_id(project_path.worktree_id, cx) - { + let worktree = match self.project.read(cx).worktree_for_id(path.worktree_id, cx) { Some(worktree) => worktree, None => { - log::error!("worktree {} does not exist", project_path.worktree_id); - return None; + return Task::ready(Err(Arc::new(anyhow!( + "worktree {} does not exist", + path.worktree_id + )))); } }; - let project_path = project_path.clone(); - let entry_openers = self.entry_openers.clone(); - let task = worktree.update(cx, |worktree, cx| { - for opener in entry_openers.iter() { + let project_path = path.clone(); + let path_openers = self.path_openers.clone(); + let open_task = worktree.update(cx, |worktree, cx| { + for opener in path_openers.iter() { if let Some(task) = opener.open(worktree, project_path.clone(), cx) { - return Some(task); + return task; } } - log::error!("no opener for path {:?} found", project_path); - None - })?; + Task::ready(Err(anyhow!("no opener found for path {:?}", project_path))) + }); - let pane = pane.downgrade(); - Some(cx.spawn(|this, mut cx| async move { - let load_result = task.await; + let pane = self.active_pane().clone().downgrade(); + cx.spawn(|this, mut cx| async move { + let item = open_task.await?; this.update(&mut cx, |this, cx| { let pane = pane .upgrade(&cx) .ok_or_else(|| anyhow!("could not upgrade pane reference"))?; - let item = load_result?; - - // By the time loading finishes, the entry could have been already added - // to the pane. If it was, we activate it, otherwise we'll store the - // item and add a new view for it. - if let Some(existing) = - this.activate_or_open_existing_entry(project_path, &pane, cx) - { - Ok(existing) - } else { - Ok(this.add_item(item, cx)) - } + Ok(this.open_item_in_pane(item, &pane, cx)) }) - })) + }) } - fn activate_or_open_existing_entry( - &mut self, - project_path: ProjectPath, - pane: &ViewHandle, - cx: &mut ViewContext, - ) -> Option> { - // If the pane contains a view for this file, then activate - // that item view. - if let Some(existing_item_view) = - pane.update(cx, |pane, cx| pane.activate_entry(project_path.clone(), cx)) - { - return Some(existing_item_view); - } - - // Otherwise, if this file is already open somewhere in the workspace, - // then add another view for it. - let settings = self.settings.clone(); - let mut view_for_existing_item = None; - self.items.retain(|item| { - if let Some(item) = item.upgrade(cx) { - if view_for_existing_item.is_none() - && item - .project_path(cx) - .map_or(false, |item_project_path| item_project_path == project_path) - { - view_for_existing_item = - Some(item.add_view(cx.window_id(), settings.clone(), cx.as_mut())); - } - true - } else { - false - } - }); - if let Some(view) = view_for_existing_item { - pane.add_item_view(view.boxed_clone(), cx.as_mut()); - Some(view) - } else { - None - } + fn item_for_path(&self, path: &ProjectPath, cx: &AppContext) -> Option> { + self.items + .iter() + .filter_map(|i| i.upgrade(cx)) + .find(|i| i.project_path(cx).as_ref() == Some(path)) } pub fn active_item(&self, cx: &AppContext) -> Option> { @@ -908,19 +893,28 @@ impl Workspace { pane } - pub fn add_item( + pub fn open_item( + &mut self, + item_handle: T, + cx: &mut ViewContext, + ) -> Box + where + T: 'static + ItemHandle, + { + self.open_item_in_pane(item_handle, &self.active_pane().clone(), cx) + } + + pub fn open_item_in_pane( &mut self, item_handle: T, + pane: &ViewHandle, cx: &mut ViewContext, ) -> Box where - T: ItemHandle, + T: 'static + ItemHandle, { - let view = item_handle.add_view(cx.window_id(), self.settings.clone(), cx); - self.items.push(item_handle.downgrade()); - self.active_pane() - .add_item_view(view.boxed_clone(), cx.as_mut()); - view + self.items.insert(item_handle.downgrade()); + pane.update(cx, |pane, cx| pane.open_item(item_handle, cx)) } fn activate_pane(&mut self, pane: ViewHandle, cx: &mut ViewContext) { @@ -965,7 +959,7 @@ impl Workspace { self.activate_pane(new_pane.clone(), cx); if let Some(item) = pane.read(cx).active_item() { if let Some(clone) = item.clone_on_split(cx.as_mut()) { - new_pane.add_item_view(clone, cx.as_mut()); + new_pane.update(cx, |new_pane, cx| new_pane.add_item_view(clone, cx)); } } self.center diff --git a/crates/zed/src/main.rs b/crates/zed/src/main.rs index 09b53603df515cd94b792cf97445851609c5f843..a06610967e0aa215947b263f7240ce42ed7e82de 100644 --- a/crates/zed/src/main.rs +++ b/crates/zed/src/main.rs @@ -51,11 +51,11 @@ fn main() { let http = http::client(); let client = client::Client::new(http.clone()); let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http.clone(), cx)); - let mut entry_openers = Vec::new(); + let mut path_openers = Vec::new(); client::init(client.clone(), cx); workspace::init(cx); - editor::init(cx, &mut entry_openers); + editor::init(cx, &mut path_openers); go_to_line::init(cx); file_finder::init(cx); chat_panel::init(cx); @@ -72,7 +72,7 @@ fn main() { client, user_store, fs: Arc::new(RealFs), - entry_openers: Arc::from(entry_openers), + path_openers: Arc::from(path_openers), build_window_options: &build_window_options, build_workspace: &build_workspace, }); diff --git a/crates/zed/src/test.rs b/crates/zed/src/test.rs index c56e24dd1cc2f7c0ff8416fbf0d8ccd456aaf2a0..4f685415d087f078e1d3f9ef3c9d969b3df31439 100644 --- a/crates/zed/src/test.rs +++ b/crates/zed/src/test.rs @@ -16,8 +16,8 @@ fn init_logger() { } pub fn test_app_state(cx: &mut MutableAppContext) -> Arc { - let mut entry_openers = Vec::new(); - editor::init(cx, &mut entry_openers); + let mut path_openers = Vec::new(); + editor::init(cx, &mut path_openers); let (settings_tx, settings) = watch::channel_with(build_settings(cx)); let themes = ThemeRegistry::new(Assets, cx.font_cache().clone()); let http = FakeHttpClient::with_404_response(); @@ -41,7 +41,7 @@ pub fn test_app_state(cx: &mut MutableAppContext) -> Arc { client, user_store, fs: Arc::new(FakeFs::new()), - entry_openers: Arc::from(entry_openers), + path_openers: Arc::from(path_openers), build_window_options: &build_window_options, build_workspace: &build_workspace, }) diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index 04e21fc58cb4e5ab17e6d9e44a51f37515834d9f..d2da4fdd53e3d8e3b5633485595a20e2db22fa0a 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -62,7 +62,7 @@ pub fn build_workspace( settings: app_state.settings.clone(), user_store: app_state.user_store.clone(), channel_list: app_state.channel_list.clone(), - entry_openers: app_state.entry_openers.clone(), + path_openers: app_state.path_openers.clone(), }; let mut workspace = Workspace::new(&workspace_params, cx); let project = workspace.project().clone(); @@ -265,7 +265,6 @@ mod tests { // Open the first entry let entry_1 = workspace .update(&mut cx, |w, cx| w.open_path(file1.clone(), cx)) - .unwrap() .await .unwrap(); cx.read(|cx| { @@ -274,13 +273,12 @@ mod tests { pane.active_item().unwrap().project_path(cx), Some(file1.clone()) ); - assert_eq!(pane.items().len(), 1); + assert_eq!(pane.item_views().len(), 1); }); // Open the second entry workspace .update(&mut cx, |w, cx| w.open_path(file2.clone(), cx)) - .unwrap() .await .unwrap(); cx.read(|cx| { @@ -289,12 +287,12 @@ mod tests { pane.active_item().unwrap().project_path(cx), Some(file2.clone()) ); - assert_eq!(pane.items().len(), 2); + assert_eq!(pane.item_views().len(), 2); }); // Open the first entry again. The existing pane item is activated. let entry_1b = workspace - .update(&mut cx, |w, cx| w.open_path(file1.clone(), cx).unwrap()) + .update(&mut cx, |w, cx| w.open_path(file1.clone(), cx)) .await .unwrap(); assert_eq!(entry_1.id(), entry_1b.id()); @@ -305,14 +303,14 @@ mod tests { pane.active_item().unwrap().project_path(cx), Some(file1.clone()) ); - assert_eq!(pane.items().len(), 2); + assert_eq!(pane.item_views().len(), 2); }); // Split the pane with the first entry, then open the second entry again. workspace .update(&mut cx, |w, cx| { w.split_pane(w.active_pane().clone(), SplitDirection::Right, cx); - w.open_path(file2.clone(), cx).unwrap() + w.open_path(file2.clone(), cx) }) .await .unwrap(); @@ -331,8 +329,8 @@ mod tests { // Open the third entry twice concurrently. Only one pane item is added. let (t1, t2) = workspace.update(&mut cx, |w, cx| { ( - w.open_path(file3.clone(), cx).unwrap(), - w.open_path(file3.clone(), cx).unwrap(), + w.open_path(file3.clone(), cx), + w.open_path(file3.clone(), cx), ) }); t1.await.unwrap(); @@ -344,7 +342,7 @@ mod tests { Some(file3.clone()) ); let pane_entries = pane - .items() + .item_views() .iter() .map(|i| i.project_path(cx).unwrap()) .collect::>(); @@ -561,15 +559,13 @@ mod tests { workspace .update(&mut cx, |workspace, cx| { workspace.split_pane(workspace.active_pane().clone(), SplitDirection::Right, cx); - workspace - .open_path( - ProjectPath { - worktree_id: worktree.read(cx).id(), - path: Path::new("the-new-name.rs").into(), - }, - cx, - ) - .unwrap() + workspace.open_path( + ProjectPath { + worktree_id: worktree.read(cx).id(), + path: Path::new("the-new-name.rs").into(), + }, + cx, + ) }) .await .unwrap(); @@ -667,7 +663,6 @@ mod tests { workspace .update(&mut cx, |w, cx| w.open_path(file1.clone(), cx)) - .unwrap() .await .unwrap(); cx.read(|cx| { From e5c520a2652be1ba4deca0850a1586db3c509a8b Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Fri, 7 Jan 2022 17:48:31 +0100 Subject: [PATCH 35/43] Use `Buffer` handles instead of `MultiBuffer` as editor workspace items Co-Authored-By: Nathan Sobo --- crates/editor/src/editor.rs | 5 ++--- crates/editor/src/items.rs | 26 +++++++++++--------------- crates/workspace/src/workspace.rs | 8 ++++---- 3 files changed, 17 insertions(+), 22 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 589525fd33eff1117d75bf2b9805254313d030d5..f7babdf8afa0f6bd0980019ae1e3c950f5e3401f 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -46,7 +46,7 @@ use sum_tree::Bias; use text::rope::TextDimension; use theme::{DiagnosticStyle, EditorStyle}; use util::post_inc; -use workspace::{EntryOpener, Workspace}; +use workspace::{PathOpener, Workspace}; const CURSOR_BLINK_INTERVAL: Duration = Duration::from_millis(500); const MAX_LINE_LEN: usize = 1024; @@ -111,7 +111,7 @@ action!(FoldSelectedRanges); action!(Scroll, Vector2F); action!(Select, SelectPhase); -pub fn init(cx: &mut MutableAppContext, path_openers: &mut Vec>) { +pub fn init(cx: &mut MutableAppContext, path_openers: &mut Vec>) { path_openers.push(Box::new(items::BufferOpener)); cx.add_bindings(vec![ Binding::new("escape", Cancel, Some("Editor")), @@ -524,7 +524,6 @@ impl Editor { let buffer = cx.add_model(|cx| { Buffer::new(0, "", cx).with_language(Some(language::PLAIN_TEXT.clone()), None, cx) }); - let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx)); workspace.open_item(BufferItemHandle(buffer), cx); } diff --git a/crates/editor/src/items.rs b/crates/editor/src/items.rs index 30c7bd80b8ef6b1f9bfdf3d3b00972389c57ffb8..57615073f7b0dbd0253f697840c7c1aa5d7150e2 100644 --- a/crates/editor/src/items.rs +++ b/crates/editor/src/items.rs @@ -5,25 +5,25 @@ use gpui::{ elements::*, AppContext, Entity, ModelContext, ModelHandle, MutableAppContext, RenderContext, Subscription, Task, View, ViewContext, ViewHandle, WeakModelHandle, }; -use language::{Diagnostic, File as _}; +use language::{Buffer, Diagnostic, File as _}; use postage::watch; use project::{File, ProjectPath, Worktree}; use std::fmt::Write; use std::path::Path; use text::{Point, Selection}; use workspace::{ - EntryOpener, ItemHandle, ItemView, ItemViewHandle, Settings, StatusItemView, WeakItemHandle, + ItemHandle, ItemView, ItemViewHandle, PathOpener, Settings, StatusItemView, WeakItemHandle, }; pub struct BufferOpener; #[derive(Clone)] -pub struct BufferItemHandle(pub ModelHandle); +pub struct BufferItemHandle(pub ModelHandle); #[derive(Clone)] -struct WeakBufferItemHandle(WeakModelHandle); +struct WeakBufferItemHandle(WeakModelHandle); -impl EntryOpener for BufferOpener { +impl PathOpener for BufferOpener { fn open( &self, worktree: &mut Worktree, @@ -31,9 +31,8 @@ impl EntryOpener for BufferOpener { cx: &mut ModelContext, ) -> Option>>> { let buffer = worktree.open_buffer(project_path.path, cx); - let task = cx.spawn(|_, mut cx| async move { + let task = cx.spawn(|_, _| async move { let buffer = buffer.await?; - let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx)); Ok(Box::new(BufferItemHandle(buffer)) as Box) }); Some(task) @@ -47,13 +46,10 @@ impl ItemHandle for BufferItemHandle { settings: watch::Receiver, cx: &mut MutableAppContext, ) -> Box { - let buffer = self.0.downgrade(); + let buffer = cx.add_model(|cx| MultiBuffer::singleton(self.0.clone(), cx)); + let weak_buffer = buffer.downgrade(); Box::new(cx.add_view(window_id, |cx| { - Editor::for_buffer( - self.0.clone(), - crate::settings_builder(buffer, settings), - cx, - ) + Editor::for_buffer(buffer, crate::settings_builder(weak_buffer, settings), cx) })) } @@ -66,7 +62,7 @@ impl ItemHandle for BufferItemHandle { } fn project_path(&self, cx: &AppContext) -> Option { - File::from_dyn(self.0.read(cx).file(cx)).map(|f| ProjectPath { + File::from_dyn(self.0.read(cx).file()).map(|f| ProjectPath { worktree_id: f.worktree_id(cx), path: f.path().clone(), }) @@ -93,7 +89,7 @@ impl ItemView for Editor { type ItemHandle = BufferItemHandle; fn item_handle(&self, cx: &AppContext) -> Self::ItemHandle { - BufferItemHandle(self.buffer.clone()) + BufferItemHandle(self.buffer.read(cx).as_singleton().unwrap()) } fn title(&self, cx: &AppContext) -> String { diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 1ff9d6aa39c2b234ba3572da014147d95bfee236..055fa1169db25480b0984beb2753285a8da78528 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -96,7 +96,7 @@ pub struct AppState { pub user_store: ModelHandle, pub fs: Arc, pub channel_list: ModelHandle, - pub path_openers: Arc<[Box]>, + pub path_openers: Arc<[Box]>, pub build_window_options: &'static dyn Fn() -> WindowOptions<'static>, pub build_workspace: &'static dyn Fn( ModelHandle, @@ -117,7 +117,7 @@ pub struct JoinProjectParams { pub app_state: Arc, } -pub trait EntryOpener { +pub trait PathOpener { fn open( &self, worktree: &mut Worktree, @@ -396,7 +396,7 @@ pub struct WorkspaceParams { pub settings: watch::Receiver, pub user_store: ModelHandle, pub channel_list: ModelHandle, - pub path_openers: Arc<[Box]>, + pub path_openers: Arc<[Box]>, } impl WorkspaceParams { @@ -466,7 +466,7 @@ pub struct Workspace { active_pane: ViewHandle, status_bar: ViewHandle, project: ModelHandle, - path_openers: Arc<[Box]>, + path_openers: Arc<[Box]>, items: HashSet>, _observe_current_user: Task<()>, } From ea263822fad2fb76ea622fe4ed2b55e4afd807d8 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Fri, 7 Jan 2022 09:59:27 -0800 Subject: [PATCH 36/43] Finish implementing ProjectDiagnostics::open_excerpts * Build workspace item views with a reference to the workspace * Add randomized test for MultiBuffer::excerpted_buffers and fix a small bug Co-Authored-By: Nathan Sobo --- crates/diagnostics/src/diagnostics.rs | 64 ++++++++++++++++++--------- crates/editor/src/items.rs | 9 +++- crates/editor/src/multi_buffer.rs | 46 ++++++++++++++----- crates/workspace/src/pane.rs | 5 ++- crates/workspace/src/workspace.rs | 26 +++++++---- 5 files changed, 106 insertions(+), 44 deletions(-) diff --git a/crates/diagnostics/src/diagnostics.rs b/crates/diagnostics/src/diagnostics.rs index 7576d495aa5b8d640b069217a72c36c7d8c19c84..fd710d596b32f83de03036daccc5646f866cbdff 100644 --- a/crates/diagnostics/src/diagnostics.rs +++ b/crates/diagnostics/src/diagnostics.rs @@ -5,16 +5,17 @@ use collections::{HashMap, HashSet}; use editor::{ context_header_renderer, diagnostic_block_renderer, diagnostic_header_renderer, display_map::{BlockDisposition, BlockId, BlockProperties}, - BuildSettings, Editor, ExcerptId, ExcerptProperties, MultiBuffer, + items::BufferItemHandle, + Autoscroll, BuildSettings, Editor, ExcerptId, ExcerptProperties, MultiBuffer, }; use gpui::{ action, elements::*, keymap::Binding, AppContext, Entity, ModelHandle, MutableAppContext, - RenderContext, Task, View, ViewContext, ViewHandle, + RenderContext, Task, View, ViewContext, ViewHandle, WeakViewHandle, }; use language::{Bias, Buffer, Diagnostic, DiagnosticEntry, Point, Selection, SelectionGoal}; use postage::watch; use project::{Project, ProjectPath, WorktreeId}; -use std::{cmp::Ordering, ops::Range, path::Path, sync::Arc}; +use std::{cmp::Ordering, mem, ops::Range, path::Path, sync::Arc}; use util::TryFutureExt; use workspace::Workspace; @@ -44,6 +45,7 @@ struct ProjectDiagnostics { struct ProjectDiagnosticsEditor { model: ModelHandle, + workspace: WeakViewHandle, editor: ViewHandle, excerpts: ModelHandle, path_states: Vec<(Arc, Vec)>, @@ -110,6 +112,7 @@ impl View for ProjectDiagnosticsEditor { impl ProjectDiagnosticsEditor { fn new( model: ModelHandle, + workspace: WeakViewHandle, settings: watch::Receiver, cx: &mut ViewContext, ) -> Self { @@ -144,6 +147,7 @@ impl ProjectDiagnosticsEditor { .collect(); let this = Self { model, + workspace, excerpts, editor, build_settings, @@ -166,20 +170,37 @@ impl ProjectDiagnosticsEditor { } fn open_excerpts(&mut self, _: &OpenExcerpts, cx: &mut ViewContext) { - let editor = self.editor.read(cx); - let excerpts = self.excerpts.read(cx); - let mut new_selections_by_buffer = HashMap::default(); - for selection in editor.local_selections::(cx) { - for (buffer, range) in excerpts.excerpted_buffers(selection.start..selection.end, cx) { - new_selections_by_buffer - .entry(buffer) - .or_insert(Vec::new()) - .push((range.start, range.end, selection.reversed)) + if let Some(workspace) = self.workspace.upgrade(cx) { + let editor = self.editor.read(cx); + let excerpts = self.excerpts.read(cx); + let mut new_selections_by_buffer = HashMap::default(); + + for selection in editor.local_selections::(cx) { + for (buffer, mut range) in + excerpts.excerpted_buffers(selection.start..selection.end, cx) + { + if selection.reversed { + mem::swap(&mut range.start, &mut range.end); + } + new_selections_by_buffer + .entry(buffer) + .or_insert(Vec::new()) + .push(range) + } } - } - for (buffer, selections) in new_selections_by_buffer { - // buffer.read(cx). + workspace.update(cx, |workspace, cx| { + for (buffer, ranges) in new_selections_by_buffer { + let editor = workspace + .open_item(BufferItemHandle(buffer), cx) + .to_any() + .downcast::() + .unwrap(); + editor.update(cx, |editor, cx| { + editor.select_ranges(ranges, Some(Autoscroll::Center), cx) + }); + } + }); } } @@ -459,10 +480,10 @@ impl workspace::Item for ProjectDiagnostics { fn build_view( handle: ModelHandle, - settings: watch::Receiver, + workspace: &Workspace, cx: &mut ViewContext, ) -> Self::View { - ProjectDiagnosticsEditor::new(handle, settings, cx) + ProjectDiagnosticsEditor::new(handle, workspace.weak_handle(), workspace.settings(), cx) } fn project_path(&self) -> Option { @@ -554,7 +575,8 @@ mod tests { #[gpui::test] async fn test_diagnostics(mut cx: TestAppContext) { - let settings = cx.update(WorkspaceParams::test).settings; + let workspace_params = cx.update(WorkspaceParams::test); + let settings = workspace_params.settings.clone(); let http_client = FakeHttpClient::new(|_| async move { Ok(ServerResponse::new(404)) }); let client = Client::new(http_client.clone()); let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http_client, cx)); @@ -684,8 +706,10 @@ mod tests { }); let model = cx.add_model(|_| ProjectDiagnostics::new(project.clone())); - let view = cx.add_view(Default::default(), |cx| { - ProjectDiagnosticsEditor::new(model, settings, cx) + let workspace = cx.add_view(0, |cx| Workspace::new(&workspace_params, cx)); + + let view = cx.add_view(0, |cx| { + ProjectDiagnosticsEditor::new(model, workspace.downgrade(), settings, cx) }); view.condition(&mut cx, |view, cx| view.text(cx).contains("fn main()")) diff --git a/crates/editor/src/items.rs b/crates/editor/src/items.rs index 57615073f7b0dbd0253f697840c7c1aa5d7150e2..8f5745cc2aa3fca45bbd126c61caad82105838d1 100644 --- a/crates/editor/src/items.rs +++ b/crates/editor/src/items.rs @@ -13,6 +13,7 @@ use std::path::Path; use text::{Point, Selection}; use workspace::{ ItemHandle, ItemView, ItemViewHandle, PathOpener, Settings, StatusItemView, WeakItemHandle, + Workspace, }; pub struct BufferOpener; @@ -43,13 +44,17 @@ impl ItemHandle for BufferItemHandle { fn add_view( &self, window_id: usize, - settings: watch::Receiver, + workspace: &Workspace, cx: &mut MutableAppContext, ) -> Box { let buffer = cx.add_model(|cx| MultiBuffer::singleton(self.0.clone(), cx)); let weak_buffer = buffer.downgrade(); Box::new(cx.add_view(window_id, |cx| { - Editor::for_buffer(buffer, crate::settings_builder(weak_buffer, settings), cx) + Editor::for_buffer( + buffer, + crate::settings_builder(weak_buffer, workspace.settings()), + cx, + ) })) } diff --git a/crates/editor/src/multi_buffer.rs b/crates/editor/src/multi_buffer.rs index 0fe155ff1138e20693643b0009c1775809f36e5b..47431c36fca2dd404dbeecd0727492d11b6f7dd5 100644 --- a/crates/editor/src/multi_buffer.rs +++ b/crates/editor/src/multi_buffer.rs @@ -706,11 +706,16 @@ impl MultiBuffer { break; } + let mut end_before_newline = cursor.end(&()); + if excerpt.has_trailing_newline { + end_before_newline -= 1; + } let excerpt_start = excerpt.range.start.to_offset(&excerpt.buffer); let start = excerpt_start + (cmp::max(start, *cursor.start()) - *cursor.start()); - let end = excerpt_start + (cmp::min(end, cursor.end(&())) - *cursor.start()); + let end = excerpt_start + (cmp::min(end, end_before_newline) - *cursor.start()); let buffer = self.buffers.borrow()[&excerpt.buffer_id].buffer.clone(); result.push((buffer, start..end)); + cursor.next(&()); } result @@ -2582,7 +2587,7 @@ mod tests { .unwrap_or(10); let mut buffers: Vec> = Vec::new(); - let list = cx.add_model(|_| MultiBuffer::new(0)); + let multibuffer = cx.add_model(|_| MultiBuffer::new(0)); let mut excerpt_ids = Vec::new(); let mut expected_excerpts = Vec::<(ModelHandle, Range)>::new(); let mut old_versions = Vec::new(); @@ -2613,7 +2618,9 @@ mod tests { ); } ids_to_remove.sort_unstable(); - list.update(cx, |list, cx| list.remove_excerpts(&ids_to_remove, cx)); + multibuffer.update(cx, |multibuffer, cx| { + multibuffer.remove_excerpts(&ids_to_remove, cx) + }); } _ => { let buffer_handle = if buffers.is_empty() || rng.gen_bool(0.4) { @@ -2645,8 +2652,8 @@ mod tests { &buffer.text()[start_ix..end_ix] ); - let excerpt_id = list.update(cx, |list, cx| { - list.insert_excerpt_after( + let excerpt_id = multibuffer.update(cx, |multibuffer, cx| { + multibuffer.insert_excerpt_after( &prev_excerpt_id, ExcerptProperties { buffer: &buffer_handle, @@ -2662,12 +2669,12 @@ mod tests { } if rng.gen_bool(0.3) { - list.update(cx, |list, cx| { - old_versions.push((list.snapshot(cx), list.subscribe())); + multibuffer.update(cx, |multibuffer, cx| { + old_versions.push((multibuffer.snapshot(cx), multibuffer.subscribe())); }) } - let snapshot = list.read(cx).snapshot(cx); + let snapshot = multibuffer.read(cx).snapshot(cx); let mut excerpt_starts = Vec::new(); let mut expected_text = String::new(); @@ -2862,15 +2869,30 @@ mod tests { let end_ix = text_rope.clip_offset(rng.gen_range(0..=text_rope.len()), Bias::Right); let start_ix = text_rope.clip_offset(rng.gen_range(0..=end_ix), Bias::Left); + let text_for_range = snapshot + .text_for_range(start_ix..end_ix) + .collect::(); assert_eq!( - snapshot - .text_for_range(start_ix..end_ix) - .collect::(), + text_for_range, &expected_text[start_ix..end_ix], "incorrect text for range {:?}", start_ix..end_ix ); + let excerpted_buffer_ranges = + multibuffer.read(cx).excerpted_buffers(start_ix..end_ix, cx); + let excerpted_buffers_text = excerpted_buffer_ranges + .into_iter() + .map(|(buffer, buffer_range)| { + buffer + .read(cx) + .text_for_range(buffer_range) + .collect::() + }) + .collect::>() + .join("\n"); + assert_eq!(excerpted_buffers_text, text_for_range); + let expected_summary = TextSummary::from(&expected_text[start_ix..end_ix]); assert_eq!( snapshot.text_summary_for_range::(start_ix..end_ix), @@ -2904,7 +2926,7 @@ mod tests { } } - let snapshot = list.read(cx).snapshot(cx); + let snapshot = multibuffer.read(cx).snapshot(cx); for (old_snapshot, subscription) in old_versions { let edits = subscription.consume().into_inner(); diff --git a/crates/workspace/src/pane.rs b/crates/workspace/src/pane.rs index c8b2c5f8687480e3ffcd18ac78b338fdb12c1265..67f92ad615cb459d38087b54cfbf8e5b7565ea4c 100644 --- a/crates/workspace/src/pane.rs +++ b/crates/workspace/src/pane.rs @@ -1,5 +1,5 @@ use super::{ItemViewHandle, SplitDirection}; -use crate::{ItemHandle, Settings}; +use crate::{ItemHandle, Settings, Workspace}; use gpui::{ action, elements::*, @@ -90,6 +90,7 @@ impl Pane { pub fn open_item( &mut self, item_handle: T, + workspace: &Workspace, cx: &mut ViewContext, ) -> Box where @@ -103,7 +104,7 @@ impl Pane { } } - let item_view = item_handle.add_view(cx.window_id(), self.settings.clone(), cx); + let item_view = item_handle.add_view(cx.window_id(), workspace, cx); self.add_item_view(item_view.boxed_clone(), cx); item_view } diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 055fa1169db25480b0984beb2753285a8da78528..ccb5d9799ca6c5ce3d7ec9ef6f52a9213745ce22 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -18,7 +18,7 @@ use gpui::{ platform::{CursorStyle, WindowOptions}, AnyViewHandle, AppContext, ClipboardItem, Entity, ModelContext, ModelHandle, MutableAppContext, PathPromptOptions, PromptLevel, RenderContext, Task, View, ViewContext, ViewHandle, - WeakModelHandle, + WeakModelHandle, WeakViewHandle, }; use language::LanguageRegistry; use log::error; @@ -131,7 +131,7 @@ pub trait Item: Entity + Sized { fn build_view( handle: ModelHandle, - settings: watch::Receiver, + workspace: &Workspace, cx: &mut ViewContext, ) -> Self::View; @@ -181,7 +181,7 @@ pub trait ItemHandle: Send + Sync { fn add_view( &self, window_id: usize, - settings: watch::Receiver, + workspace: &Workspace, cx: &mut MutableAppContext, ) -> Box; fn boxed_clone(&self) -> Box; @@ -224,10 +224,10 @@ impl ItemHandle for ModelHandle { fn add_view( &self, window_id: usize, - settings: watch::Receiver, + workspace: &Workspace, cx: &mut MutableAppContext, ) -> Box { - Box::new(cx.add_view(window_id, |cx| T::build_view(self.clone(), settings, cx))) + Box::new(cx.add_view(window_id, |cx| T::build_view(self.clone(), workspace, cx))) } fn boxed_clone(&self) -> Box { @@ -251,10 +251,10 @@ impl ItemHandle for Box { fn add_view( &self, window_id: usize, - settings: watch::Receiver, + workspace: &Workspace, cx: &mut MutableAppContext, ) -> Box { - ItemHandle::add_view(self.as_ref(), window_id, settings, cx) + ItemHandle::add_view(self.as_ref(), window_id, workspace, cx) } fn boxed_clone(&self) -> Box { @@ -455,6 +455,7 @@ impl WorkspaceParams { pub struct Workspace { pub settings: watch::Receiver, + weak_self: WeakViewHandle, client: Arc, user_store: ModelHandle, fs: Arc, @@ -509,6 +510,7 @@ impl Workspace { Workspace { modal: None, + weak_self: cx.weak_handle(), center: PaneGroup::new(pane.id()), panes: vec![pane.clone()], active_pane: pane.clone(), @@ -526,6 +528,14 @@ impl Workspace { } } + pub fn weak_handle(&self) -> WeakViewHandle { + self.weak_self.clone() + } + + pub fn settings(&self) -> watch::Receiver { + self.settings.clone() + } + pub fn left_sidebar_mut(&mut self) -> &mut Sidebar { &mut self.left_sidebar } @@ -914,7 +924,7 @@ impl Workspace { T: 'static + ItemHandle, { self.items.insert(item_handle.downgrade()); - pane.update(cx, |pane, cx| pane.open_item(item_handle, cx)) + pane.update(cx, |pane, cx| pane.open_item(item_handle, self, cx)) } fn activate_pane(&mut self, pane: ViewHandle, cx: &mut ViewContext) { From ce6f3d7f3e0c8ff663bfd42e44771be659b7e919 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Fri, 7 Jan 2022 11:00:12 -0800 Subject: [PATCH 37/43] Reuse views when moving between diagnostic view and editors Co-Authored-By: Nathan Sobo --- crates/diagnostics/src/diagnostics.rs | 13 +++++++-- crates/editor/src/items.rs | 4 +++ crates/gpui/src/app.rs | 25 ++++++++++++++++ crates/workspace/src/pane.rs | 34 +++++++++++++++------- crates/workspace/src/workspace.rs | 41 +++++++++++++++++++++++++-- crates/zed/src/zed.rs | 7 ++--- 6 files changed, 103 insertions(+), 21 deletions(-) diff --git a/crates/diagnostics/src/diagnostics.rs b/crates/diagnostics/src/diagnostics.rs index fd710d596b32f83de03036daccc5646f866cbdff..99fa7404519f30541e98d304bdf3e2ff00e0f1d7 100644 --- a/crates/diagnostics/src/diagnostics.rs +++ b/crates/diagnostics/src/diagnostics.rs @@ -165,8 +165,13 @@ impl ProjectDiagnosticsEditor { } fn deploy(workspace: &mut Workspace, _: &Deploy, cx: &mut ViewContext) { - let diagnostics = cx.add_model(|_| ProjectDiagnostics::new(workspace.project().clone())); - workspace.open_item(diagnostics, cx); + if let Some(existing) = workspace.item_of_type::(cx) { + workspace.activate_pane_for_item(&existing, cx); + } else { + let diagnostics = + cx.add_model(|_| ProjectDiagnostics::new(workspace.project().clone())); + workspace.open_item(diagnostics, cx); + } } fn open_excerpts(&mut self, _: &OpenExcerpts, cx: &mut ViewContext) { @@ -191,8 +196,10 @@ impl ProjectDiagnosticsEditor { workspace.update(cx, |workspace, cx| { for (buffer, ranges) in new_selections_by_buffer { + let buffer = BufferItemHandle(buffer); + workspace.activate_pane_for_item(&buffer, cx); let editor = workspace - .open_item(BufferItemHandle(buffer), cx) + .open_item(buffer, cx) .to_any() .downcast::() .unwrap(); diff --git a/crates/editor/src/items.rs b/crates/editor/src/items.rs index 8f5745cc2aa3fca45bbd126c61caad82105838d1..b97f01ce69608944a229a07eadb496520d380e9d 100644 --- a/crates/editor/src/items.rs +++ b/crates/editor/src/items.rs @@ -62,6 +62,10 @@ impl ItemHandle for BufferItemHandle { Box::new(self.clone()) } + fn to_any(&self) -> gpui::AnyModelHandle { + self.0.clone().into() + } + fn downgrade(&self) -> Box { Box::new(WeakBufferItemHandle(self.0.downgrade())) } diff --git a/crates/gpui/src/app.rs b/crates/gpui/src/app.rs index 82ec8fbceaf6afdc98a00b38c28cd8e6952de638..5b70981ba2239189ca0557ef1460047e87286e2b 100644 --- a/crates/gpui/src/app.rs +++ b/crates/gpui/src/app.rs @@ -3097,14 +3097,39 @@ impl Drop for AnyViewHandle { pub struct AnyModelHandle { model_id: usize, + model_type: TypeId, ref_counts: Arc>, } +impl AnyModelHandle { + pub fn downcast(self) -> Option> { + if self.is::() { + let result = Some(ModelHandle { + model_id: self.model_id, + model_type: PhantomData, + ref_counts: self.ref_counts.clone(), + }); + unsafe { + Arc::decrement_strong_count(&self.ref_counts); + } + std::mem::forget(self); + result + } else { + None + } + } + + pub fn is(&self) -> bool { + self.model_type == TypeId::of::() + } +} + impl From> for AnyModelHandle { fn from(handle: ModelHandle) -> Self { handle.ref_counts.lock().inc_model(handle.model_id); Self { model_id: handle.model_id, + model_type: TypeId::of::(), ref_counts: handle.ref_counts.clone(), } } diff --git a/crates/workspace/src/pane.rs b/crates/workspace/src/pane.rs index 67f92ad615cb459d38087b54cfbf8e5b7565ea4c..e7e1383e17c1dbbf2202a26d245d007d065ceedd 100644 --- a/crates/workspace/src/pane.rs +++ b/crates/workspace/src/pane.rs @@ -69,7 +69,7 @@ pub struct TabState { } pub struct Pane { - item_views: Vec>, + item_views: Vec<(usize, Box)>, active_item: usize, settings: watch::Receiver, } @@ -96,8 +96,8 @@ impl Pane { where T: 'static + ItemHandle, { - for (ix, item_view) in self.item_views.iter().enumerate() { - if item_view.item_handle(cx).id() == item_handle.id() { + for (ix, (item_id, item_view)) in self.item_views.iter().enumerate() { + if *item_id == item_handle.id() { let item_view = item_view.boxed_clone(); self.activate_item(ix, cx); return item_view; @@ -116,21 +116,33 @@ impl Pane { ) { item_view.added_to_pane(cx); let item_idx = cmp::min(self.active_item + 1, self.item_views.len()); - self.item_views.insert(item_idx, item_view); + self.item_views + .insert(item_idx, (item_view.item_handle(cx).id(), item_view)); self.activate_item(item_idx, cx); cx.notify(); } - pub fn item_views(&self) -> &[Box] { - &self.item_views + pub fn contains_item(&self, item: &dyn ItemHandle) -> bool { + let item_id = item.id(); + self.item_views + .iter() + .any(|(existing_item_id, _)| *existing_item_id == item_id) + } + + pub fn item_views(&self) -> impl Iterator> { + self.item_views.iter().map(|(_, view)| view) } pub fn active_item(&self) -> Option> { - self.item_views.get(self.active_item).cloned() + self.item_views + .get(self.active_item) + .map(|(_, view)| view.clone()) } pub fn item_index(&self, item: &dyn ItemViewHandle) -> Option { - self.item_views.iter().position(|i| i.id() == item.id()) + self.item_views + .iter() + .position(|(_, i)| i.id() == item.id()) } pub fn activate_item(&mut self, index: usize, cx: &mut ViewContext) { @@ -163,12 +175,12 @@ impl Pane { pub fn close_active_item(&mut self, cx: &mut ViewContext) { if !self.item_views.is_empty() { - self.close_item(self.item_views[self.active_item].id(), cx) + self.close_item(self.item_views[self.active_item].1.id(), cx) } } pub fn close_item(&mut self, item_id: usize, cx: &mut ViewContext) { - self.item_views.retain(|item| item.id() != item_id); + self.item_views.retain(|(_, item)| item.id() != item_id); self.active_item = cmp::min(self.active_item, self.item_views.len().saturating_sub(1)); if self.item_views.is_empty() { cx.emit(Event::Remove); @@ -193,7 +205,7 @@ impl Pane { enum Tabs {} let tabs = MouseEventHandler::new::(cx.view_id(), cx, |mouse_state, cx| { let mut row = Flex::row(); - for (ix, item_view) in self.item_views.iter().enumerate() { + for (ix, (_, item_view)) in self.item_views.iter().enumerate() { let is_active = ix == self.active_item; row.add_child({ diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index ccb5d9799ca6c5ce3d7ec9ef6f52a9213745ce22..55538c09dd691413817fbe3ec4115d2151c317c3 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -16,9 +16,9 @@ use gpui::{ json::{self, to_string_pretty, ToJson}, keymap::Binding, platform::{CursorStyle, WindowOptions}, - AnyViewHandle, AppContext, ClipboardItem, Entity, ModelContext, ModelHandle, MutableAppContext, - PathPromptOptions, PromptLevel, RenderContext, Task, View, ViewContext, ViewHandle, - WeakModelHandle, WeakViewHandle, + AnyModelHandle, AnyViewHandle, AppContext, ClipboardItem, Entity, ModelContext, ModelHandle, + MutableAppContext, PathPromptOptions, PromptLevel, RenderContext, Task, View, ViewContext, + ViewHandle, WeakModelHandle, WeakViewHandle, }; use language::LanguageRegistry; use log::error; @@ -186,6 +186,7 @@ pub trait ItemHandle: Send + Sync { ) -> Box; fn boxed_clone(&self) -> Box; fn downgrade(&self) -> Box; + fn to_any(&self) -> AnyModelHandle; fn project_path(&self, cx: &AppContext) -> Option; } @@ -238,6 +239,10 @@ impl ItemHandle for ModelHandle { Box::new(self.downgrade()) } + fn to_any(&self) -> AnyModelHandle { + self.clone().into() + } + fn project_path(&self, cx: &AppContext) -> Option { self.read(cx).project_path() } @@ -265,6 +270,10 @@ impl ItemHandle for Box { self.as_ref().downgrade() } + fn to_any(&self) -> AnyModelHandle { + self.as_ref().to_any() + } + fn project_path(&self, cx: &AppContext) -> Option { self.as_ref().project_path(cx) } @@ -760,6 +769,12 @@ impl Workspace { .find(|i| i.project_path(cx).as_ref() == Some(path)) } + pub fn item_of_type(&self, cx: &AppContext) -> Option> { + self.items + .iter() + .find_map(|i| i.upgrade(cx).and_then(|i| i.to_any().downcast())) + } + pub fn active_item(&self, cx: &AppContext) -> Option> { self.active_pane().read(cx).active_item() } @@ -927,6 +942,26 @@ impl Workspace { pane.update(cx, |pane, cx| pane.open_item(item_handle, self, cx)) } + pub fn activate_pane_for_item( + &mut self, + item: &dyn ItemHandle, + cx: &mut ViewContext, + ) -> bool { + let pane = self.panes.iter().find_map(|pane| { + if pane.read(cx).contains_item(item) { + Some(pane.clone()) + } else { + None + } + }); + if let Some(pane) = pane { + self.activate_pane(pane.clone(), cx); + true + } else { + false + } + } + fn activate_pane(&mut self, pane: ViewHandle, cx: &mut ViewContext) { self.active_pane = pane; self.status_bar.update(cx, |status_bar, cx| { diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index d2da4fdd53e3d8e3b5633485595a20e2db22fa0a..31fbc4ed785549fb38d0806825e63ada9cc5d509 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -273,7 +273,7 @@ mod tests { pane.active_item().unwrap().project_path(cx), Some(file1.clone()) ); - assert_eq!(pane.item_views().len(), 1); + assert_eq!(pane.item_views().count(), 1); }); // Open the second entry @@ -287,7 +287,7 @@ mod tests { pane.active_item().unwrap().project_path(cx), Some(file2.clone()) ); - assert_eq!(pane.item_views().len(), 2); + assert_eq!(pane.item_views().count(), 2); }); // Open the first entry again. The existing pane item is activated. @@ -303,7 +303,7 @@ mod tests { pane.active_item().unwrap().project_path(cx), Some(file1.clone()) ); - assert_eq!(pane.item_views().len(), 2); + assert_eq!(pane.item_views().count(), 2); }); // Split the pane with the first entry, then open the second entry again. @@ -343,7 +343,6 @@ mod tests { ); let pane_entries = pane .item_views() - .iter() .map(|i| i.project_path(cx).unwrap()) .collect::>(); assert_eq!(pane_entries, &[file1, file2, file3]); From f933d54469791ac360b0b9b7b50c5415f4db92eb Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Fri, 7 Jan 2022 14:53:33 -0800 Subject: [PATCH 38/43] When selections lose their excerpts, move them to the next primary diagnostic --- crates/diagnostics/src/diagnostics.rs | 64 ++++++++++++----- crates/editor/src/editor.rs | 31 ++++++-- crates/editor/src/multi_buffer.rs | 100 ++++++++++++++++++++------ 3 files changed, 149 insertions(+), 46 deletions(-) diff --git a/crates/diagnostics/src/diagnostics.rs b/crates/diagnostics/src/diagnostics.rs index 99fa7404519f30541e98d304bdf3e2ff00e0f1d7..3d14aea1a6e903ebbb3b0f3805655e8f905180a2 100644 --- a/crates/diagnostics/src/diagnostics.rs +++ b/crates/diagnostics/src/diagnostics.rs @@ -6,7 +6,7 @@ use editor::{ context_header_renderer, diagnostic_block_renderer, diagnostic_header_renderer, display_map::{BlockDisposition, BlockId, BlockProperties}, items::BufferItemHandle, - Autoscroll, BuildSettings, Editor, ExcerptId, ExcerptProperties, MultiBuffer, + Autoscroll, BuildSettings, Editor, ExcerptId, ExcerptProperties, MultiBuffer, ToOffset, }; use gpui::{ action, elements::*, keymap::Binding, AppContext, Entity, ModelHandle, MutableAppContext, @@ -56,6 +56,7 @@ struct ProjectDiagnosticsEditor { struct DiagnosticGroupState { primary_diagnostic: DiagnosticEntry, + primary_excerpt_ix: usize, excerpts: Vec, blocks: HashMap, block_count: usize, @@ -300,6 +301,7 @@ impl ProjectDiagnosticsEditor { if let Some(group) = to_insert { let mut group_state = DiagnosticGroupState { primary_diagnostic: group.entries[group.primary_ix].clone(), + primary_excerpt_ix: 0, excerpts: Default::default(), blocks: Default::default(), block_count: 0, @@ -370,6 +372,7 @@ impl ProjectDiagnosticsEditor { for entry in &group.entries[*start_ix..ix] { let mut diagnostic = entry.diagnostic.clone(); if diagnostic.is_primary { + group_state.primary_excerpt_ix = group_state.excerpts.len() - 1; diagnostic.message = entry.diagnostic.message.split('\n').skip(1).collect(); } @@ -433,22 +436,6 @@ impl ProjectDiagnosticsEditor { for group_state in &mut groups_to_add { group_state.blocks = block_ids.by_ref().take(group_state.block_count).collect(); } - - if was_empty { - editor.update_selections( - vec![Selection { - id: 0, - start: 0, - end: 0, - reversed: false, - goal: SelectionGoal::None, - }], - None, - cx, - ); - } else { - editor.refresh_selections(cx); - } }); for ix in group_ixs_to_remove.into_iter().rev() { @@ -469,6 +456,49 @@ impl ProjectDiagnosticsEditor { self.path_states.remove(path_ix); } + self.editor.update(cx, |editor, cx| { + let groups = self.path_states.get(path_ix)?.1.as_slice(); + + let mut selections; + let new_excerpt_ids_by_selection_id; + if was_empty { + new_excerpt_ids_by_selection_id = [(0, ExcerptId::min())].into_iter().collect(); + selections = vec![Selection { + id: 0, + start: 0, + end: 0, + reversed: false, + goal: SelectionGoal::None, + }]; + } else { + new_excerpt_ids_by_selection_id = editor.refresh_selections(cx); + selections = editor.local_selections::(cx); + } + + // If any selection has lost its position, move it to start of the next primary diagnostic. + for selection in &mut selections { + if let Some(new_excerpt_id) = new_excerpt_ids_by_selection_id.get(&selection.id) { + let group_ix = match groups.binary_search_by(|probe| { + probe.excerpts.last().unwrap().cmp(&new_excerpt_id) + }) { + Ok(ix) | Err(ix) => ix, + }; + if let Some(group) = groups.get(group_ix) { + let offset = excerpts_snapshot + .anchor_in_excerpt( + group.excerpts[group.primary_excerpt_ix].clone(), + group.primary_diagnostic.range.start.clone(), + ) + .to_offset(&excerpts_snapshot); + selection.start = offset; + selection.end = offset; + } + } + } + editor.update_selections(selections, None, cx); + Some(()) + }); + if self.path_states.is_empty() { if self.editor.is_focused(cx) { cx.focus_self(); diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index f7babdf8afa0f6bd0980019ae1e3c950f5e3401f..c8d872f67f0e1cb92a19b83ba7a562a03b960d38 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -28,8 +28,8 @@ use language::{ BracketPair, Buffer, Diagnostic, DiagnosticSeverity, Language, Point, Selection, SelectionGoal, TransactionId, }; -pub use multi_buffer::{Anchor, ExcerptId, ExcerptProperties, MultiBuffer}; -use multi_buffer::{AnchorRangeExt, MultiBufferChunks, MultiBufferSnapshot, ToOffset, ToPoint}; +pub use multi_buffer::{Anchor, ExcerptId, ExcerptProperties, MultiBuffer, ToOffset, ToPoint}; +use multi_buffer::{AnchorRangeExt, MultiBufferChunks, MultiBufferSnapshot}; use postage::watch; use serde::{Deserialize, Serialize}; use smallvec::SmallVec; @@ -3297,8 +3297,14 @@ impl Editor { ); } - pub fn refresh_selections(&mut self, cx: &mut ViewContext) { - let anchors = self.buffer.update(cx, |buffer, cx| { + /// Compute new ranges for any selections that were located in excerpts that have + /// since been removed. + /// + /// Returns a `HashMap` indicating which selections whose former head position + /// was no longer present. The keys of the map are selection ids. The values are + /// the id of the new excerpt where the head of the selection has been moved. + pub fn refresh_selections(&mut self, cx: &mut ViewContext) -> HashMap { + let anchors_with_status = self.buffer.update(cx, |buffer, cx| { let snapshot = buffer.read(cx); snapshot.refresh_anchors( self.selections @@ -3306,17 +3312,28 @@ impl Editor { .flat_map(|selection| [&selection.start, &selection.end]), ) }); + let mut selections_with_lost_position = HashMap::default(); self.selections = self .selections .iter() .cloned() - .zip(anchors.chunks(2)) + .zip(anchors_with_status.chunks(2)) .map(|(mut selection, anchors)| { - selection.start = anchors[0].clone(); - selection.end = anchors[1].clone(); + selection.start = anchors[0].0.clone(); + selection.end = anchors[1].0.clone(); + let kept_head_position = if selection.reversed { + anchors[0].1 + } else { + anchors[1].1 + }; + if !kept_head_position { + selections_with_lost_position + .insert(selection.id, selection.head().excerpt_id.clone()); + } selection }) .collect(); + selections_with_lost_position } fn set_selections(&mut self, selections: Arc<[Selection]>, cx: &mut ViewContext) { diff --git a/crates/editor/src/multi_buffer.rs b/crates/editor/src/multi_buffer.rs index 47431c36fca2dd404dbeecd0727492d11b6f7dd5..28d935d2ec54144c381c4f4eeef7aca02c30198d 100644 --- a/crates/editor/src/multi_buffer.rs +++ b/crates/editor/src/multi_buffer.rs @@ -1342,7 +1342,7 @@ impl MultiBufferSnapshot { position } - pub fn refresh_anchors<'a, I>(&'a self, anchors: I) -> Vec + pub fn refresh_anchors<'a, I>(&'a self, anchors: I) -> Vec<(Anchor, bool)> where I: 'a + IntoIterator, { @@ -1352,7 +1352,7 @@ impl MultiBufferSnapshot { while let Some(anchor) = anchors.peek() { let old_excerpt_id = &anchor.excerpt_id; - // Find the location where this anchor's excerpt should be, + // Find the location where this anchor's excerpt should be. cursor.seek_forward(&Some(old_excerpt_id), Bias::Left, &()); if cursor.item().is_none() { cursor.next(&()); @@ -1366,32 +1366,58 @@ impl MultiBufferSnapshot { if anchor.excerpt_id != *old_excerpt_id { break; } + let mut kept_position = false; let mut anchor = anchors.next().unwrap().clone(); + // Leave min and max anchors unchanged. + if *old_excerpt_id == ExcerptId::max() || *old_excerpt_id == ExcerptId::min() { + kept_position = true; + } + // If the old excerpt still exists at this location, then leave + // the anchor unchanged. + else if next_excerpt.map_or(false, |excerpt| { + excerpt.id == *old_excerpt_id && excerpt.buffer_id == anchor.buffer_id + }) { + kept_position = true; + } // If the old excerpt no longer exists at this location, then attempt to // find an equivalent position for this anchor in an adjacent excerpt. - if next_excerpt.map_or(true, |e| e.id != *old_excerpt_id) { + else { for excerpt in [next_excerpt, prev_excerpt].iter().filter_map(|e| *e) { - if excerpt.buffer_id == anchor.buffer_id - && excerpt - .range - .start - .cmp(&anchor.text_anchor, &excerpt.buffer) - .unwrap() - .is_le() - && excerpt - .range - .end - .cmp(&anchor.text_anchor, &excerpt.buffer) - .unwrap() - .is_ge() - { + if excerpt.contains(&anchor) { anchor.excerpt_id = excerpt.id.clone(); + kept_position = true; + break; } } } + // If there's no adjacent excerpt that contains the anchor's position, + // then report that the anchor has lost its position. + if !kept_position { + anchor = if let Some(excerpt) = next_excerpt { + Anchor { + buffer_id: excerpt.buffer_id, + excerpt_id: excerpt.id.clone(), + text_anchor: excerpt + .buffer + .anchor_at(&excerpt.range.start, anchor.text_anchor.bias), + } + } else if let Some(excerpt) = prev_excerpt { + Anchor { + buffer_id: excerpt.buffer_id, + excerpt_id: excerpt.id.clone(), + text_anchor: excerpt + .buffer + .anchor_at(&excerpt.range.end, anchor.text_anchor.bias), + } + } else if anchor.text_anchor.bias == Bias::Left { + Anchor::min() + } else { + Anchor::max() + }; + } - result.push(anchor); + result.push((anchor, kept_position)); } } result @@ -1894,6 +1920,22 @@ impl Excerpt { text_anchor } } + + fn contains(&self, anchor: &Anchor) -> bool { + self.buffer_id == anchor.buffer_id + && self + .range + .start + .cmp(&anchor.text_anchor, &self.buffer) + .unwrap() + .is_le() + && self + .range + .end + .cmp(&anchor.text_anchor, &self.buffer) + .unwrap() + .is_ge() + } } impl fmt::Debug for Excerpt { @@ -2523,7 +2565,7 @@ mod tests { let snapshot_2 = multibuffer.read(cx).snapshot(cx); assert_eq!(snapshot_2.text(), "ABCD\nGHIJ\nMNOP"); - // And excerpt id has been reused. + // The old excerpt id has been reused. assert_eq!(excerpt_id_2, excerpt_id_1); // Resolve some anchors from the previous snapshot in the new snapshot. @@ -2541,6 +2583,15 @@ mod tests { ]), vec![0, 0] ); + let refresh = + snapshot_2.refresh_anchors(&[snapshot_1.anchor_before(2), snapshot_1.anchor_after(3)]); + assert_eq!( + refresh, + &[ + (snapshot_2.anchor_before(0), false), + (snapshot_2.anchor_after(0), false), + ] + ); // Replace the middle excerpt with a smaller excerpt in buffer 2, // that intersects the old excerpt. @@ -2564,19 +2615,24 @@ mod tests { // The anchor in the middle excerpt snaps to the beginning of the // excerpt, since it is not let anchors = [ + snapshot_2.anchor_before(0), snapshot_2.anchor_after(2), snapshot_2.anchor_after(6), snapshot_2.anchor_after(14), ]; assert_eq!( snapshot_3.summaries_for_anchors::(&anchors), - &[2, 9, 13] + &[0, 2, 9, 13] ); let new_anchors = snapshot_3.refresh_anchors(&anchors); assert_eq!( - snapshot_3.summaries_for_anchors::(&new_anchors), - &[2, 7, 13] + new_anchors.iter().map(|a| a.1).collect::>(), + &[true, true, true, true] + ); + assert_eq!( + snapshot_3.summaries_for_anchors::(new_anchors.iter().map(|a| &a.0)), + &[0, 2, 7, 13] ); } From 1a53d5b7ba61bc020f3f5ad624b5327391a13dca Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Mon, 10 Jan 2022 10:10:11 +0100 Subject: [PATCH 39/43] Use a new `Workspace::activate_item` API in project diagnostics Previously, we would only activate the pane without switching the pane's *active item*. --- crates/diagnostics/src/diagnostics.rs | 2 +- crates/workspace/src/pane.rs | 8 ++++++-- crates/workspace/src/workspace.rs | 19 ++++++++++++++++++- 3 files changed, 25 insertions(+), 4 deletions(-) diff --git a/crates/diagnostics/src/diagnostics.rs b/crates/diagnostics/src/diagnostics.rs index 3d14aea1a6e903ebbb3b0f3805655e8f905180a2..f9cbb68aab6ba2e70da4121bfeee4c2828c29f83 100644 --- a/crates/diagnostics/src/diagnostics.rs +++ b/crates/diagnostics/src/diagnostics.rs @@ -167,7 +167,7 @@ impl ProjectDiagnosticsEditor { fn deploy(workspace: &mut Workspace, _: &Deploy, cx: &mut ViewContext) { if let Some(existing) = workspace.item_of_type::(cx) { - workspace.activate_pane_for_item(&existing, cx); + workspace.activate_item(&existing, cx); } else { let diagnostics = cx.add_model(|_| ProjectDiagnostics::new(workspace.project().clone())); diff --git a/crates/workspace/src/pane.rs b/crates/workspace/src/pane.rs index e7e1383e17c1dbbf2202a26d245d007d065ceedd..731db29d634049ea6cebeec5b2369c4a98fa63fa 100644 --- a/crates/workspace/src/pane.rs +++ b/crates/workspace/src/pane.rs @@ -139,10 +139,14 @@ impl Pane { .map(|(_, view)| view.clone()) } - pub fn item_index(&self, item: &dyn ItemViewHandle) -> Option { + pub fn index_for_item_view(&self, item_view: &dyn ItemViewHandle) -> Option { self.item_views .iter() - .position(|(_, i)| i.id() == item.id()) + .position(|(_, i)| i.id() == item_view.id()) + } + + pub fn index_for_item(&self, item: &dyn ItemHandle) -> Option { + self.item_views.iter().position(|(id, _)| *id == item.id()) } pub fn activate_item(&mut self, index: usize, cx: &mut ViewContext) { diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 55538c09dd691413817fbe3ec4115d2151c317c3..ea6eb6603140108238ff916e605fb787451e12f3 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -334,7 +334,7 @@ impl ItemViewHandle for ViewHandle { return; } if T::should_activate_item_on_event(event) { - if let Some(ix) = pane.item_index(&item) { + if let Some(ix) = pane.index_for_item_view(&item) { pane.activate_item(ix, cx); pane.activate(cx); } @@ -962,6 +962,23 @@ impl Workspace { } } + pub fn activate_item(&mut self, item: &dyn ItemHandle, cx: &mut ViewContext) -> bool { + let result = self.panes.iter().find_map(|pane| { + if let Some(ix) = pane.read(cx).index_for_item(item) { + Some((pane.clone(), ix)) + } else { + None + } + }); + if let Some((pane, ix)) = result { + self.activate_pane(pane.clone(), cx); + pane.update(cx, |pane, cx| pane.activate_item(ix, cx)); + true + } else { + false + } + } + fn activate_pane(&mut self, pane: ViewHandle, cx: &mut ViewContext) { self.active_pane = pane; self.status_bar.update(cx, |status_bar, cx| { From 0742640b393f7680d60fac93fb85d89780761390 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Mon, 10 Jan 2022 11:26:48 +0100 Subject: [PATCH 40/43] Correctly report line boundaries when a map contains both folds and wraps This fixes the randomized test failures that were occurring on main. --- crates/editor/src/display_map.rs | 10 ++++++++-- crates/editor/src/display_map/fold_map.rs | 2 -- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/crates/editor/src/display_map.rs b/crates/editor/src/display_map.rs index 342ef90b3845022db2e7fdfdee91b5d807d485c6..faf770cb1bde70fea6ab314f542395223a3c84c6 100644 --- a/crates/editor/src/display_map.rs +++ b/crates/editor/src/display_map.rs @@ -199,7 +199,10 @@ impl DisplaySnapshot { pub fn prev_line_boundary(&self, mut point: Point) -> (Point, DisplayPoint) { loop { - point.column = 0; + let mut fold_point = point.to_fold_point(&self.folds_snapshot, Bias::Left); + *fold_point.column_mut() = 0; + point = fold_point.to_buffer_point(&self.folds_snapshot); + let mut display_point = self.point_to_display_point(point, Bias::Left); *display_point.column_mut() = 0; let next_point = self.display_point_to_point(display_point, Bias::Left); @@ -212,7 +215,10 @@ impl DisplaySnapshot { pub fn next_line_boundary(&self, mut point: Point) -> (Point, DisplayPoint) { loop { - point.column = self.buffer_snapshot.line_len(point.row); + let mut fold_point = point.to_fold_point(&self.folds_snapshot, Bias::Right); + *fold_point.column_mut() = self.folds_snapshot.line_len(fold_point.row()); + point = fold_point.to_buffer_point(&self.folds_snapshot); + let mut display_point = self.point_to_display_point(point, Bias::Right); *display_point.column_mut() = self.line_len(display_point.row()); let next_point = self.display_point_to_point(display_point, Bias::Right); diff --git a/crates/editor/src/display_map/fold_map.rs b/crates/editor/src/display_map/fold_map.rs index 60dd40c85325cd92cbb6eefa0ea912139d5d4f7e..fd5f1de8f810ca0d1eb0d1c86f90627a0985cc83 100644 --- a/crates/editor/src/display_map/fold_map.rs +++ b/crates/editor/src/display_map/fold_map.rs @@ -37,7 +37,6 @@ impl FoldPoint { &mut self.0.row } - #[cfg(test)] pub fn column_mut(&mut self) -> &mut u32 { &mut self.0.column } @@ -549,7 +548,6 @@ impl FoldSnapshot { FoldOffset(self.transforms.summary().output.bytes) } - #[cfg(test)] pub fn line_len(&self, row: u32) -> u32 { let line_start = FoldPoint::new(row, 0).to_offset(self).0; let line_end = if row >= self.max_point().row() { From a1597578ff1afc30517bcdacfe934b4ae362d69b Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Mon, 10 Jan 2022 11:56:00 +0100 Subject: [PATCH 41/43] Compare singleton buffers in `test_open_and_save_new_file` --- crates/zed/src/zed.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index 31fbc4ed785549fb38d0806825e63ada9cc5d509..61300d1f56ad74785e9ccd16a4dfe8bd0cf391a4 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -577,7 +577,10 @@ mod tests { .unwrap() }); cx.read(|cx| { - assert_eq!(editor2.read(cx).buffer(), editor.read(cx).buffer()); + assert_eq!( + editor2.read(cx).buffer().read(cx).as_singleton().unwrap(), + editor.read(cx).buffer().read(cx).as_singleton().unwrap() + ); }) } From eb353648e6fdb36d7febbe715f56bfe63523e0d8 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Mon, 10 Jan 2022 12:14:52 +0100 Subject: [PATCH 42/43] :art: --- crates/diagnostics/src/diagnostics.rs | 20 ++++---------------- 1 file changed, 4 insertions(+), 16 deletions(-) diff --git a/crates/diagnostics/src/diagnostics.rs b/crates/diagnostics/src/diagnostics.rs index f9cbb68aab6ba2e70da4121bfeee4c2828c29f83..c1877b95e8e5ab03e2600e4f2b0f89b5e0948a9d 100644 --- a/crates/diagnostics/src/diagnostics.rs +++ b/crates/diagnostics/src/diagnostics.rs @@ -12,7 +12,7 @@ use gpui::{ action, elements::*, keymap::Binding, AppContext, Entity, ModelHandle, MutableAppContext, RenderContext, Task, View, ViewContext, ViewHandle, WeakViewHandle, }; -use language::{Bias, Buffer, Diagnostic, DiagnosticEntry, Point, Selection, SelectionGoal}; +use language::{Bias, Buffer, DiagnosticEntry, Point, Selection, SelectionGoal}; use postage::watch; use project::{Project, ProjectPath, WorktreeId}; use std::{cmp::Ordering, mem, ops::Range, path::Path, sync::Arc}; @@ -58,16 +58,10 @@ struct DiagnosticGroupState { primary_diagnostic: DiagnosticEntry, primary_excerpt_ix: usize, excerpts: Vec, - blocks: HashMap, + blocks: HashSet, block_count: usize, } -enum DiagnosticBlock { - Header(Diagnostic), - Inline(Diagnostic), - Context, -} - impl ProjectDiagnostics { fn new(project: ModelHandle) -> Self { Self { project } @@ -267,7 +261,6 @@ impl ProjectDiagnosticsEditor { let mut group_ixs_to_remove = Vec::new(); let mut blocks_to_add = Vec::new(); let mut blocks_to_remove = HashSet::default(); - let mut diagnostic_blocks = Vec::new(); let excerpts_snapshot = self.excerpts.update(cx, |excerpts, excerpts_cx| { let mut old_groups = groups.iter().enumerate().peekable(); let mut new_groups = snapshot @@ -346,7 +339,6 @@ impl ProjectDiagnosticsEditor { header.message = primary.message.split('\n').next().unwrap().to_string(); group_state.block_count += 1; - diagnostic_blocks.push(DiagnosticBlock::Header(header.clone())); blocks_to_add.push(BlockProperties { position: header_position, height: 3, @@ -360,7 +352,6 @@ impl ProjectDiagnosticsEditor { }); } else { group_state.block_count += 1; - diagnostic_blocks.push(DiagnosticBlock::Context); blocks_to_add.push(BlockProperties { position: header_position, height: 1, @@ -379,8 +370,6 @@ impl ProjectDiagnosticsEditor { if !diagnostic.message.is_empty() { group_state.block_count += 1; - diagnostic_blocks - .push(DiagnosticBlock::Inline(diagnostic.clone())); blocks_to_add.push(BlockProperties { position: (excerpt_id.clone(), entry.range.start.clone()), height: diagnostic.message.matches('\n').count() as u8 + 1, @@ -406,7 +395,7 @@ impl ProjectDiagnosticsEditor { } else if let Some((group_ix, group_state)) = to_invalidate { excerpts.remove_excerpts(group_state.excerpts.iter(), excerpts_cx); group_ixs_to_remove.push(group_ix); - blocks_to_remove.extend(group_state.blocks.keys().copied()); + blocks_to_remove.extend(group_state.blocks.iter().copied()); } else if let Some((_, group)) = to_keep { prev_excerpt_id = group.excerpts.last().unwrap().clone(); } @@ -430,8 +419,7 @@ impl ProjectDiagnosticsEditor { }), cx, ) - .into_iter() - .zip(diagnostic_blocks); + .into_iter(); for group_state in &mut groups_to_add { group_state.blocks = block_ids.by_ref().take(group_state.block_count).collect(); From 5c3ae8808ba74b6a672d97e6176e140b40939557 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Mon, 10 Jan 2022 14:28:25 +0100 Subject: [PATCH 43/43] Fix diagnostic unit test assertions --- crates/diagnostics/src/diagnostics.rs | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/crates/diagnostics/src/diagnostics.rs b/crates/diagnostics/src/diagnostics.rs index c1877b95e8e5ab03e2600e4f2b0f89b5e0948a9d..e70cd07f506d3e4a22276ab7e18de2743f5f59f8 100644 --- a/crates/diagnostics/src/diagnostics.rs +++ b/crates/diagnostics/src/diagnostics.rs @@ -590,6 +590,7 @@ fn compare_diagnostics( mod tests { use super::*; use client::{http::ServerResponse, test::FakeHttpClient, Client, UserStore}; + use editor::DisplayPoint; use gpui::TestAppContext; use language::{Diagnostic, DiagnosticEntry, DiagnosticSeverity, LanguageRegistry, PointUtf16}; use project::{worktree, FakeFs}; @@ -749,6 +750,7 @@ mod tests { // // main.rs, diagnostic group 1 // + "\n", // padding "\n", // primary message "\n", // filename " let x = vec![];\n", @@ -765,6 +767,7 @@ mod tests { // // main.rs, diagnostic group 2 // + "\n", // padding "\n", // primary message "\n", // filename "fn main() {\n", @@ -783,7 +786,10 @@ mod tests { ); view.editor.update(cx, |editor, cx| { - assert_eq!(editor.selected_ranges::(cx), [0..0]); + assert_eq!( + editor.selected_display_ranges(cx), + [DisplayPoint::new(11, 6)..DisplayPoint::new(11, 6)] + ); }); }); @@ -821,6 +827,7 @@ mod tests { // // a.rs // + "\n", // padding "\n", // primary message "\n", // filename "const a: i32 = 'a';\n", @@ -829,6 +836,7 @@ mod tests { // // main.rs, diagnostic group 1 // + "\n", // padding "\n", // primary message "\n", // filename " let x = vec![];\n", @@ -845,6 +853,7 @@ mod tests { // // main.rs, diagnostic group 2 // + "\n", // padding "\n", // primary message "\n", // filename "fn main() {\n",