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/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/diagnostics/src/diagnostics.rs b/crates/diagnostics/src/diagnostics.rs index 87d9c521ab677c99839555096980d8bc05813234..e70cd07f506d3e4a22276ab7e18de2743f5f59f8 100644 --- a/crates/diagnostics/src/diagnostics.rs +++ b/crates/diagnostics/src/diagnostics.rs @@ -1,37 +1,40 @@ +pub mod items; + use anyhow::Result; 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, ToOffset, }; 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}; +use language::{Bias, Buffer, DiagnosticEntry, Point, Selection, SelectionGoal}; use postage::watch; -use project::Project; -use std::{cmp::Ordering, ops::Range, path::Path, sync::Arc}; +use project::{Project, ProjectPath, WorktreeId}; +use std::{cmp::Ordering, mem, ops::Range, path::Path, sync::Arc}; use util::TryFutureExt; use workspace::Workspace; -action!(Toggle); -action!(ClearInvalid); +action!(Deploy); +action!(OpenExcerpts); 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-D", Deploy, Some("Workspace")), Binding::new( - "alt-shift-C", - ClearInvalid, + "alt-shift-D", + OpenExcerpts, Some("ProjectDiagnosticsEditor"), ), ]); - cx.add_action(ProjectDiagnosticsEditor::toggle); - cx.add_action(ProjectDiagnosticsEditor::clear_invalid); + cx.add_action(ProjectDiagnosticsEditor::deploy); + cx.add_action(ProjectDiagnosticsEditor::open_excerpts); } type Event = editor::Event; @@ -41,24 +44,22 @@ struct ProjectDiagnostics { } struct ProjectDiagnosticsEditor { + model: ModelHandle, + workspace: WeakViewHandle, editor: ViewHandle, excerpts: ModelHandle, path_states: Vec<(Arc, Vec)>, + paths_to_update: HashMap>, build_settings: BuildSettings, + settings: watch::Receiver, } struct DiagnosticGroupState { primary_diagnostic: DiagnosticEntry, + primary_excerpt_ix: usize, excerpts: Vec, - blocks: HashMap, + blocks: HashSet, block_count: usize, - is_valid: bool, -} - -enum DiagnosticBlock { - Header(Diagnostic), - Inline(Diagnostic), - Context, } impl ProjectDiagnostics { @@ -81,55 +82,49 @@ 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); + } } } impl ProjectDiagnosticsEditor { fn new( - project: ModelHandle, + model: ModelHandle, + workspace: WeakViewHandle, 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)) + 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) { + 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 +134,24 @@ 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 { + model, + workspace, excerpts, editor, build_settings, + settings, path_states: Default::default(), - } + paths_to_update: Default::default(), + }; + this.update_excerpts(paths_to_update, cx); + this } #[cfg(test)] @@ -152,41 +159,68 @@ impl ProjectDiagnosticsEditor { self.editor.read(cx).text(cx) } - fn toggle(workspace: &mut Workspace, _: &Toggle, cx: &mut ViewContext) { - let diagnostics = cx.add_model(|_| ProjectDiagnostics::new(workspace.project().clone())); - workspace.add_item(diagnostics, cx); + fn deploy(workspace: &mut Workspace, _: &Deploy, cx: &mut ViewContext) { + if let Some(existing) = workspace.item_of_type::(cx) { + workspace.activate_item(&existing, cx); + } else { + let diagnostics = + cx.add_model(|_| ProjectDiagnostics::new(workspace.project().clone())); + workspace.open_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 + fn open_excerpts(&mut self, _: &OpenExcerpts, cx: &mut ViewContext) { + 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) } - }); - - 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); + 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(buffer, cx) + .to_any() + .downcast::() + .unwrap(); + editor.update(cx, |editor, cx| { + editor.select_ranges(ranges, Some(Autoscroll::Center), cx) + }); + } + }); } + } - 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.model.read(cx).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) { @@ -202,6 +236,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()) @@ -225,18 +260,9 @@ 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() @@ -246,7 +272,7 @@ impl ProjectDiagnosticsEditor { loop { let mut to_insert = None; let mut to_invalidate = None; - let mut to_validate = None; + let mut to_keep = None; match (old_groups.peek(), new_groups.peek()) { (None, None) => break, (None, Some(_)) => to_insert = new_groups.next(), @@ -257,7 +283,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(); + to_keep = old_groups.next(); new_groups.next(); } Ordering::Greater => to_insert = new_groups.next(), @@ -268,10 +294,10 @@ 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, - is_valid: true, }; let mut pending_range: Option<(Range, usize)> = None; let mut is_first_excerpt_for_group = true; @@ -309,14 +335,16 @@ 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())); blocks_to_add.push(BlockProperties { position: header_position, - height: 2, + height: 3, render: diagnostic_header_renderer( buffer.clone(), - primary.clone(), + header, true, self.build_settings.clone(), ), @@ -324,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, @@ -334,17 +361,20 @@ 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 { + group_state.primary_excerpt_ix = group_state.excerpts.len() - 1; + 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())); 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(), ), @@ -363,76 +393,11 @@ 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.iter().copied()); + } else if let Some((_, group)) = to_keep { + prev_excerpt_id = group.excerpts.last().unwrap().clone(); } } @@ -441,7 +406,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| { @@ -455,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(); @@ -481,6 +444,58 @@ 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(); + } + } else { + if cx.handle().is_focused(cx) { + cx.focus(&self.editor); + } + } cx.notify(); } } @@ -490,11 +505,10 @@ impl workspace::Item for ProjectDiagnostics { fn build_view( handle: ModelHandle, - settings: watch::Receiver, + workspace: &Workspace, cx: &mut ViewContext, ) -> Self::View { - let project = handle.read(cx).project.clone(); - ProjectDiagnosticsEditor::new(project, settings, cx) + ProjectDiagnosticsEditor::new(handle, workspace.weak_handle(), workspace.settings(), cx) } fn project_path(&self) -> Option { @@ -503,6 +517,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() } @@ -511,10 +531,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, @@ -524,28 +560,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( @@ -570,9 +590,10 @@ 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}; - use project::FakeFs; + use language::{Diagnostic, DiagnosticEntry, DiagnosticSeverity, LanguageRegistry, PointUtf16}; + use project::{worktree, FakeFs}; use serde_json::json; use std::sync::Arc; use unindent::Unindent as _; @@ -580,7 +601,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)); @@ -629,11 +651,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 +669,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 +682,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 +693,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,9 +704,9 @@ mod tests { }, }, DiagnosticEntry { - range: 112..113, + 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, @@ -692,20 +715,9 @@ mod tests { }, }, DiagnosticEntry { - range: 112..113, + 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: 0, - ..Default::default() - }, - }, - DiagnosticEntry { - range: 122..123, - 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, @@ -713,25 +725,17 @@ mod tests { ..Default::default() }, }, - DiagnosticEntry { - range: 122..123, - 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, ) .unwrap(); }); - let view = cx.add_view(Default::default(), |cx| { - ProjectDiagnosticsEditor::new(project.clone(), settings, cx) + let model = cx.add_model(|_| ProjectDiagnostics::new(project.clone())); + 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()")) @@ -746,6 +750,7 @@ mod tests { // // main.rs, diagnostic group 1 // + "\n", // padding "\n", // primary message "\n", // filename " let x = vec![];\n", @@ -762,6 +767,7 @@ mod tests { // // main.rs, diagnostic group 2 // + "\n", // padding "\n", // primary message "\n", // filename "fn main() {\n", @@ -778,39 +784,35 @@ mod tests { "}" ) ); + + view.editor.update(cx, |editor, cx| { + assert_eq!( + editor.selected_display_ranges(cx), + [DisplayPoint::new(11, 6)..DisplayPoint::new(11, 6)] + ); + }); }); worktree.update(&mut cx, |worktree, cx| { worktree - .update_diagnostics_from_provider( + .update_diagnostic_entries( Arc::from("/test/a.rs".as_ref()), - vec![ - DiagnosticEntry { - range: 15..15, - diagnostic: Diagnostic { - message: "mismatched types".to_string(), - severity: DiagnosticSeverity::ERROR, - is_primary: true, - is_disk_based: true, - group_id: 0, - ..Default::default() - }, + None, + 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: 15..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(); + cx.emit(worktree::Event::DiskBasedDiagnosticsUpdated); }); view.condition(&mut cx, |view, cx| view.text(cx).contains("const a")) @@ -825,6 +827,7 @@ mod tests { // // a.rs // + "\n", // padding "\n", // primary message "\n", // filename "const a: i32 = 'a';\n", @@ -833,6 +836,7 @@ mod tests { // // main.rs, diagnostic group 1 // + "\n", // padding "\n", // primary message "\n", // filename " let x = vec![];\n", @@ -849,6 +853,7 @@ mod tests { // // main.rs, diagnostic group 2 // + "\n", // padding "\n", // primary message "\n", // filename "fn main() {\n", diff --git a/crates/diagnostics/src/items.rs b/crates/diagnostics/src/items.rs new file mode 100644 index 0000000000000000000000000000000000000000..072738fa77417542fbc7cdfa1036f8d55418c6e8 --- /dev/null +++ b/crates/diagnostics/src/items.rs @@ -0,0 +1,87 @@ +use gpui::{ + elements::*, platform::CursorStyle, Entity, ModelHandle, RenderContext, View, ViewContext, +}; +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 { + pub fn new( + project: &ModelHandle, + settings: watch::Receiver, + cx: &mut ViewContext, + ) -> Self { + 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(), + } + } +} + +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; + 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(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)) + .boxed() + } +} + +impl StatusItemView for DiagnosticSummary { + fn set_active_pane_item( + &mut self, + _: Option<&dyn workspace::ItemViewHandle>, + _: &mut ViewContext, + ) { + } +} diff --git a/crates/editor/src/display_map.rs b/crates/editor/src/display_map.rs index cf436971a53102047b2c1b43b1b7d8514051e255..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); @@ -446,10 +452,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 +500,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 +596,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/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() { 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/editor.rs b/crates/editor/src/editor.rs index d626f28e7cd974aa0800d452c1549650b830fc5a..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; @@ -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,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")), @@ -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, @@ -524,8 +524,7 @@ 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.add_item(BufferItemHandle(buffer), cx); + workspace.open_item(BufferItemHandle(buffer), cx); } pub fn replica_id(&self, cx: &AppContext) -> ReplicaId { @@ -565,15 +564,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(); } @@ -1049,6 +1055,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, @@ -1059,7 +1104,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); @@ -1078,7 +1123,8 @@ impl Editor { goal: SelectionGoal::None, } }) - .collect(); + .collect::>(); + selections.sort_unstable_by_key(|s| s.start); self.update_selections(selections, autoscroll, cx); } @@ -1564,7 +1610,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 +1635,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 +1644,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); } @@ -1629,7 +1680,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; @@ -1659,28 +1710,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); } @@ -2867,19 +2903,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, @@ -2907,7 +2943,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 @@ -2933,12 +2969,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); @@ -2947,7 +2978,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; @@ -3113,10 +3144,6 @@ impl Editor { .collect() } - pub fn local_anchor_selections(&self) -> &Arc<[Selection]> { - &self.selections - } - fn resolve_selections<'a, D, I>( &self, selections: I, @@ -3270,6 +3297,45 @@ impl Editor { ); } + /// 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 + .iter() + .flat_map(|selection| [&selection.start, &selection.end]), + ) + }); + let mut selections_with_lost_position = HashMap::default(); + self.selections = self + .selections + .iter() + .cloned() + .zip(anchors_with_status.chunks(2)) + .map(|(mut selection, anchors)| { + 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) { self.selections = selections; self.buffer.update(cx, |buffer, cx| { @@ -3650,10 +3716,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 } @@ -3786,6 +3856,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() @@ -3801,7 +3872,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 { @@ -3809,8 +3881,17 @@ 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()) + .aligned() + .left() + .contained() + .with_style(diagnostic_style.header) + .expanded() .boxed() }) } @@ -6160,45 +6241,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 diff --git a/crates/editor/src/items.rs b/crates/editor/src/items.rs index 6d49c9ae7be3a10e35f7f594ae65fb3f597579a3..b97f01ce69608944a229a07eadb496520d380e9d 100644 --- a/crates/editor/src/items.rs +++ b/crates/editor/src/items.rs @@ -5,25 +5,26 @@ 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, + Workspace, }; 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 +32,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) @@ -44,14 +44,15 @@ impl ItemHandle for BufferItemHandle { fn add_view( &self, window_id: usize, - settings: watch::Receiver, + workspace: &Workspace, 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), + buffer, + crate::settings_builder(weak_buffer, workspace.settings()), cx, ) })) @@ -61,16 +62,24 @@ 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())) } 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(), }) } + + fn id(&self) -> usize { + self.0.id() + } } impl WeakItemHandle for WeakBufferItemHandle { @@ -79,22 +88,17 @@ impl WeakItemHandle for WeakBufferItemHandle { .upgrade(cx) .map(|buffer| Box::new(BufferItemHandle(buffer)) as Box) } + + fn id(&self) -> usize { + self.0.id() + } } impl ItemView for Editor { - fn should_activate_item_on_event(event: &Event) -> bool { - matches!(event, Event::Activate) - } + type ItemHandle = BufferItemHandle; - fn should_close_item_on_event(event: &Event) -> bool { - matches!(event, Event::Closed) - } - - 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.read(cx).as_singleton().unwrap()) } fn title(&self, cx: &AppContext) -> String { @@ -124,6 +128,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 +148,10 @@ impl ItemView for Editor { })) } + fn can_save_as(&self, _: &AppContext) -> bool { + true + } + fn save_as( &mut self, worktree: ModelHandle, @@ -180,20 +200,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 + ) } } @@ -298,9 +317,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/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 { diff --git a/crates/editor/src/multi_buffer.rs b/crates/editor/src/multi_buffer.rs index 7e4826422158181039838b72e54cc22483b10bf6..28d935d2ec54144c381c4f4eeef7aca02c30198d 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 @@ -681,6 +689,38 @@ 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 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, end_before_newline) - *cursor.start()); + let buffer = self.buffers.borrow()[&excerpt.buffer_id].buffer.clone(); + result.push((buffer, start..end)); + cursor.next(&()); + } + + result + } + pub fn remove_excerpts<'a>( &mut self, excerpt_ids: impl IntoIterator, @@ -1291,7 +1331,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 { @@ -1302,6 +1342,87 @@ impl MultiBufferSnapshot { position } + pub fn refresh_anchors<'a, I>(&'a self, anchors: I) -> Vec<(Anchor, bool)> + 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 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. + else { + for excerpt in [next_excerpt, prev_excerpt].iter().filter_map(|e| *e) { + 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, kept_position)); + } + } + result + } + pub fn summaries_for_anchors<'a, D, I>(&'a self, anchors: I) -> Vec where D: TextDimension + Ord + Sub, @@ -1312,6 +1433,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 +1450,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 +1501,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 +1520,7 @@ impl MultiBufferSnapshot { let text_anchor = excerpt.clip_anchor(text_anchor); drop(cursor); return Anchor { + buffer_id: excerpt.buffer_id, excerpt_id, text_anchor, }; @@ -1497,7 +1621,6 @@ impl MultiBufferSnapshot { pub fn diagnostic_group<'a, O>( &'a self, - provider_name: &'a str, group_id: usize, ) -> impl Iterator> + 'a where @@ -1505,13 +1628,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, @@ -1596,10 +1719,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(), }; @@ -1795,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 { @@ -2370,6 +2511,131 @@ mod tests { assert_eq!(old_snapshot.anchor_after(10).to_offset(&new_snapshot), 14); } + #[gpui::test] + 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, "ABCDEFGHIJKLMNOP", cx)); + let multibuffer = cx.add_model(|_| MultiBuffer::new(0)); + + // 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, + range: 0..7, + }, + cx, + ) + }); + + 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, _) = 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"); + + // 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. + // 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] + ); + 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. + 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: 5..8, + }, + cx, + ) + }); + + 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_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), + &[0, 2, 9, 13] + ); + + let new_anchors = snapshot_3.refresh_anchors(&anchors); + assert_eq!( + 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] + ); + } + #[gpui::test(iterations = 100)] fn test_random_excerpts(cx: &mut MutableAppContext, mut rng: StdRng) { let operations = env::var("OPERATIONS") @@ -2377,7 +2643,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(); @@ -2408,7 +2674,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) { @@ -2440,8 +2708,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, @@ -2457,12 +2725,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(); @@ -2657,15 +2925,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), @@ -2699,7 +2982,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/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/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/file_finder/src/file_finder.rs b/crates/file_finder/src/file_finder.rs index c973551f8df465e16cbaa2313c72a851c3bacd85..707a3bfb20602820f3be4f86681bb1605b04e0b8 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()) @@ -249,8 +250,8 @@ impl FileFinder { match event { Event::Selected(project_path) => { workspace - .open_entry(project_path.clone(), cx) - .map(|d| d.detach()); + .open_path(project_path.clone(), cx) + .detach_and_log_err(cx); workspace.dismiss_modal(cx); } Event::Dismissed => { @@ -430,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 a14664a9f824ec19a993ccbae9e21544003bb6ab..5b70981ba2239189ca0557ef1460047e87286e2b 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, @@ -2672,9 +2672,11 @@ impl ModelHandle { } } + cx.borrow().foreground().start_waiting(); rx.recv() .await .expect("model dropped with pending condition"); + cx.borrow().foreground().finish_waiting(); } }) .await @@ -2771,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) } @@ -2914,9 +2920,11 @@ impl ViewHandle { } } + cx.borrow().foreground().start_waiting(); rx.recv() .await .expect("view dropped with pending condition"); + cx.borrow().foreground().finish_waiting(); } }) .await @@ -3089,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/gpui/src/elements.rs b/crates/gpui/src/elements.rs index c0b6cdd14391e6e8e6c7a628f04efc962e553e12..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,11 +132,18 @@ pub trait Element { Container::new(self.boxed()) } - fn expanded(self, flex: f32) -> Expanded + fn expanded(self) -> Expanded where Self: 'static + Sized, { - Expanded::new(flex, self.boxed()) + Expanded::new(self.boxed()) + } + + fn flexible(self, flex: f32, expanded: bool) -> Flexible + where + Self: 'static + Sized, + { + Flexible::new(flex, expanded, self.boxed()) } } 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) + }) + } +} 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/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; diff --git a/crates/gpui/src/executor.rs b/crates/gpui/src/executor.rs index 23b870c11f09539c44a3ad0efb629420cee3be46..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 { @@ -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,6 +208,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() @@ -244,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(); } @@ -261,6 +262,26 @@ impl Deterministic { } } +impl DeterministicState { + 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 + ); + } + } +} + #[derive(Default)] struct Trace { executed: Vec, @@ -306,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() @@ -340,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")?; @@ -349,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, "+++++++++++++++++++")?; } @@ -433,6 +475,31 @@ impl Foreground { *any_value.downcast().unwrap() } + pub fn parking_forbidden(&self) -> bool { + match self { + Self::Deterministic(executor) => executor.state.lock().forbid_parking, + _ => panic!("this method can only be called on a deterministic executor"), + } + } + + pub fn start_waiting(&self) { + match self { + 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"), + } + } + pub fn forbid_parking(&self) { match self { Self::Deterministic(executor) => { @@ -615,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/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/buffer.rs b/crates/language/src/buffer.rs index 39e95a99ac72f9a65b5d44918836979c46365396..c0c3d9c5f149894a3f8fdb16138c1362a1ca59da 100644 --- a/crates/language/src/buffer.rs +++ b/crates/language/src/buffer.rs @@ -65,9 +65,9 @@ pub struct Buffer { syntax_tree: Mutex>, parsing_in_background: bool, parse_count: usize, + diagnostics: DiagnosticSet, remote_selections: TreeMap, selections_update_count: usize, - diagnostic_sets: Vec, diagnostics_update_count: usize, language_server: Option, deferred_ops: OperationQueue, @@ -78,7 +78,7 @@ pub struct Buffer { pub struct BufferSnapshot { text: text::BufferSnapshot, tree: Option, - diagnostic_sets: Vec, + diagnostics: DiagnosticSet, diagnostics_update_count: usize, remote_selections: TreeMap, selections_update_count: usize, @@ -129,7 +129,6 @@ struct LanguageServerSnapshot { pub enum Operation { Buffer(text::Operation), UpdateDiagnostics { - provider_name: String, diagnostics: Arc<[DiagnosticEntry]>, lamport_timestamp: clock::Lamport, }, @@ -323,17 +322,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, + ); let deferred_ops = message .deferred_operations @@ -371,13 +364,7 @@ impl Buffer { lamport_timestamp: set.lamport_timestamp.value, }) .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()), deferred_operations: self .deferred_ops .iter() @@ -423,7 +410,7 @@ impl Buffer { language: None, remote_selections: Default::default(), selections_update_count: 0, - diagnostic_sets: Default::default(), + diagnostics: Default::default(), diagnostics_update_count: 0, language_server: None, deferred_ops: OperationQueue::new(), @@ -437,7 +424,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(), @@ -793,7 +780,6 @@ impl Buffer { pub fn update_diagnostics( &mut self, - provider_name: Arc, version: Option, mut diagnostics: Vec>, cx: &mut ModelContext, @@ -883,10 +869,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(), }) @@ -1395,17 +1380,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, ); } @@ -1433,15 +1413,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); @@ -1712,7 +1685,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, @@ -1853,38 +1826,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 { @@ -1906,8 +1869,8 @@ impl Clone for BufferSnapshot { text: self.text.clone(), tree: self.tree.clone(), remote_selections: self.remote_selections.clone(), + diagnostics: self.diagnostics.clone(), selections_update_count: self.selections_update_count, - diagnostic_sets: self.diagnostic_sets.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..9c2091739f15acb8b2ddb6046104a5f18a5baf84 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>, } @@ -21,6 +19,7 @@ pub struct DiagnosticEntry { pub diagnostic: Diagnostic, } +#[derive(Debug)] pub struct DiagnosticGroup { pub entries: Vec>, pub primary_ix: usize, @@ -36,32 +35,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 +148,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..9f7f9f75ac4d6b190210b940b2cec422308d6685 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; @@ -47,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)>, @@ -60,18 +60,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 +127,6 @@ impl Language { highlight_map: Default::default(), }) }), - diagnostic_provider: None, } } @@ -170,11 +160,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 +193,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 @@ -219,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 } @@ -249,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/language/src/proto.rs b/crates/language/src/proto.rs index cc283e6287d8ef936ead94dac99a1231531b4636..771d8b7fd3e9a4303dfdf69ceb9f6df03479dffc 100644 --- a/crates/language/src/proto.rs +++ b/crates/language/src/proto.rs @@ -51,16 +51,12 @@ pub fn serialize_operation(operation: &Operation) -> proto::Operation { selections: serialize_selections(selections), }), 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()), }), }), } @@ -134,33 +130,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 { @@ -239,21 +231,13 @@ pub fn deserialize_operation(message: proto::Operation) -> Result { selections: Arc::from(selections), } } - 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, + }, + }, }, ) } @@ -340,40 +324,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 937d0d16c252e93f3254cfa8af7ab38325c4ad82..cf73e8dd23218a33946b02169e6ec54cc86eb964 100644 --- a/crates/language/src/tests.rs +++ b/crates/language/src/tests.rs @@ -460,7 +460,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 { @@ -508,34 +507,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!( @@ -562,7 +555,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 { @@ -596,33 +588,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!( @@ -659,7 +645,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 { @@ -694,34 +679,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() + }, + } ] ); }); @@ -740,7 +719,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/lsp/src/lsp.rs b/crates/lsp/src/lsp.rs index 769922523c3ca422e97f11e0964aef44c3a247b7..c3d264e8a99f227156378c07e25a4dca726204fa 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, @@ -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/project.rs b/crates/project/src/project.rs index b3dccbb858a6c390c6953fbeef25a77898c6f402..62ecf94b7edff08c012c27bfd6de21ad4fc7eb77 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}; @@ -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::*; @@ -33,6 +33,7 @@ pub struct Project { client_state: ProjectClientState, collaborators: HashMap, subscriptions: Vec, + pending_disk_based_diagnostics: isize, } enum ProjectClientState { @@ -60,6 +61,9 @@ pub struct Collaborator { pub enum Event { ActiveEntryChanged(Option), WorktreeRemoved(WorktreeId), + DiskBasedDiagnosticsStarted, + DiskBasedDiagnosticsUpdated { worktree_id: WorktreeId }, + DiskBasedDiagnosticsFinished, DiagnosticsUpdated(ProjectPath), } @@ -69,7 +73,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, @@ -100,6 +104,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)] @@ -176,6 +190,7 @@ impl Project { client, user_store, fs, + pending_disk_based_diagnostics: 0, } }) } @@ -228,29 +243,51 @@ 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_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, - }, + 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_updating, + ), + 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, + replica_id, + }, + pending_disk_based_diagnostics: 0, + }; + for worktree in worktrees { + this.add_worktree(worktree, cx); + } + this })) } @@ -479,13 +516,30 @@ 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(); self.worktrees.push(worktree); @@ -507,34 +561,19 @@ 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 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) { + 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>( @@ -685,6 +724,60 @@ 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_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, + _: 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 06ac9ecfd7644d3c4b045366e40b2919ea6e4531..46caf8cd93609718a605876348baf2a251551dc0 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::{ @@ -35,7 +34,6 @@ use std::{ ffi::{OsStr, OsString}, fmt, future::Future, - mem, ops::{Deref, Range}, path::{Path, PathBuf}, sync::{ @@ -44,14 +42,12 @@ 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}; 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)] @@ -69,8 +65,10 @@ pub enum Worktree { Remote(RemoteWorktree), } -#[derive(Debug)] +#[derive(Clone, Debug, Eq, PartialEq)] pub enum Event { + DiskBasedDiagnosticsUpdating, + DiskBasedDiagnosticsUpdated, DiagnosticsUpdated(Arc), } @@ -143,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(); @@ -167,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; @@ -224,10 +237,10 @@ impl Worktree { client: client.clone(), loading_buffers: Default::default(), open_buffers: Default::default(), - diagnostic_summaries: Default::default(), queued_operations: Default::default(), languages, user_store, + diagnostic_summaries, }) }) }); @@ -352,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 { @@ -676,9 +689,9 @@ impl Worktree { } } - pub fn update_diagnostics_from_lsp( + pub fn update_diagnostics( &mut self, - mut params: lsp::PublishDiagnosticsParams, + params: lsp::PublishDiagnosticsParams, disk_based_sources: &HashSet, cx: &mut ModelContext, ) -> Result<()> { @@ -693,98 +706,103 @@ 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::>(); - - 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; + 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; } } } - 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() { @@ -792,17 +810,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,10 +825,40 @@ impl Worktree { } let this = self.as_local_mut().unwrap(); + let summary = DiagnosticSummary::new(&diagnostics); this.diagnostic_summaries - .insert(path.clone(), DiagnosticSummary::new(&diagnostics)); - this.provider_diagnostics.insert(path.clone(), diagnostics); - cx.emit(Event::DiagnosticsUpdated(path.clone())); + .insert(PathKey(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, + 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() + } + }) + .detach(); + } + Ok(()) } @@ -910,9 +953,8 @@ pub struct LocalWorktree { loading_buffers: LoadingBuffers, open_buffers: HashMap>, shared_buffers: HashMap>>, - lsp_diagnostics: HashMap, Vec>>, - provider_diagnostics: HashMap, Vec>>, - diagnostic_summaries: BTreeMap, DiagnosticSummary>, + diagnostics: HashMap, Vec>>, + diagnostic_summaries: TreeMap, queued_operations: Vec<(u64, Operation)>, language_registry: Arc, client: Arc, @@ -936,10 +978,10 @@ 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)>, + diagnostic_summaries: TreeMap, } type LoadingBuffers = HashMap< @@ -1018,8 +1060,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, @@ -1093,23 +1134,133 @@ impl LocalWorktree { .log_err() .flatten() { + enum DiagnosticProgress { + Updating, + Updated, + } + let disk_based_sources = language .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(); + 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(); }) .detach(); + 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)) { + 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(DiagnosticProgress::Updated), + ) + .ok(); + } + }) + } else { + break; + } + } + } + }) + .detach(); + + let mut pending_disk_based_diagnostics: i32 = 0; + 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::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(DiagnosticProgress::Updated), + ) + .ok(); + } + } + _ => {} + }, + } + } + }) + .detach(); + let rpc = self.client.clone(); cx.spawn_weak(|this, mut cx| async move { - while let Ok(diagnostics) = diagnostics_rx.recv().await { + while let Ok(progress) = 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_from_lsp(diagnostics, &disk_based_sources, cx) - .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; } @@ -1158,35 +1309,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.get(&path).cloned(); + 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 @@ -1405,10 +1546,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)), } }); @@ -1576,6 +1718,34 @@ impl RemoteWorktree { Ok(()) } + pub fn update_diagnostic_summary( + &mut self, + envelope: TypedEnvelope, + cx: &mut ModelContext, + ) { + if let Some(summary) = envelope.payload.summary { + let path: Arc = Path::new(&summary.path).into(); + self.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_updating(&self, cx: &mut ModelContext) { + cx.emit(Event::DiskBasedDiagnosticsUpdating); + } + + 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) { @@ -1605,17 +1775,24 @@ 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, root_name, entries: self .entries_by_path - .cursor::<()>() + .iter() .filter(|e| !e.is_ignored) .map(Into::into) .collect(), + diagnostic_summaries: diagnostic_summaries + .iter() + .map(|(path, summary)| summary.to_proto(path.0.clone())) + .collect(), } } @@ -3013,32 +3190,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)] @@ -3048,6 +3203,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; @@ -3245,7 +3401,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(), @@ -3697,6 +3853,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 { @@ -3736,6 +3896,18 @@ mod tests { .await .unwrap(); + 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; + fake_server .notify::(lsp::PublishDiagnosticsParams { uri: Url::from_file_path(dir.path().join("a.rs")).unwrap(), @@ -3748,6 +3920,17 @@ mod tests { }], }) .await; + assert_eq!( + 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; + assert_eq!( + events.next().await.unwrap(), + Event::DiskBasedDiagnosticsUpdated + ); let buffer = tree .update(&mut cx, |tree, cx| tree.open_buffer("a.rs", cx)) @@ -3761,19 +3944,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() } - )] + }] ) }); } @@ -3918,7 +4098,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()); @@ -3928,78 +4108,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), @@ -4024,9 +4187,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/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 9ced3fbeb0062c1fc4f7a667cd7bd94c8249188d..1ff1a33172094a25bb51b0f0c0c9d34d378b14e3 100644 --- a/crates/project_panel/src/project_panel.rs +++ b/crates/project_panel/src/project_panel.rs @@ -124,14 +124,14 @@ 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(), }, cx, ) - .map(|t| t.detach()); + .detach_and_log_err(cx); } } } diff --git a/crates/rpc/proto/zed.proto b/crates/rpc/proto/zed.proto index 43f648d972ea8b8ac2add85719234aa5d7637044..f6300c44952a0935d3eb1f3d90ccc15062b8d9e9 100644 --- a/crates/rpc/proto/zed.proto +++ b/crates/rpc/proto/zed.proto @@ -23,32 +23,34 @@ 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; + 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; } } @@ -169,9 +171,25 @@ 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; + uint32 info_count = 6; + uint32 hint_count = 7; +} + +message DiskBasedDiagnosticsUpdating { + uint64 project_id = 1; + uint64 worktree_id = 2; +} + +message DiskBasedDiagnosticsUpdated { + uint64 project_id = 1; + uint64 worktree_id = 2; } message GetChannels {} @@ -248,6 +266,7 @@ message Worktree { uint64 id = 1; string root_name = 2; repeated Entry entries = 3; + repeated DiagnosticSummary diagnostic_summaries = 4; } message Entry { @@ -268,7 +287,7 @@ message Buffer { repeated UndoMapEntry undo_map = 5; repeated VectorClockEntry version = 6; repeated SelectionSet selections = 7; - repeated DiagnosticSet diagnostic_sets = 8; + repeated Diagnostic diagnostics = 8; uint32 lamport_timestamp = 9; repeated Operation deferred_operations = 10; } @@ -309,15 +328,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 { @@ -345,7 +359,7 @@ message Operation { Edit edit = 1; Undo undo = 2; UpdateSelections update_selections = 3; - UpdateDiagnosticSet update_diagnostic_set = 4; + UpdateDiagnostics update_diagnostics = 4; } message Edit { diff --git a/crates/rpc/src/proto.rs b/crates/rpc/src/proto.rs index 9274049a7b601e1c25bbcdb816c8ed3d8b07ccbe..91abc2523d3c026567b0a3c4f83fa00115ab3cdd 100644 --- a/crates/rpc/src/proto.rs +++ b/crates/rpc/src/proto.rs @@ -125,6 +125,8 @@ messages!( BufferSaved, ChannelMessageSent, CloseBuffer, + DiskBasedDiagnosticsUpdated, + DiskBasedDiagnosticsUpdating, Error, GetChannelMessages, GetChannelMessagesResponse, @@ -155,6 +157,7 @@ messages!( UnshareProject, UpdateBuffer, UpdateContacts, + UpdateDiagnosticSummary, UpdateWorktree, ); @@ -178,17 +181,20 @@ request_messages!( entity_messages!( project_id, AddProjectCollaborator, - RemoveProjectCollaborator, + BufferSaved, + CloseBuffer, + DiskBasedDiagnosticsUpdated, + DiskBasedDiagnosticsUpdating, 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 e507d3d7b82828ae86a7e46b7569e614400d4b17..76698c3a19171cb34a76b196db5dbb95ed805f7b 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; @@ -71,6 +71,9 @@ 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_updating) + .add_handler(Server::disk_based_diagnostics_updated) .add_handler(Server::open_buffer) .add_handler(Server::close_buffer) .add_handler(Server::update_buffer) @@ -300,6 +303,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(); @@ -471,11 +479,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?; @@ -517,6 +531,64 @@ impl Server { Ok(()) } + async fn update_diagnostic_summary( + mut self: Arc, + request: TypedEnvelope, + ) -> tide::Result<()> { + 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()) + }) + .await?; + 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, + ) -> 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, @@ -999,7 +1071,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; @@ -1008,6 +1080,7 @@ mod tests { use std::{ ops::Deref, path::Path, + rc::Rc, sync::{ atomic::{AtomicBool, Ordering::SeqCst}, Arc, @@ -1026,7 +1099,7 @@ mod tests { LanguageRegistry, LanguageServerConfig, Point, }, lsp, - project::Project, + project::{DiagnosticSummary, Project, ProjectPath}, }; #[gpui::test] @@ -1037,7 +1110,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; @@ -1170,7 +1243,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; @@ -1246,7 +1319,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; @@ -1396,7 +1469,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; @@ -1492,7 +1565,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; @@ -1572,7 +1645,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; @@ -1647,7 +1720,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; @@ -1734,7 +1807,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; @@ -1767,6 +1840,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 @@ -1782,6 +1856,68 @@ mod tests { .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, + client_b.clone(), + client_b.user_store.clone(), + lang_registry.clone(), + fs.clone(), + &mut cx_b.to_async(), + ) + .await + .unwrap(); + + 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(), @@ -1806,20 +1942,26 @@ 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()); + // Client b gets the updated summaries + project_b + .condition(&cx_b, |project, cx| { + project.diagnostic_summaries(cx).collect::>() + == &[( + ProjectPath { + worktree_id, + path: Arc::from(Path::new("a.rs")), + }, + DiagnosticSummary { + error_count: 1, + warning_count: 1, + ..Default::default() + }, + )] + }) + .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() .spawn(worktree_b.update(&mut cx_b, |worktree, cx| worktree.open_buffer("a.rs", cx))) @@ -1831,7 +1973,7 @@ mod tests { buffer .snapshot() .diagnostics_in_range::<_, Point>(0..buffer.len()) - .map(|(_, entry)| entry) + .map(|entry| entry) .collect::>(), &[ DiagnosticEntry { @@ -1864,7 +2006,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; @@ -2003,7 +2145,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; @@ -2064,7 +2206,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(); @@ -2282,7 +2424,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; @@ -2415,6 +2557,7 @@ mod tests { peer: Arc, app_state: Arc, server: Arc, + foreground: Rc, notifications: mpsc::Receiver<()>, connection_killers: Arc>>>>, forbid_connections: Arc, @@ -2422,7 +2565,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(); @@ -2432,6 +2575,7 @@ mod tests { peer, app_state, server, + foreground, notifications: notifications.1, connection_killers: Default::default(), forbid_connections: Default::default(), @@ -2547,7 +2691,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 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, &()); diff --git a/crates/text/src/text.rs b/crates/text/src/text.rs index 108863dd4822c8c14c884df8dc205f4f4dd69ccb..895e6ad4714a96b85a37d8b8b5207e53dec04231 100644 --- a/crates/text/src/text.rs +++ b/crates/text/src/text.rs @@ -1536,7 +1536,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(); @@ -1578,7 +1578,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); diff --git a/crates/theme/src/theme.rs b/crates/theme/src/theme.rs index 6fca6966896c2290f2bc2ed2a7c69b15ec50b872..1ba2fc8faf85e1f83fbe088d8f66ac693ec8c635 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,14 @@ pub struct ContainedLabel { pub label: LabelStyle, } +#[derive(Clone, Deserialize, Default)] +pub struct ProjectDiagnostics { + #[serde(flatten)] + pub container: ContainerStyle, + pub empty_message: TextStyle, + pub status_bar_item: ContainedText, +} + #[derive(Clone, Deserialize, Default)] pub struct EditorStyle { pub text: TextStyle, @@ -253,6 +262,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/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/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 5a3411c9268c254f811075cf11397f9e73079a8b..731db29d634049ea6cebeec5b2369c4a98fa63fa 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, Workspace}; 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<(usize, Box)>, 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,70 @@ 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); + pub fn open_item( + &mut self, + item_handle: T, + workspace: &Workspace, + cx: &mut ViewContext, + ) -> Box + where + T: 'static + ItemHandle, + { + 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; + } + } + + let item_view = item_handle.add_view(cx.window_id(), workspace, cx); + self.add_item_view(item_view.boxed_clone(), cx); + item_view + } + + 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.item_handle(cx).id(), item_view)); + self.activate_item(item_idx, cx); cx.notify(); - item_idx } - pub fn items(&self) -> &[Box] { - &self.items + 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.items.get(self.active_item).cloned() + self.item_views + .get(self.active_item) + .map(|(_, view)| view.clone()) } - 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 index_for_item_view(&self, item_view: &dyn ItemViewHandle) -> Option { + self.item_views + .iter() + .position(|(_, i)| i.id() == item_view.id()) } - pub fn item_index(&self, item: &dyn ItemViewHandle) -> Option { - self.items.iter().position(|i| i.id() == item.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) { - if index < self.items.len() { + if index < self.item_views.len() { self.active_item = index; self.focus_active_item(cx); cx.notify(); @@ -134,15 +160,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 +178,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].1.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 +209,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 +238,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 +297,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::( @@ -314,13 +340,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 +369,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") @@ -356,17 +380,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/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 3c7c86860872dc9080bc21b4fe7b5a09e48aadbf..ea6eb6603140108238ff916e605fb787451e12f3 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, @@ -15,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, + AnyModelHandle, AnyViewHandle, AppContext, ClipboardItem, Entity, ModelContext, ModelHandle, + MutableAppContext, PathPromptOptions, PromptLevel, RenderContext, Task, View, ViewContext, + ViewHandle, WeakModelHandle, WeakViewHandle, }; use language::LanguageRegistry; use log::error; @@ -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, @@ -115,7 +117,7 @@ pub struct JoinProjectParams { pub app_state: Arc, } -pub trait EntryOpener { +pub trait PathOpener { fn open( &self, worktree: &mut Worktree, @@ -129,7 +131,7 @@ pub trait Item: Entity + Sized { fn build_view( handle: ModelHandle, - settings: watch::Receiver, + workspace: &Workspace, cx: &mut ViewContext, ) -> Self::View; @@ -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,27 +177,31 @@ pub trait ItemView: View { } pub trait ItemHandle: Send + Sync { + fn id(&self) -> usize; fn add_view( &self, window_id: usize, - settings: watch::Receiver, + workspace: &Workspace, cx: &mut MutableAppContext, ) -> Box; fn boxed_clone(&self) -> Box; fn downgrade(&self) -> Box; + fn to_any(&self) -> AnyModelHandle; fn project_path(&self, cx: &AppContext) -> Option; } 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,13 +218,17 @@ pub trait ItemViewHandle { } impl ItemHandle for ModelHandle { + fn id(&self) -> usize { + self.id() + } + 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 { @@ -226,19 +239,27 @@ 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() } } impl ItemHandle for Box { + fn id(&self) -> usize { + ItemHandle::id(self.as_ref()) + } + 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 { @@ -249,18 +270,44 @@ 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) } } 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 +327,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.index_for_item_view(&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 +405,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 +437,7 @@ impl WorkspaceParams { languages, settings: watch::channel_with(settings).1, user_store, - entry_openers: Arc::from([]), + path_openers: Arc::from([]), } } @@ -412,13 +457,14 @@ 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(), } } } pub struct Workspace { pub settings: watch::Receiver, + weak_self: WeakViewHandle, client: Arc, user_store: ModelHandle, fs: Arc, @@ -430,8 +476,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<()>, } @@ -473,6 +519,7 @@ impl Workspace { Workspace { modal: None, + weak_self: cx.weak_handle(), center: PaneGroup::new(pane.id()), panes: vec![pane.clone()], active_pane: pane.clone(), @@ -484,12 +531,20 @@ 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, } } + 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 } @@ -560,13 +615,13 @@ impl Workspace { async move { 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)) - { - return Some(entry.await); - } + Some( + this.update(&mut cx, |this, cx| this.open_path(project_path, cx)) + .await, + ) + } else { + None } - None } }) }) @@ -665,104 +720,59 @@ impl Workspace { } #[must_use] - pub fn open_entry( + 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); - } + 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)) + } - // 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 - } + 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> { @@ -791,24 +801,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 +842,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() } @@ -920,19 +918,65 @@ impl Workspace { pane } - pub fn add_item( + pub fn open_item( &mut self, item_handle: T, 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.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: 'static + ItemHandle, + { + self.items.insert(item_handle.downgrade()); + 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 + } + } + + 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) { @@ -977,7 +1021,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 @@ -1203,50 +1247,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) { diff --git a/crates/zed/assets/themes/_base.toml b/crates/zed/assets/themes/_base.toml index 958620521d8e9048feea444275fb79ec101f0cf5..a8a5694978aca9d9207f1bd2a84f9b11fb4cf695 100644 --- a/crates/zed/assets/themes/_base.toml +++ b/crates/zed/assets/themes/_base.toml @@ -249,11 +249,28 @@ 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" +status_bar_item = { extends = "$text.2", margin.right = 10 } 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" 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 { 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 1483982b601ed2bbc1f45becf5df756c9e177204..61300d1f56ad74785e9ccd16a4dfe8bd0cf391a4 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(); @@ -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); }); @@ -256,8 +264,7 @@ mod tests { // Open the first entry let entry_1 = workspace - .update(&mut cx, |w, cx| w.open_entry(file1.clone(), cx)) - .unwrap() + .update(&mut cx, |w, cx| w.open_path(file1.clone(), cx)) .await .unwrap(); cx.read(|cx| { @@ -266,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().count(), 1); }); // Open the second entry workspace - .update(&mut cx, |w, cx| w.open_entry(file2.clone(), cx)) - .unwrap() + .update(&mut cx, |w, cx| w.open_path(file2.clone(), cx)) .await .unwrap(); cx.read(|cx| { @@ -281,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().count(), 2); }); // 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)) .await .unwrap(); assert_eq!(entry_1.id(), entry_1b.id()); @@ -297,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().count(), 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_entry(file2.clone(), cx).unwrap() + w.open_path(file2.clone(), cx) }) .await .unwrap(); @@ -323,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_entry(file3.clone(), cx).unwrap(), - w.open_entry(file3.clone(), cx).unwrap(), + w.open_path(file3.clone(), cx), + w.open_path(file3.clone(), cx), ) }); t1.await.unwrap(); @@ -336,8 +342,7 @@ mod tests { Some(file3.clone()) ); let pane_entries = pane - .items() - .iter() + .item_views() .map(|i| i.project_path(cx).unwrap()) .collect::>(); assert_eq!(pane_entries, &[file1, file2, file3]); @@ -553,15 +558,13 @@ mod tests { workspace .update(&mut cx, |workspace, cx| { workspace.split_pane(workspace.active_pane().clone(), SplitDirection::Right, cx); - workspace - .open_entry( - 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(); @@ -574,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() + ); }) } @@ -658,8 +664,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)) - .unwrap() + .update(&mut cx, |w, cx| w.open_path(file1.clone(), cx)) .await .unwrap(); cx.read(|cx| {