From 088668ecf8aadb9cb34d7e4f1c2c6fc73de4340a Mon Sep 17 00:00:00 2001 From: Nate Butler Date: Fri, 22 Jul 2022 11:57:53 -0400 Subject: [PATCH 01/42] Fix reversed disclosure arrows in project panel --- crates/project_panel/src/project_panel.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/project_panel/src/project_panel.rs b/crates/project_panel/src/project_panel.rs index 792d970fcd..0a32a8427b 100644 --- a/crates/project_panel/src/project_panel.rs +++ b/crates/project_panel/src/project_panel.rs @@ -1028,11 +1028,11 @@ impl ProjectPanel { .with_child( ConstrainedBox::new(if kind == EntryKind::Dir { if details.is_expanded { - Svg::new("icons/chevron_right_8.svg") + Svg::new("icons/chevron_down_8.svg") .with_color(style.icon_color) .boxed() } else { - Svg::new("icons/chevron_down_8.svg") + Svg::new("icons/chevron_right_8.svg") .with_color(style.icon_color) .boxed() } From f963c0ed457c2f6fd3e495c9fbeac36461ad186c Mon Sep 17 00:00:00 2001 From: Nate Butler Date: Fri, 22 Jul 2022 17:28:58 -0400 Subject: [PATCH 02/42] WIP for keith --- crates/theme/src/theme.rs | 21 ++++++-- styles/src/styleTree/tabBar.ts | 85 ++++++++++++++++++++++++++++++ styles/src/styleTree/workspace.ts | 64 +--------------------- styles/src/themes/common/base16.ts | 4 +- 4 files changed, 105 insertions(+), 69 deletions(-) create mode 100644 styles/src/styleTree/tabBar.ts diff --git a/crates/theme/src/theme.rs b/crates/theme/src/theme.rs index 540c5bfd6c..23dfa4b58b 100644 --- a/crates/theme/src/theme.rs +++ b/crates/theme/src/theme.rs @@ -38,11 +38,7 @@ pub struct Theme { pub struct Workspace { pub background: Color, pub titlebar: Titlebar, - pub active_pane_active_tab: Tab, - pub active_pane_inactive_tab: Tab, - pub inactive_pane_active_tab: Tab, - pub inactive_pane_inactive_tab: Tab, - pub pane_button: Interactive, + pub tab_bar: TabBar, pub pane_divider: Border, pub leader_border_opacity: f32, pub leader_border_width: f32, @@ -72,6 +68,21 @@ pub struct Titlebar { pub outdated_warning: ContainedText, } +#[derive(Clone, Deserialize, Default)] +pub struct TabBar { + #[serde(flatten)] + pub container: ContainerStyle, + pub pane_button: Interactive, + pub active_pane: TabStyles, + pub inactive_pane: TabStyles, +} + +#[derive(Clone, Deserialize, Default)] +pub struct TabStyles { + active_tab: Tab, + inactive_tab: Tab, +} + #[derive(Clone, Deserialize, Default)] pub struct AvatarRibbon { #[serde(flatten)] diff --git a/styles/src/styleTree/tabBar.ts b/styles/src/styleTree/tabBar.ts new file mode 100644 index 0000000000..d35af34ac9 --- /dev/null +++ b/styles/src/styleTree/tabBar.ts @@ -0,0 +1,85 @@ +import Theme from "../themes/common/theme"; +import { iconColor, text, border, backgroundColor } from "./components"; + +export default function tabBar(theme: Theme) { + const height = 32; + + const tab = { + height, + background: backgroundColor(theme, 300), + border: border(theme, "primary", { + left: true, + bottom: true, + overlay: true, + }), + iconClose: iconColor(theme, "muted"), + iconCloseActive: iconColor(theme, "active"), + iconConflict: iconColor(theme, "warning"), + iconDirty: iconColor(theme, "info"), + iconWidth: 8, + spacing: 8, + text: text(theme, "sans", "secondary", { size: "sm" }), + padding: { + left: 8, + right: 8, + }, + description: { + margin: { left: 6, top: 1 }, + ...text(theme, "sans", "muted", { size: "2xs" }) + } + }; + + const activePaneActiveTab = { + ...tab, + background: backgroundColor(theme, 500), + text: text(theme, "sans", "active", { size: "sm" }), + border: { + ...tab.border, + }, + }; + + const inactivePaneInactiveTab = { + ...tab, + background: backgroundColor(theme, 300), + text: text(theme, "sans", "muted", { size: "sm" }), + }; + + const inactivePaneActiveTab = { + ...tab, + background: backgroundColor(theme, 500), + text: text(theme, "sans", "secondary", { size: "sm" }), + border: { + ...tab.border, + }, + } + + return { + height, + background: backgroundColor(theme, 300), + border: border(theme, "primary", { + left: true, + bottom: true, + overlay: true, + }), + activePane: { + activeTab: activePaneActiveTab, + inactiveTab: tab, + }, + inactivePane: { + activeTab: inactivePaneActiveTab, + inactiveTab: inactivePaneInactiveTab, + }, + paneButton: { + color: iconColor(theme, "secondary"), + border: { + ...activePaneActiveTab.border, + }, + iconWidth: 12, + buttonWidth: activePaneActiveTab.height, + hover: { + color: iconColor(theme, "active"), + background: backgroundColor(theme, 300), + }, + }, + } +} \ No newline at end of file diff --git a/styles/src/styleTree/workspace.ts b/styles/src/styleTree/workspace.ts index c86dce7226..4937edfdee 100644 --- a/styles/src/styleTree/workspace.ts +++ b/styles/src/styleTree/workspace.ts @@ -8,58 +8,13 @@ import { text, } from "./components"; import statusBar from "./statusBar"; +import tabBar from "./tabBar"; export function workspaceBackground(theme: Theme) { return backgroundColor(theme, 300); } export default function workspace(theme: Theme) { - const activePaneInactiveTab = { - height: 32, - background: workspaceBackground(theme), - iconClose: iconColor(theme, "muted"), - iconCloseActive: iconColor(theme, "active"), - iconConflict: iconColor(theme, "warning"), - iconDirty: iconColor(theme, "info"), - iconWidth: 8, - spacing: 8, - text: text(theme, "sans", "secondary", { size: "sm" }), - border: border(theme, "primary", { - left: true, - bottom: true, - overlay: true, - }), - padding: { - left: 8, - right: 8, - }, - description: { - margin: { left: 6, top: 1 }, - ...text(theme, "sans", "muted", { size: "2xs" }) - } - }; - - const activePaneActiveTab = { - ...activePaneInactiveTab, - background: backgroundColor(theme, 500), - text: text(theme, "sans", "active", { size: "sm" }), - border: { - ...activePaneInactiveTab.border, - bottom: false, - }, - }; - - const inactivePaneInactiveTab = { - ...activePaneInactiveTab, - background: backgroundColor(theme, 100), - text: text(theme, "sans", "placeholder", { size: "sm" }), - }; - - const inactivePaneActiveTab = { - ...activePaneInactiveTab, - text: text(theme, "sans", "placeholder", { size: "sm" }), - } - const titlebarPadding = 6; return { @@ -74,22 +29,7 @@ export default function workspace(theme: Theme) { }, leaderBorderOpacity: 0.7, leaderBorderWidth: 2.0, - activePaneActiveTab, - activePaneInactiveTab, - inactivePaneActiveTab, - inactivePaneInactiveTab, - paneButton: { - color: iconColor(theme, "secondary"), - border: { - ...activePaneActiveTab.border, - }, - iconWidth: 12, - buttonWidth: activePaneActiveTab.height, - hover: { - color: iconColor(theme, "active"), - background: backgroundColor(theme, 300), - }, - }, + tabBar: tabBar(theme), modal: { margin: { bottom: 52, diff --git a/styles/src/themes/common/base16.ts b/styles/src/themes/common/base16.ts index a73ac7f0cf..55219b1687 100644 --- a/styles/src/themes/common/base16.ts +++ b/styles/src/themes/common/base16.ts @@ -115,8 +115,8 @@ export function createTheme( const textColor = { primary: sample(ramps.neutral, 6), secondary: sample(ramps.neutral, 5), - muted: sample(ramps.neutral, 5), - placeholder: sample(ramps.neutral, 4), + muted: sample(ramps.neutral, 4), + placeholder: sample(ramps.neutral, 3), active: sample(ramps.neutral, 7), feature: sample(ramps.blue, 0.5), ok: sample(ramps.green, 0.5), From 45eb0e788986a988392f6044ca80b496225babdf Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Tue, 26 Jul 2022 08:43:27 +0200 Subject: [PATCH 03/42] Clip invalid edits from LSP instead of reporting an error This fixes an issue with the Go language server, which reports invalid formatting ranges when there's a missing newline at the end of the file. Specifically, if the buffer is `N` lines long, it will try to insert the newline at `Point(N + 1, 0)`. I confirmed the behavior is the same in VS Code, and they indeed clip the LSP ranges as well. --- crates/project/src/project.rs | 10 ++++------ crates/project/src/project_tests.rs | 4 ++-- 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 759eb90194..e116761eb9 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -5764,6 +5764,10 @@ impl Project { let mut lsp_edits = lsp_edits.into_iter().peekable(); let mut edits = Vec::new(); while let Some((mut range, mut new_text)) = lsp_edits.next() { + // Clip invalid ranges provided by the language server. + range.start = snapshot.clip_point_utf16(range.start, Bias::Left); + range.end = snapshot.clip_point_utf16(range.end, Bias::Left); + // Combine any LSP edits that are adjacent. // // Also, combine LSP edits that are separated from each other by only @@ -5791,12 +5795,6 @@ impl Project { lsp_edits.next(); } - if snapshot.clip_point_utf16(range.start, Bias::Left) != range.start - || snapshot.clip_point_utf16(range.end, Bias::Left) != range.end - { - return Err(anyhow!("invalid edits received from language server")); - } - // For multiline edits, perform a diff of the old and new text so that // we can identify the changes more precisely, preserving the locations // of any anchors positioned in the unchanged regions. diff --git a/crates/project/src/project_tests.rs b/crates/project/src/project_tests.rs index 1080daff1d..e36bd2c75a 100644 --- a/crates/project/src/project_tests.rs +++ b/crates/project/src/project_tests.rs @@ -1565,7 +1565,7 @@ async fn test_invalid_edits_from_lsp(cx: &mut gpui::TestAppContext) { .unwrap(); // Simulate the language server sending us edits in a non-ordered fashion, - // with ranges sometimes being inverted. + // with ranges sometimes being inverted or pointing to invalid locations. let edits = project .update(cx, |project, cx| { project.edits_from_lsp( @@ -1580,7 +1580,7 @@ async fn test_invalid_edits_from_lsp(cx: &mut gpui::TestAppContext) { new_text: "a::{b, c}".into(), }, lsp::TextEdit { - range: lsp::Range::new(lsp::Position::new(1, 0), lsp::Position::new(7, 0)), + range: lsp::Range::new(lsp::Position::new(1, 0), lsp::Position::new(99, 0)), new_text: "".into(), }, lsp::TextEdit { From f775cb29475efd07ca6eb1e85ee540175e26192e Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Tue, 26 Jul 2022 10:41:39 +0200 Subject: [PATCH 04/42] Honor `sort_text` when language server provides completions --- crates/editor/src/editor.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index d92bd04251..f570fb2fb6 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -755,9 +755,11 @@ impl CompletionsMenu { .collect() }; matches.sort_unstable_by_key(|mat| { + let completion = &self.completions[mat.candidate_id]; ( + completion.lsp_completion.sort_text.as_ref(), Reverse(OrderedFloat(mat.score)), - self.completions[mat.candidate_id].sort_key(), + completion.sort_key(), ) }); From 8552ba15dc211c145fd48322de82e96f71678647 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Tue, 26 Jul 2022 14:48:18 +0200 Subject: [PATCH 05/42] Show symbols located in visible paths before ones located externally --- crates/collab/src/integration_tests.rs | 2 +- crates/fuzzy/src/fuzzy.rs | 2 +- crates/project/src/project.rs | 55 ++++----- crates/project_symbols/src/project_symbols.rs | 106 +++++++++++------- crates/workspace/src/workspace.rs | 12 +- 5 files changed, 103 insertions(+), 74 deletions(-) diff --git a/crates/collab/src/integration_tests.rs b/crates/collab/src/integration_tests.rs index a3dcd5fbce..9471967eee 100644 --- a/crates/collab/src/integration_tests.rs +++ b/crates/collab/src/integration_tests.rs @@ -2589,7 +2589,7 @@ async fn test_project_symbols(cx_a: &mut TestAppContext, cx_b: &mut TestAppConte // Attempt to craft a symbol and violate host's privacy by opening an arbitrary file. let mut fake_symbol = symbols[0].clone(); - fake_symbol.path = Path::new("/code/secrets").into(); + fake_symbol.path.path = Path::new("/code/secrets").into(); let error = project_b .update(cx_b, |project, cx| { project.open_buffer_for_symbol(&fake_symbol, cx) diff --git a/crates/fuzzy/src/fuzzy.rs b/crates/fuzzy/src/fuzzy.rs index f6abb22ddc..401ab33d7f 100644 --- a/crates/fuzzy/src/fuzzy.rs +++ b/crates/fuzzy/src/fuzzy.rs @@ -181,7 +181,7 @@ pub async fn match_strings( cancel_flag: &AtomicBool, background: Arc, ) -> Vec { - if candidates.is_empty() { + if candidates.is_empty() || max_results == 0 { return Default::default(); } diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index e116761eb9..29106b5889 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -254,10 +254,9 @@ pub struct DocumentHighlight { #[derive(Clone, Debug)] pub struct Symbol { - pub source_worktree_id: WorktreeId, - pub worktree_id: WorktreeId, pub language_server_name: LanguageServerName, - pub path: PathBuf, + pub source_worktree_id: WorktreeId, + pub path: ProjectPath, pub label: CodeLabel, pub name: String, pub kind: lsp::SymbolKind, @@ -3324,16 +3323,19 @@ impl Project { if let Some((worktree, rel_path)) = this.find_local_worktree(&abs_path, cx) { - worktree_id = worktree.read(cx).id(); + worktree_id = (&worktree.read(cx)).id(); path = rel_path; } else { path = relativize_path(&worktree_abs_path, &abs_path); } - let signature = this.symbol_signature(worktree_id, &path); - let language = this.languages.select_language(&path); + let project_path = ProjectPath { + worktree_id, + path: path.into(), + }; + let signature = this.symbol_signature(&project_path); + let language = this.languages.select_language(&project_path.path); let language_server_name = adapter.name.clone(); - Some(async move { let label = if let Some(language) = language { language @@ -3344,15 +3346,14 @@ impl Project { }; Symbol { - source_worktree_id, - worktree_id, language_server_name, + source_worktree_id, + path: project_path, label: label.unwrap_or_else(|| { CodeLabel::plain(lsp_symbol.name.clone(), None) }), kind: lsp_symbol.kind, name: lsp_symbol.name, - path, range: range_from_lsp(lsp_symbol.location.range), signature, } @@ -3410,7 +3411,7 @@ impl Project { }; let worktree_abs_path = if let Some(worktree_abs_path) = self - .worktree_for_id(symbol.worktree_id, cx) + .worktree_for_id(symbol.path.worktree_id, cx) .and_then(|worktree| worktree.read(cx).as_local()) .map(|local_worktree| local_worktree.abs_path()) { @@ -3418,7 +3419,7 @@ impl Project { } else { return Task::ready(Err(anyhow!("worktree not found for symbol"))); }; - let symbol_abs_path = worktree_abs_path.join(&symbol.path); + let symbol_abs_path = worktree_abs_path.join(&symbol.path.path); let symbol_uri = if let Ok(uri) = lsp::Url::from_file_path(symbol_abs_path) { uri } else { @@ -4622,11 +4623,11 @@ impl Project { self.active_entry } - pub fn entry_for_path(&self, path: &ProjectPath, cx: &AppContext) -> Option { + pub fn entry_for_path(&self, path: &ProjectPath, cx: &AppContext) -> Option { self.worktree_for_id(path.worktree_id, cx)? .read(cx) .entry_for_path(&path.path) - .map(|entry| entry.id) + .cloned() } pub fn path_for_entry(&self, entry_id: ProjectEntryId, cx: &AppContext) -> Option { @@ -5436,7 +5437,7 @@ impl Project { .read_with(&cx, |this, _| this.deserialize_symbol(symbol)) .await?; let symbol = this.read_with(&cx, |this, _| { - let signature = this.symbol_signature(symbol.worktree_id, &symbol.path); + let signature = this.symbol_signature(&symbol.path); if signature == symbol.signature { Ok(symbol) } else { @@ -5454,10 +5455,10 @@ impl Project { }) } - fn symbol_signature(&self, worktree_id: WorktreeId, path: &Path) -> [u8; 32] { + fn symbol_signature(&self, project_path: &ProjectPath) -> [u8; 32] { let mut hasher = Sha256::new(); - hasher.update(worktree_id.to_proto().to_be_bytes()); - hasher.update(path.to_string_lossy().as_bytes()); + hasher.update(project_path.worktree_id.to_proto().to_be_bytes()); + hasher.update(project_path.path.to_string_lossy().as_bytes()); hasher.update(self.nonce.to_be_bytes()); hasher.finalize().as_slice().try_into().unwrap() } @@ -5655,14 +5656,17 @@ impl Project { .end .ok_or_else(|| anyhow!("invalid end"))?; let kind = unsafe { mem::transmute(serialized_symbol.kind) }; - let path = PathBuf::from(serialized_symbol.path); - let language = languages.select_language(&path); - Ok(Symbol { - source_worktree_id, + let path = ProjectPath { worktree_id, + path: PathBuf::from(serialized_symbol.path).into(), + }; + let language = languages.select_language(&path.path); + Ok(Symbol { language_server_name: LanguageServerName( serialized_symbol.language_server_name.into(), ), + source_worktree_id, + path, label: { match language { Some(language) => { @@ -5676,7 +5680,6 @@ impl Project { }, name: serialized_symbol.name, - path, range: PointUtf16::new(start.row, start.column) ..PointUtf16::new(end.row, end.column), kind, @@ -6142,12 +6145,12 @@ impl From for fs::RemoveOptions { fn serialize_symbol(symbol: &Symbol) -> proto::Symbol { proto::Symbol { - source_worktree_id: symbol.source_worktree_id.to_proto(), - worktree_id: symbol.worktree_id.to_proto(), language_server_name: symbol.language_server_name.0.to_string(), + source_worktree_id: symbol.source_worktree_id.to_proto(), + worktree_id: symbol.path.worktree_id.to_proto(), + path: symbol.path.path.to_string_lossy().to_string(), name: symbol.name.clone(), kind: unsafe { mem::transmute(symbol.kind) }, - path: symbol.path.to_string_lossy().to_string(), start: Some(proto::Point { row: symbol.range.start.row, column: symbol.range.start.column, diff --git a/crates/project_symbols/src/project_symbols.rs b/crates/project_symbols/src/project_symbols.rs index 10425f63a8..8f2305eaff 100644 --- a/crates/project_symbols/src/project_symbols.rs +++ b/crates/project_symbols/src/project_symbols.rs @@ -26,7 +26,8 @@ pub struct ProjectSymbolsView { project: ModelHandle, selected_match_index: usize, symbols: Vec, - match_candidates: Vec, + visible_match_candidates: Vec, + external_match_candidates: Vec, show_worktree_root_name: bool, pending_update: Task<()>, matches: Vec, @@ -63,7 +64,8 @@ impl ProjectSymbolsView { picker: cx.add_view(|cx| Picker::new(handle, cx)), selected_match_index: 0, symbols: Default::default(), - match_candidates: Default::default(), + visible_match_candidates: Default::default(), + external_match_candidates: Default::default(), matches: Default::default(), show_worktree_root_name: false, pending_update: Task::ready(()), @@ -80,38 +82,39 @@ impl ProjectSymbolsView { } fn filter(&mut self, query: &str, cx: &mut ViewContext) { - let mut matches = if query.is_empty() { - self.match_candidates - .iter() - .enumerate() - .map(|(candidate_id, candidate)| StringMatch { - candidate_id, - score: Default::default(), - positions: Default::default(), - string: candidate.string.clone(), - }) - .collect() - } else { - cx.background_executor().block(fuzzy::match_strings( - &self.match_candidates, - query, - false, - 100, - &Default::default(), - cx.background().clone(), - )) - }; - - matches.sort_unstable_by_key(|mat| { - let label = &self.symbols[mat.candidate_id].label; + const MAX_MATCHES: usize = 100; + let mut visible_matches = cx.background_executor().block(fuzzy::match_strings( + &self.visible_match_candidates, + query, + false, + MAX_MATCHES, + &Default::default(), + cx.background().clone(), + )); + let mut external_matches = cx.background_executor().block(fuzzy::match_strings( + &self.external_match_candidates, + query, + false, + MAX_MATCHES - visible_matches.len(), + &Default::default(), + cx.background().clone(), + )); + let sort_key_for_match = |mat: &StringMatch| { + let symbol = &self.symbols[mat.candidate_id]; ( Reverse(OrderedFloat(mat.score)), - &label.text[label.filter_range.clone()], + &symbol.label.text[symbol.label.filter_range.clone()], ) - }); + }; + + visible_matches.sort_unstable_by_key(sort_key_for_match); + external_matches.sort_unstable_by_key(sort_key_for_match); + let mut matches = visible_matches; + matches.append(&mut external_matches); for mat in &mut matches { - let filter_start = self.symbols[mat.candidate_id].label.filter_range.start; + let symbol = &self.symbols[mat.candidate_id]; + let filter_start = symbol.label.filter_range.start; for position in &mut mat.positions { *position += filter_start; } @@ -198,7 +201,8 @@ impl PickerDelegate for ProjectSymbolsView { if let Some(this) = this.upgrade(&cx) { if let Some(symbols) = symbols { this.update(&mut cx, |this, cx| { - this.match_candidates = symbols + let project = this.project.read(cx); + let (visible_match_candidates, external_match_candidates) = symbols .iter() .enumerate() .map(|(id, symbol)| { @@ -208,7 +212,14 @@ impl PickerDelegate for ProjectSymbolsView { .to_string(), ) }) - .collect(); + .partition(|candidate| { + project + .entry_for_path(&symbols[candidate.id].path, cx) + .map_or(false, |e| !e.is_ignored) + }); + + this.visible_match_candidates = visible_match_candidates; + this.external_match_candidates = external_match_candidates; this.symbols = symbols; this.filter(&query, cx); }); @@ -232,10 +243,10 @@ impl PickerDelegate for ProjectSymbolsView { let symbol = &self.symbols[string_match.candidate_id]; let syntax_runs = styled_runs_for_code_label(&symbol.label, &settings.theme.editor.syntax); - let mut path = symbol.path.to_string_lossy(); + let mut path = symbol.path.path.to_string_lossy(); if self.show_worktree_root_name { let project = self.project.read(cx); - if let Some(worktree) = project.worktree_for_id(symbol.worktree_id, cx) { + if let Some(worktree) = project.worktree_for_id(symbol.path.worktree_id, cx) { path = Cow::Owned(format!( "{}{}{}", worktree.read(cx).root_name(), @@ -275,7 +286,7 @@ mod tests { use gpui::{serde_json::json, TestAppContext}; use language::{FakeLspAdapter, Language, LanguageConfig}; use project::FakeFs; - use std::sync::Arc; + use std::{path::Path, sync::Arc}; #[gpui::test] async fn test_project_symbols(cx: &mut TestAppContext) { @@ -309,15 +320,21 @@ mod tests { // Set up fake langauge server to return fuzzy matches against // a fixed set of symbol names. - let fake_symbol_names = ["one", "ton", "uno"]; + let fake_symbols = [ + symbol("one", "/external"), + symbol("ton", "/dir/test.rs"), + symbol("uno", "/dir/test.rs"), + ]; let fake_server = fake_servers.next().await.unwrap(); fake_server.handle_request::( move |params: lsp::WorkspaceSymbolParams, cx| { let executor = cx.background(); + let fake_symbols = fake_symbols.clone(); async move { - let candidates = fake_symbol_names - .into_iter() - .map(|name| StringMatchCandidate::new(0, name.into())) + let candidates = fake_symbols + .iter() + .enumerate() + .map(|(id, symbol)| StringMatchCandidate::new(id, symbol.name.clone())) .collect::>(); let matches = if params.query.is_empty() { Vec::new() @@ -334,7 +351,10 @@ mod tests { }; Ok(Some( - matches.into_iter().map(|mat| symbol(&mat.string)).collect(), + matches + .into_iter() + .map(|mat| fake_symbols[mat.candidate_id].clone()) + .collect(), )) } }, @@ -367,8 +387,8 @@ mod tests { cx.foreground().run_until_parked(); symbols_view.read_with(cx, |symbols_view, _| { assert_eq!(symbols_view.matches.len(), 2); - assert_eq!(symbols_view.matches[0].string, "one"); - assert_eq!(symbols_view.matches[1].string, "ton"); + assert_eq!(symbols_view.matches[0].string, "ton"); + assert_eq!(symbols_view.matches[1].string, "one"); }); // Spawn more updates such that in the end, there are again no matches. @@ -383,7 +403,7 @@ mod tests { }); } - fn symbol(name: &str) -> lsp::SymbolInformation { + fn symbol(name: &str, path: impl AsRef) -> lsp::SymbolInformation { #[allow(deprecated)] lsp::SymbolInformation { name: name.to_string(), @@ -392,7 +412,7 @@ mod tests { deprecated: None, container_name: None, location: lsp::Location::new( - lsp::Url::from_file_path("/a/b").unwrap(), + lsp::Url::from_file_path(path.as_ref()).unwrap(), lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)), ), } diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 0321e770cb..e8ff1932d4 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -2821,7 +2821,9 @@ mod tests { project.read_with(cx, |project, cx| { assert_eq!( project.active_entry(), - project.entry_for_path(&(worktree_id, "one.txt").into(), cx) + project + .entry_for_path(&(worktree_id, "one.txt").into(), cx) + .map(|e| e.id) ); }); assert_eq!( @@ -2838,7 +2840,9 @@ mod tests { project.read_with(cx, |project, cx| { assert_eq!( project.active_entry(), - project.entry_for_path(&(worktree_id, "two.txt").into(), cx) + project + .entry_for_path(&(worktree_id, "two.txt").into(), cx) + .map(|e| e.id) ); }); @@ -2856,7 +2860,9 @@ mod tests { project.read_with(cx, |project, cx| { assert_eq!( project.active_entry(), - project.entry_for_path(&(worktree_id, "one.txt").into(), cx) + project + .entry_for_path(&(worktree_id, "one.txt").into(), cx) + .map(|e| e.id) ); }); From 0bbf11672459c957063efb821cebb69653a30f06 Mon Sep 17 00:00:00 2001 From: Nate Butler Date: Tue, 26 Jul 2022 12:45:09 -0400 Subject: [PATCH 06/42] Use Fontawesome Icons Update arrow variants, download, check marks to use Font awesome icon variants --- assets/icons/arrow_down_12.svg | 2 +- assets/icons/arrow_down_16.svg | 2 +- assets/icons/arrow_down_8.svg | 2 +- assets/icons/arrow_left_12.svg | 2 +- assets/icons/arrow_left_16.svg | 2 +- assets/icons/arrow_left_8.svg | 2 +- assets/icons/arrow_right_12.svg | 2 +- assets/icons/arrow_right_16.svg | 2 +- assets/icons/arrow_right_8.svg | 2 +- assets/icons/arrow_up_12.svg | 2 +- assets/icons/arrow_up_16.svg | 2 +- assets/icons/arrow_up_8.svg | 2 +- assets/icons/backspace _12.svg | 3 +++ assets/icons/backspace _16.svg | 3 +++ assets/icons/backspace _8.svg | 3 +++ assets/icons/bolt_16.svg | 2 +- assets/icons/check_12.svg | 2 +- assets/icons/check_16.svg | 2 +- assets/icons/check_8.svg | 2 +- assets/icons/chevron_right_16.svg | 2 +- assets/icons/chevron_right_8.svg | 2 +- assets/icons/cloud_slash_12.svg | 2 +- assets/icons/cloud_slash_8.svg | 2 +- assets/icons/download_12.svg | 9 ++++++++- assets/icons/download_8.svg | 9 ++++++++- assets/icons/folder_tree_16.svg | 2 +- assets/icons/magnifying_glass_12.svg | 2 +- assets/icons/magnifying_glass_8.svg | 2 +- assets/icons/terminal_16.svg | 2 +- assets/icons/user_circle_8.svg | 2 +- assets/icons/user_group_12.svg | 2 +- assets/icons/user_group_16.svg | 2 +- assets/icons/user_group_8.svg | 2 +- assets/icons/user_plus_16.svg | 2 +- 34 files changed, 54 insertions(+), 31 deletions(-) create mode 100644 assets/icons/backspace _12.svg create mode 100644 assets/icons/backspace _16.svg create mode 100644 assets/icons/backspace _8.svg diff --git a/assets/icons/arrow_down_12.svg b/assets/icons/arrow_down_12.svg index 63cbe50997..5d9a8ee5e3 100644 --- a/assets/icons/arrow_down_12.svg +++ b/assets/icons/arrow_down_12.svg @@ -1,3 +1,3 @@ - + diff --git a/assets/icons/arrow_down_16.svg b/assets/icons/arrow_down_16.svg index 2e8a42a9ea..65114ebd60 100644 --- a/assets/icons/arrow_down_16.svg +++ b/assets/icons/arrow_down_16.svg @@ -1,3 +1,3 @@ - + diff --git a/assets/icons/arrow_down_8.svg b/assets/icons/arrow_down_8.svg index 18ac436d2a..53d3c2944e 100644 --- a/assets/icons/arrow_down_8.svg +++ b/assets/icons/arrow_down_8.svg @@ -1,3 +1,3 @@ - + diff --git a/assets/icons/arrow_left_12.svg b/assets/icons/arrow_left_12.svg index d7245fc106..fc7cabc9c6 100644 --- a/assets/icons/arrow_left_12.svg +++ b/assets/icons/arrow_left_12.svg @@ -1,3 +1,3 @@ - + diff --git a/assets/icons/arrow_left_16.svg b/assets/icons/arrow_left_16.svg index a313cf002e..07a2db5348 100644 --- a/assets/icons/arrow_left_16.svg +++ b/assets/icons/arrow_left_16.svg @@ -1,3 +1,3 @@ - + diff --git a/assets/icons/arrow_left_8.svg b/assets/icons/arrow_left_8.svg index 7851d9f219..a6a7079dd5 100644 --- a/assets/icons/arrow_left_8.svg +++ b/assets/icons/arrow_left_8.svg @@ -1,3 +1,3 @@ - + diff --git a/assets/icons/arrow_right_12.svg b/assets/icons/arrow_right_12.svg index a38f55dcaf..00dc3918fc 100644 --- a/assets/icons/arrow_right_12.svg +++ b/assets/icons/arrow_right_12.svg @@ -1,3 +1,3 @@ - + diff --git a/assets/icons/arrow_right_16.svg b/assets/icons/arrow_right_16.svg index 790a142162..b41e8fc810 100644 --- a/assets/icons/arrow_right_16.svg +++ b/assets/icons/arrow_right_16.svg @@ -1,3 +1,3 @@ - + diff --git a/assets/icons/arrow_right_8.svg b/assets/icons/arrow_right_8.svg index 3115e0e669..ef28fc81e5 100644 --- a/assets/icons/arrow_right_8.svg +++ b/assets/icons/arrow_right_8.svg @@ -1,3 +1,3 @@ - + diff --git a/assets/icons/arrow_up_12.svg b/assets/icons/arrow_up_12.svg index 84bcc4ebd8..ae4cf92899 100644 --- a/assets/icons/arrow_up_12.svg +++ b/assets/icons/arrow_up_12.svg @@ -1,3 +1,3 @@ - + diff --git a/assets/icons/arrow_up_16.svg b/assets/icons/arrow_up_16.svg index 63ea16b1d8..0d8add4ed7 100644 --- a/assets/icons/arrow_up_16.svg +++ b/assets/icons/arrow_up_16.svg @@ -1,3 +1,3 @@ - + diff --git a/assets/icons/arrow_up_8.svg b/assets/icons/arrow_up_8.svg index 24a040abbb..9d3f76a894 100644 --- a/assets/icons/arrow_up_8.svg +++ b/assets/icons/arrow_up_8.svg @@ -1,3 +1,3 @@ - + diff --git a/assets/icons/backspace _12.svg b/assets/icons/backspace _12.svg new file mode 100644 index 0000000000..68bad3da26 --- /dev/null +++ b/assets/icons/backspace _12.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/icons/backspace _16.svg b/assets/icons/backspace _16.svg new file mode 100644 index 0000000000..965470690e --- /dev/null +++ b/assets/icons/backspace _16.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/icons/backspace _8.svg b/assets/icons/backspace _8.svg new file mode 100644 index 0000000000..60972007b6 --- /dev/null +++ b/assets/icons/backspace _8.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/icons/bolt_16.svg b/assets/icons/bolt_16.svg index c3859443b2..aca476ef50 100644 --- a/assets/icons/bolt_16.svg +++ b/assets/icons/bolt_16.svg @@ -1,3 +1,3 @@ - + diff --git a/assets/icons/check_12.svg b/assets/icons/check_12.svg index e4255646bb..3e15dd7d1f 100644 --- a/assets/icons/check_12.svg +++ b/assets/icons/check_12.svg @@ -1,3 +1,3 @@ - + diff --git a/assets/icons/check_16.svg b/assets/icons/check_16.svg index 0ebf3fe75e..7e959b5924 100644 --- a/assets/icons/check_16.svg +++ b/assets/icons/check_16.svg @@ -1,3 +1,3 @@ - + diff --git a/assets/icons/check_8.svg b/assets/icons/check_8.svg index 7513909ce1..268f8bb498 100644 --- a/assets/icons/check_8.svg +++ b/assets/icons/check_8.svg @@ -1,3 +1,3 @@ - + diff --git a/assets/icons/chevron_right_16.svg b/assets/icons/chevron_right_16.svg index 3a2aafbcd6..270a33db70 100644 --- a/assets/icons/chevron_right_16.svg +++ b/assets/icons/chevron_right_16.svg @@ -1,3 +1,3 @@ - + diff --git a/assets/icons/chevron_right_8.svg b/assets/icons/chevron_right_8.svg index 7349274681..64910c54e7 100644 --- a/assets/icons/chevron_right_8.svg +++ b/assets/icons/chevron_right_8.svg @@ -1,3 +1,3 @@ - + diff --git a/assets/icons/cloud_slash_12.svg b/assets/icons/cloud_slash_12.svg index 37d0ee904c..adb60d2c27 100644 --- a/assets/icons/cloud_slash_12.svg +++ b/assets/icons/cloud_slash_12.svg @@ -1,3 +1,3 @@ - + diff --git a/assets/icons/cloud_slash_8.svg b/assets/icons/cloud_slash_8.svg index 524012d31a..9b7f0011b2 100644 --- a/assets/icons/cloud_slash_8.svg +++ b/assets/icons/cloud_slash_8.svg @@ -1,3 +1,3 @@ - + diff --git a/assets/icons/download_12.svg b/assets/icons/download_12.svg index 8386cecbeb..bcf66dfdf7 100644 --- a/assets/icons/download_12.svg +++ b/assets/icons/download_12.svg @@ -1,3 +1,10 @@ - + + + + + + + + diff --git a/assets/icons/download_8.svg b/assets/icons/download_8.svg index 629ba0c620..fb8b021d6b 100644 --- a/assets/icons/download_8.svg +++ b/assets/icons/download_8.svg @@ -1,3 +1,10 @@ - + + + + + + + + diff --git a/assets/icons/folder_tree_16.svg b/assets/icons/folder_tree_16.svg index 425bfa6c43..a264a32573 100644 --- a/assets/icons/folder_tree_16.svg +++ b/assets/icons/folder_tree_16.svg @@ -1,3 +1,3 @@ - + diff --git a/assets/icons/magnifying_glass_12.svg b/assets/icons/magnifying_glass_12.svg index b45c30172e..0ee5e24b24 100644 --- a/assets/icons/magnifying_glass_12.svg +++ b/assets/icons/magnifying_glass_12.svg @@ -1,3 +1,3 @@ - + diff --git a/assets/icons/magnifying_glass_8.svg b/assets/icons/magnifying_glass_8.svg index 00eb332440..958614203c 100644 --- a/assets/icons/magnifying_glass_8.svg +++ b/assets/icons/magnifying_glass_8.svg @@ -1,3 +1,3 @@ - + diff --git a/assets/icons/terminal_16.svg b/assets/icons/terminal_16.svg index 927982deee..95da7ff4e1 100644 --- a/assets/icons/terminal_16.svg +++ b/assets/icons/terminal_16.svg @@ -1,3 +1,3 @@ - + diff --git a/assets/icons/user_circle_8.svg b/assets/icons/user_circle_8.svg index 4bbf9c0301..7c884eec39 100644 --- a/assets/icons/user_circle_8.svg +++ b/assets/icons/user_circle_8.svg @@ -1,3 +1,3 @@ - + diff --git a/assets/icons/user_group_12.svg b/assets/icons/user_group_12.svg index 5eae1d55b7..13ed0b12e4 100644 --- a/assets/icons/user_group_12.svg +++ b/assets/icons/user_group_12.svg @@ -1,3 +1,3 @@ - + diff --git a/assets/icons/user_group_16.svg b/assets/icons/user_group_16.svg index 4f72d4b6b4..aa99277646 100644 --- a/assets/icons/user_group_16.svg +++ b/assets/icons/user_group_16.svg @@ -1,3 +1,3 @@ - + diff --git a/assets/icons/user_group_8.svg b/assets/icons/user_group_8.svg index 39058e51d4..9cf29f6e15 100644 --- a/assets/icons/user_group_8.svg +++ b/assets/icons/user_group_8.svg @@ -1,3 +1,3 @@ - + diff --git a/assets/icons/user_plus_16.svg b/assets/icons/user_plus_16.svg index 213eb8f13b..3fd6e13554 100644 --- a/assets/icons/user_plus_16.svg +++ b/assets/icons/user_plus_16.svg @@ -1,3 +1,3 @@ - + From 2bd0819ac80f2c687eb5131d952a04d3084ea6de Mon Sep 17 00:00:00 2001 From: K Simmons Date: Tue, 26 Jul 2022 10:41:02 -0700 Subject: [PATCH 07/42] Extract tab bar from workspace --- crates/theme/src/theme.rs | 5 +++-- crates/workspace/src/pane.rs | 35 +++++++++++++++-------------------- 2 files changed, 18 insertions(+), 22 deletions(-) diff --git a/crates/theme/src/theme.rs b/crates/theme/src/theme.rs index 23dfa4b58b..457f649a6b 100644 --- a/crates/theme/src/theme.rs +++ b/crates/theme/src/theme.rs @@ -75,12 +75,13 @@ pub struct TabBar { pub pane_button: Interactive, pub active_pane: TabStyles, pub inactive_pane: TabStyles, + pub height: f32, } #[derive(Clone, Deserialize, Default)] pub struct TabStyles { - active_tab: Tab, - inactive_tab: Tab, + pub active_tab: Tab, + pub inactive_tab: Tab, } #[derive(Clone, Deserialize, Default)] diff --git a/crates/workspace/src/pane.rs b/crates/workspace/src/pane.rs index c384748c3c..b06da7fe33 100644 --- a/crates/workspace/src/pane.rs +++ b/crates/workspace/src/pane.rs @@ -873,18 +873,24 @@ impl Pane { }; let is_pane_active = self.is_active; + + let tab_styles = match is_pane_active { + true => theme.workspace.tab_bar.active_pane.clone(), + false => theme.workspace.tab_bar.inactive_pane.clone(), + }; + let filler_style = tab_styles.inactive_tab.clone(); + let mut row = Flex::row().scrollable::(1, autoscroll, cx); for (ix, (item, detail)) in self.items.iter().zip(self.tab_details(cx)).enumerate() { let detail = if detail == 0 { None } else { Some(detail) }; let is_tab_active = ix == self.active_item_index; row.add_child({ - let mut tab_style = match (is_pane_active, is_tab_active) { - (true, true) => theme.workspace.active_pane_active_tab.clone(), - (true, false) => theme.workspace.active_pane_inactive_tab.clone(), - (false, true) => theme.workspace.inactive_pane_active_tab.clone(), - (false, false) => theme.workspace.inactive_pane_inactive_tab.clone(), + let mut tab_style = match is_tab_active { + true => tab_styles.active_tab.clone(), + false => tab_styles.inactive_tab.clone(), }; + let title = item.tab_content(detail, &tab_style, cx); if ix == 0 { @@ -996,17 +1002,11 @@ impl Pane { }) } - let filler_style = if is_pane_active { - &theme.workspace.active_pane_inactive_tab - } else { - &theme.workspace.inactive_pane_inactive_tab - }; - row.add_child( Empty::new() .contained() .with_style(filler_style.container) - .with_border(theme.workspace.active_pane_active_tab.container.border) + .with_border(filler_style.container.border) .flex(0., true) .named("filler"), ); @@ -1081,7 +1081,8 @@ impl View for Pane { 0, cx, |mouse_state, cx| { - let theme = &cx.global::().theme.workspace; + let theme = + &cx.global::().theme.workspace.tab_bar; let style = theme.pane_button.style_for(mouse_state, false); Svg::new("icons/split_12.svg") @@ -1111,13 +1112,7 @@ impl View for Pane { tab_row .constrained() - .with_height( - cx.global::() - .theme - .workspace - .active_pane_active_tab - .height, - ) + .with_height(cx.global::().theme.workspace.tab_bar.height) .boxed() }) .with_child(ChildView::new(&self.toolbar).boxed()) From 01eed2c8449e79b5268954ccfe005f65e4a0a0f0 Mon Sep 17 00:00:00 2001 From: Nate Butler Date: Tue, 26 Jul 2022 15:53:48 -0400 Subject: [PATCH 08/42] Rebalance text and icon color usage... ...to better differentiate between primary, secondary and muted. --- styles/package-lock.json | 1 + styles/src/styleTree/contextMenu.ts | 4 ++-- styles/src/styleTree/editor.ts | 16 ++++++++-------- styles/src/styleTree/picker.ts | 4 ++-- styles/src/styleTree/projectPanel.ts | 11 +++++------ styles/src/styleTree/statusBar.ts | 20 ++++++++++---------- styles/src/styleTree/tooltip.ts | 4 ++-- styles/src/styleTree/workspace.ts | 2 +- 8 files changed, 31 insertions(+), 31 deletions(-) diff --git a/styles/package-lock.json b/styles/package-lock.json index 5499f1852c..582f1c8496 100644 --- a/styles/package-lock.json +++ b/styles/package-lock.json @@ -5,6 +5,7 @@ "requires": true, "packages": { "": { + "name": "styles", "version": "1.0.0", "license": "ISC", "dependencies": { diff --git a/styles/src/styleTree/contextMenu.ts b/styles/src/styleTree/contextMenu.ts index a7c69d7660..0244641ec7 100644 --- a/styles/src/styleTree/contextMenu.ts +++ b/styles/src/styleTree/contextMenu.ts @@ -18,7 +18,7 @@ export default function contextMenu(theme: Theme) { item: { padding: { left: 4, right: 4, top: 2, bottom: 2 }, cornerRadius: 6, - label: text(theme, "sans", "secondary", { size: "sm" }), + label: text(theme, "sans", "primary", { size: "sm" }), keystroke: { ...text(theme, "sans", "muted", { size: "sm", weight: "bold" }), padding: { left: 3, right: 3 }, @@ -29,7 +29,7 @@ export default function contextMenu(theme: Theme) { }, active: { background: backgroundColor(theme, 300, "active"), - text: text(theme, "sans", "primary", { size: "sm" }), + text: text(theme, "sans", "active", { size: "sm" }), }, activeHover: { background: backgroundColor(theme, 300, "hovered"), diff --git a/styles/src/styleTree/editor.ts b/styles/src/styleTree/editor.ts index 507eb74d8d..e3c1844974 100644 --- a/styles/src/styleTree/editor.ts +++ b/styles/src/styleTree/editor.ts @@ -55,7 +55,7 @@ export default function editor(theme: Theme) { textColor: theme.syntax.primary.color, background: backgroundColor(theme, 500), activeLineBackground: theme.editor.line.active, - codeActionsIndicator: iconColor(theme, "muted"), + codeActionsIndicator: iconColor(theme, "secondary"), diffBackgroundDeleted: backgroundColor(theme, "error"), diffBackgroundInserted: backgroundColor(theme, "ok"), documentHighlightReadBackground: theme.editor.highlight.occurrence, @@ -107,7 +107,7 @@ export default function editor(theme: Theme) { top: true, }), code: { - ...text(theme, "mono", "muted", { size: "sm" }), + ...text(theme, "mono", "secondary", { size: "sm" }), margin: { left: 10, }, @@ -135,17 +135,17 @@ export default function editor(theme: Theme) { warningDiagnostic: diagnostic(theme, "warning"), informationDiagnostic: diagnostic(theme, "info"), hintDiagnostic: diagnostic(theme, "info"), - invalidErrorDiagnostic: diagnostic(theme, "muted"), - invalidHintDiagnostic: diagnostic(theme, "muted"), - invalidInformationDiagnostic: diagnostic(theme, "muted"), - invalidWarningDiagnostic: diagnostic(theme, "muted"), + invalidErrorDiagnostic: diagnostic(theme, "secondary"), + invalidHintDiagnostic: diagnostic(theme, "secondary"), + invalidInformationDiagnostic: diagnostic(theme, "secondary"), + invalidWarningDiagnostic: diagnostic(theme, "secondary"), hoverPopover: hoverPopover(theme), linkDefinition: { color: theme.syntax.linkUri.color, underline: theme.syntax.linkUri.underline, }, jumpIcon: { - color: iconColor(theme, "muted"), + color: iconColor(theme, "secondary"), iconWidth: 20, buttonWidth: 20, cornerRadius: 6, @@ -157,7 +157,7 @@ export default function editor(theme: Theme) { }, hover: { color: iconColor(theme, "active"), - background: backgroundColor(theme, "on500", "base"), + background: backgroundColor(theme, "on500"), }, }, compositionMark: { diff --git a/styles/src/styleTree/picker.ts b/styles/src/styleTree/picker.ts index 84a72aabcc..514deb4c33 100644 --- a/styles/src/styleTree/picker.ts +++ b/styles/src/styleTree/picker.ts @@ -24,7 +24,7 @@ export default function picker(theme: Theme) { highlightText: text(theme, "sans", "feature", { weight: "bold" }), active: { background: backgroundColor(theme, 300, "active"), - text: text(theme, "sans", "primary"), + text: text(theme, "sans", "active"), }, hover: { background: backgroundColor(theme, 300, "hovered"), @@ -32,7 +32,7 @@ export default function picker(theme: Theme) { }, border: border(theme, "primary"), empty: { - text: text(theme, "sans", "placeholder"), + text: text(theme, "sans", "muted"), padding: { bottom: 4, left: 12, diff --git a/styles/src/styleTree/projectPanel.ts b/styles/src/styleTree/projectPanel.ts index 7007a5da2d..26f88c6784 100644 --- a/styles/src/styleTree/projectPanel.ts +++ b/styles/src/styleTree/projectPanel.ts @@ -12,25 +12,24 @@ export default function projectPanel(theme: Theme) { iconColor: iconColor(theme, "muted"), iconSize: 8, iconSpacing: 8, - text: text(theme, "mono", "muted", { size: "sm" }), + text: text(theme, "mono", "secondary", { size: "sm" }), hover: { background: backgroundColor(theme, 300, "hovered"), - text: text(theme, "mono", "primary", { size: "sm" }), }, active: { background: backgroundColor(theme, 300, "active"), - text: text(theme, "mono", "primary", { size: "sm" }), + text: text(theme, "mono", "active", { size: "sm" }), }, activeHover: { - background: backgroundColor(theme, 300, "hovered"), + background: backgroundColor(theme, 300, "active"), text: text(theme, "mono", "active", { size: "sm" }), }, }, cutEntryFade: 0.4, ignoredEntryFade: 0.6, filenameEditor: { - background: backgroundColor(theme, 500, "active"), - text: text(theme, "mono", "primary", { size: "sm" }), + background: backgroundColor(theme, "on300"), + text: text(theme, "mono", "active", { size: "sm" }), selection: player(theme, 1).selection, }, }; diff --git a/styles/src/styleTree/statusBar.ts b/styles/src/styleTree/statusBar.ts index d5d63fdadf..2a4c6d7f70 100644 --- a/styles/src/styleTree/statusBar.ts +++ b/styles/src/styleTree/statusBar.ts @@ -23,28 +23,28 @@ export default function statusBar(theme: Theme) { right: 6, }, border: border(theme, "primary", { top: true, overlay: true }), - cursorPosition: text(theme, "sans", "muted"), - autoUpdateProgressMessage: text(theme, "sans", "muted"), - autoUpdateDoneMessage: text(theme, "sans", "muted"), + cursorPosition: text(theme, "sans", "secondary"), + autoUpdateProgressMessage: text(theme, "sans", "secondary"), + autoUpdateDoneMessage: text(theme, "sans", "secondary"), lspStatus: { ...diagnosticStatusContainer, iconSpacing: 4, iconWidth: 14, height: 18, - message: text(theme, "sans", "muted"), + message: text(theme, "sans", "secondary"), iconColor: iconColor(theme, "muted"), hover: { message: text(theme, "sans", "primary"), - iconColor: iconColor(theme, "active"), + iconColor: iconColor(theme, "primary"), background: backgroundColor(theme, 300, "hovered"), }, }, diagnosticMessage: { - ...text(theme, "sans", "muted"), - hover: text(theme, "sans", "secondary"), + ...text(theme, "sans", "secondary"), + hover: text(theme, "sans", "active"), }, feedback: { - ...text(theme, "sans", "muted"), + ...text(theme, "sans", "secondary"), hover: text(theme, "sans", "active"), }, diagnosticSummary: { @@ -53,7 +53,7 @@ export default function statusBar(theme: Theme) { iconSpacing: 2, summarySpacing: 6, text: text(theme, "sans", "primary", { size: "sm" }), - iconColorOk: iconColor(theme, "secondary"), + iconColorOk: iconColor(theme, "muted"), iconColorWarning: iconColor(theme, "warning"), iconColorError: iconColor(theme, "error"), containerOk: { @@ -95,7 +95,7 @@ export default function statusBar(theme: Theme) { item: { ...statusContainer, iconSize: 16, - iconColor: iconColor(theme, "secondary"), + iconColor: iconColor(theme, "muted"), hover: { iconColor: iconColor(theme, "active"), background: backgroundColor(theme, 300, "hovered"), diff --git a/styles/src/styleTree/tooltip.ts b/styles/src/styleTree/tooltip.ts index 8d1bb8078d..7545ad762e 100644 --- a/styles/src/styleTree/tooltip.ts +++ b/styles/src/styleTree/tooltip.ts @@ -9,13 +9,13 @@ export default function tooltip(theme: Theme) { margin: { top: 6, left: 6 }, shadow: popoverShadow(theme), cornerRadius: 6, - text: text(theme, "sans", "secondary", { size: "xs", weight: "bold" }), + text: text(theme, "sans", "primary", { size: "xs" }), keystroke: { background: backgroundColor(theme, "on500"), cornerRadius: 4, margin: { left: 6 }, padding: { left: 4, right: 4 }, - ...text(theme, "mono", "muted", { size: "xs", weight: "bold" }), + ...text(theme, "mono", "secondary", { size: "xs", weight: "bold" }), }, maxTextWidth: 200, }; diff --git a/styles/src/styleTree/workspace.ts b/styles/src/styleTree/workspace.ts index 4937edfdee..875b2bb2d5 100644 --- a/styles/src/styleTree/workspace.ts +++ b/styles/src/styleTree/workspace.ts @@ -128,7 +128,7 @@ export default function workspace(theme: Theme) { cornerRadius: 6, hover: { color: iconColor(theme, "active"), - background: backgroundColor(theme, 300), + background: backgroundColor(theme, "on500", "hovered"), }, disabled: { color: withOpacity(iconColor(theme, "muted"), 0.6), From 20b8e03ff0792ca271b34f9a1f2ebd1b51fae56d Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Tue, 26 Jul 2022 14:20:29 -0700 Subject: [PATCH 09/42] Restore lack of bottom border on active tab Co-authored-by: Keith Simmons --- styles/src/styleTree/tabBar.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/styles/src/styleTree/tabBar.ts b/styles/src/styleTree/tabBar.ts index d35af34ac9..66da26d7ed 100644 --- a/styles/src/styleTree/tabBar.ts +++ b/styles/src/styleTree/tabBar.ts @@ -35,6 +35,7 @@ export default function tabBar(theme: Theme) { text: text(theme, "sans", "active", { size: "sm" }), border: { ...tab.border, + bottom: false }, }; @@ -50,6 +51,7 @@ export default function tabBar(theme: Theme) { text: text(theme, "sans", "secondary", { size: "sm" }), border: { ...tab.border, + bottom: false }, } @@ -72,7 +74,7 @@ export default function tabBar(theme: Theme) { paneButton: { color: iconColor(theme, "secondary"), border: { - ...activePaneActiveTab.border, + ...tab.border, }, iconWidth: 12, buttonWidth: activePaneActiveTab.height, From ef1efb615e816064cb7e3aad0dcee3c5aa0d1029 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Tue, 26 Jul 2022 14:26:40 -0700 Subject: [PATCH 10/42] 0.49.0 --- Cargo.lock | 2 +- crates/zed/Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 8e9e976efd..ed860b9ecf 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6993,7 +6993,7 @@ dependencies = [ [[package]] name = "zed" -version = "0.48.1" +version = "0.49.0" dependencies = [ "activity_indicator", "anyhow", diff --git a/crates/zed/Cargo.toml b/crates/zed/Cargo.toml index 0fe39b4096..d56305cee9 100644 --- a/crates/zed/Cargo.toml +++ b/crates/zed/Cargo.toml @@ -3,7 +3,7 @@ authors = ["Nathan Sobo "] description = "The fast, collaborative code editor." edition = "2021" name = "zed" -version = "0.48.1" +version = "0.49.0" [lib] name = "zed" From 88202a567caa05b1e3a316845f226e149efe7354 Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Tue, 26 Jul 2022 16:22:07 -0700 Subject: [PATCH 11/42] Fixed regression in working directory code --- crates/terminal/src/terminal.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/crates/terminal/src/terminal.rs b/crates/terminal/src/terminal.rs index 4e5829559e..1dc4b0ddbf 100644 --- a/crates/terminal/src/terminal.rs +++ b/crates/terminal/src/terminal.rs @@ -335,7 +335,8 @@ fn get_working_directory(workspace: &Workspace, cx: &AppContext) -> Option current_project_directory(workspace, cx), + WorkingDirectory::CurrentProjectDirectory => current_project_directory(workspace, cx) + .or_else(|| first_project_directory(workspace, cx)), WorkingDirectory::FirstProjectDirectory => first_project_directory(workspace, cx), WorkingDirectory::AlwaysHome => None, WorkingDirectory::Always { directory } => { From 8a6605c090b72cf96b6cefec954eb35bf6696e46 Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Tue, 26 Jul 2022 16:30:51 -0700 Subject: [PATCH 12/42] Added test covering this feature --- assets/settings/default.json | 3 +- crates/terminal/src/modal_view.rs | 10 ++++++- crates/terminal/src/terminal.rs | 47 +++++++++++++++++++++++++------ 3 files changed, 49 insertions(+), 11 deletions(-) diff --git a/assets/settings/default.json b/assets/settings/default.json index 6c34d6be70..5c05e8eaba 100644 --- a/assets/settings/default.json +++ b/assets/settings/default.json @@ -84,7 +84,8 @@ "shell": "system", // What working directory to use when launching the terminal. // May take 4 values: - // 1. Use the current file's project directory. + // 1. Use the current file's project directory. Will Fallback to the + // first project directory strategy if unsuccessful // "working_directory": "current_project_directory" // 2. Use the first project in this workspace's directory // "working_directory": "first_project_directory" diff --git a/crates/terminal/src/modal_view.rs b/crates/terminal/src/modal_view.rs index ec5280befc..9ff0507382 100644 --- a/crates/terminal/src/modal_view.rs +++ b/crates/terminal/src/modal_view.rs @@ -1,4 +1,5 @@ use gpui::{ModelHandle, ViewContext}; +use settings::{Settings, WorkingDirectory}; use workspace::Workspace; use crate::{ @@ -27,7 +28,14 @@ pub fn deploy_modal(workspace: &mut Workspace, _: &DeployModal, cx: &mut ViewCon // No connection was stored, create a new terminal if let Some(closed_terminal_handle) = workspace.toggle_modal(cx, |workspace, cx| { // No terminal modal visible, construct a new one. - let working_directory = get_working_directory(workspace, cx); + let wd_strategy = cx + .global::() + .terminal_overrides + .working_directory + .clone() + .unwrap_or(WorkingDirectory::CurrentProjectDirectory); + + let working_directory = get_working_directory(workspace, cx, wd_strategy); let this = cx.add_view(|cx| TerminalView::new(working_directory, true, cx)); diff --git a/crates/terminal/src/terminal.rs b/crates/terminal/src/terminal.rs index 1dc4b0ddbf..27339d2d00 100644 --- a/crates/terminal/src/terminal.rs +++ b/crates/terminal/src/terminal.rs @@ -80,7 +80,14 @@ impl Entity for ErrorView { impl TerminalView { ///Create a new Terminal in the current working directory or the user's home directory fn deploy(workspace: &mut Workspace, _: &Deploy, cx: &mut ViewContext) { - let working_directory = get_working_directory(workspace, cx); + let wd_strategy = cx + .global::() + .terminal_overrides + .working_directory + .clone() + .unwrap_or(WorkingDirectory::CurrentProjectDirectory); + + let working_directory = get_working_directory(workspace, cx, wd_strategy); let view = cx.add_view(|cx| TerminalView::new(working_directory, false, cx)); workspace.add_item(Box::new(view), cx); } @@ -327,14 +334,12 @@ impl Item for TerminalView { } ///Get's the working directory for the given workspace, respecting the user's settings. -fn get_working_directory(workspace: &Workspace, cx: &AppContext) -> Option { - let wd_setting = cx - .global::() - .terminal_overrides - .working_directory - .clone() - .unwrap_or(WorkingDirectory::CurrentProjectDirectory); - let res = match wd_setting { +fn get_working_directory( + workspace: &Workspace, + cx: &AppContext, + strategy: WorkingDirectory, +) -> Option { + let res = match strategy { WorkingDirectory::CurrentProjectDirectory => current_project_directory(workspace, cx) .or_else(|| first_project_directory(workspace, cx)), WorkingDirectory::FirstProjectDirectory => first_project_directory(workspace, cx), @@ -520,4 +525,28 @@ mod tests { assert_eq!(res, Some((Path::new("/root1/")).to_path_buf())); }); } + + //Active entry with a work tree, worktree is a file, integration test with the strategy interface + #[gpui::test] + async fn active_entry_worktree_is_file_int(cx: &mut TestAppContext) { + //Setup variables + let mut cx = TerminalTestContext::new(cx, true); + let (project, workspace) = cx.blank_workspace().await; + let (_wt, _entry) = cx.create_folder_wt(project.clone(), "/root1/").await; + let (wt2, entry2) = cx.create_file_wt(project.clone(), "/root2.txt").await; + cx.insert_active_entry_for(wt2, entry2, project.clone()); + + //Test + cx.cx.update(|cx| { + let workspace = workspace.read(cx); + let active_entry = project.read(cx).active_entry(); + + assert!(active_entry.is_some()); + + let res = + get_working_directory(workspace, cx, WorkingDirectory::CurrentProjectDirectory); + let first = first_project_directory(workspace, cx); + assert_eq!(res, first); + }); + } } From 09ed149184356e7e88ab7d4a9117f7511c7af20d Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Wed, 20 Jul 2022 09:07:54 -0700 Subject: [PATCH 13/42] Improve calculation of which lines are new when auto-indenting --- crates/language/src/buffer.rs | 92 +++++++++++++++++++---------------- crates/language/src/tests.rs | 73 +++++++++++++++++++++++++-- 2 files changed, 121 insertions(+), 44 deletions(-) diff --git a/crates/language/src/buffer.rs b/crates/language/src/buffer.rs index bff9438124..e2685ba36e 100644 --- a/crates/language/src/buffer.rs +++ b/crates/language/src/buffer.rs @@ -232,7 +232,7 @@ struct SyntaxTree { struct AutoindentRequest { before_edit: BufferSnapshot, edited: Vec, - inserted: Option>>, + inserted: Vec>, indent_size: IndentSize, } @@ -874,9 +874,10 @@ impl Buffer { yield_now().await; } - if let Some(inserted) = request.inserted.as_ref() { + if !request.inserted.is_empty() { let inserted_row_ranges = contiguous_ranges( - inserted + request + .inserted .iter() .map(|range| range.to_point(&snapshot)) .flat_map(|range| range.start.row..range.end.row + 1), @@ -1203,52 +1204,61 @@ impl Buffer { self.start_transaction(); self.pending_autoindent.take(); - let autoindent_request = - self.language - .as_ref() - .and_then(|_| autoindent_size) - .map(|autoindent_size| { - let before_edit = self.snapshot(); - let edited = edits - .iter() - .filter_map(|(range, new_text)| { - let start = range.start.to_point(self); - if new_text.starts_with('\n') - && start.column == self.line_len(start.row) - { - None - } else { - Some(self.anchor_before(range.start)) - } - }) - .collect(); - (before_edit, edited, autoindent_size) - }); + let autoindent_request = self + .language + .as_ref() + .and_then(|_| autoindent_size) + .map(|autoindent_size| (self.snapshot(), autoindent_size)); let edit_operation = self.text.edit(edits.iter().cloned()); let edit_id = edit_operation.local_timestamp(); - if let Some((before_edit, edited, size)) = autoindent_request { - let mut delta = 0isize; + if let Some((before_edit, size)) = autoindent_request { + let mut inserted = Vec::new(); + let mut edited = Vec::new(); - let inserted_ranges = edits + let mut delta = 0isize; + for ((range, _), new_text) in edits .into_iter() .zip(&edit_operation.as_edit().unwrap().new_text) - .filter_map(|((range, _), new_text)| { - let first_newline_ix = new_text.find('\n')?; - let new_text_len = new_text.len(); - let start = (delta + range.start as isize) as usize + first_newline_ix + 1; - let end = (delta + range.start as isize) as usize + new_text_len; - delta += new_text_len as isize - (range.end as isize - range.start as isize); - Some(self.anchor_before(start)..self.anchor_after(end)) - }) - .collect::>>(); + { + let new_text_len = new_text.len(); + let first_newline_ix = new_text.find('\n'); + let old_start = range.start.to_point(&before_edit); - let inserted = if inserted_ranges.is_empty() { - None - } else { - Some(inserted_ranges) - }; + let start = (delta + range.start as isize) as usize; + delta += new_text_len as isize - (range.end as isize - range.start as isize); + + // When inserting multiple lines of text at the beginning of a line, + // treat all of the affected lines as newly-inserted. + if first_newline_ix.is_some() + && old_start.column < before_edit.indent_size_for_line(old_start.row).len + { + inserted + .push(self.anchor_before(start)..self.anchor_after(start + new_text_len)); + continue; + } + + // When inserting a newline at the end of an existing line, treat the following + // line as newly-inserted. + if first_newline_ix == Some(0) + && old_start.column == before_edit.line_len(old_start.row) + { + inserted.push( + self.anchor_before(start + 1)..self.anchor_after(start + new_text_len), + ); + continue; + } + + // Otherwise, mark the start of the edit as edited, and any subsequent + // lines as newly inserted. + edited.push(before_edit.anchor_before(range.start)); + if let Some(ix) = first_newline_ix { + inserted.push( + self.anchor_before(start + ix + 1)..self.anchor_after(start + new_text_len), + ); + } + } self.autoindent_requests.push(Arc::new(AutoindentRequest { before_edit, diff --git a/crates/language/src/tests.rs b/crates/language/src/tests.rs index ac3759c257..3f1f0f1b8c 100644 --- a/crates/language/src/tests.rs +++ b/crates/language/src/tests.rs @@ -756,8 +756,18 @@ fn test_autoindent_does_not_adjust_lines_with_unchanged_suggestion(cx: &mut Muta }); cx.add_model(|cx| { - let text = "fn a() {\n {\n b()?\n }\n\n Ok(())\n}"; + let text = " + fn a() { + { + b()? + } + Ok(()) + } + " + .unindent(); let mut buffer = Buffer::new(0, text, cx).with_language(Arc::new(rust_lang()), cx); + + // Delete a closing curly brace changes the suggested indent for the line. buffer.edit_with_autoindent( [(Point::new(3, 4)..Point::new(3, 5), "")], IndentSize::spaces(4), @@ -765,9 +775,19 @@ fn test_autoindent_does_not_adjust_lines_with_unchanged_suggestion(cx: &mut Muta ); assert_eq!( buffer.text(), - "fn a() {\n {\n b()?\n \n\n Ok(())\n}" + " + fn a() { + { + b()? + | + Ok(()) + } + " + .replace("|", "") // included in the string to preserve trailing whites + .unindent() ); + // Manually editing the leading whitespace buffer.edit_with_autoindent( [(Point::new(3, 0)..Point::new(3, 12), "")], IndentSize::spaces(4), @@ -775,7 +795,15 @@ fn test_autoindent_does_not_adjust_lines_with_unchanged_suggestion(cx: &mut Muta ); assert_eq!( buffer.text(), - "fn a() {\n {\n b()?\n\n\n Ok(())\n}" + " + fn a() { + { + b()? + + Ok(()) + } + " + .unindent() ); buffer }); @@ -832,6 +860,45 @@ fn test_autoindent_with_edit_at_end_of_buffer(cx: &mut MutableAppContext) { }); } +#[gpui::test] +fn test_autoindent_multi_line_insertion(cx: &mut MutableAppContext) { + cx.add_model(|cx| { + let text = " + const a: usize = 1; + fn b() { + if c { + let d = 2; + } + } + " + .unindent(); + + let mut buffer = Buffer::new(0, text, cx).with_language(Arc::new(rust_lang()), cx); + buffer.edit_with_autoindent( + [(Point::new(3, 0)..Point::new(3, 0), "e(\n f()\n);\n")], + IndentSize::spaces(4), + cx, + ); + assert_eq!( + buffer.text(), + " + const a: usize = 1; + fn b() { + if c { + e( + f() + ); + let d = 2; + } + } + " + .unindent() + ); + + buffer + }); +} + #[gpui::test] fn test_autoindent_disabled(cx: &mut MutableAppContext) { cx.add_model(|cx| { From 537530bf76a011dbda1982203276302be68bc60b Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Tue, 26 Jul 2022 08:56:22 -0700 Subject: [PATCH 14/42] :art: compute_autoindents --- crates/language/src/buffer.rs | 109 +++++++++++++++------------------- 1 file changed, 48 insertions(+), 61 deletions(-) diff --git a/crates/language/src/buffer.rs b/crates/language/src/buffer.rs index e2685ba36e..c349ed50f2 100644 --- a/crates/language/src/buffer.rs +++ b/crates/language/src/buffer.rs @@ -19,7 +19,7 @@ use smol::future::yield_now; use std::{ any::Any, cmp::{self, Ordering}, - collections::{BTreeMap, HashMap}, + collections::BTreeMap, ffi::OsStr, future::Future, iter::{self, Iterator, Peekable}, @@ -808,7 +808,7 @@ impl Buffer { ) .collect::>(); - let mut old_suggestions = HashMap::::default(); + let mut old_suggestions = BTreeMap::::default(); let old_edited_ranges = contiguous_ranges(old_to_new_rows.keys().copied(), max_rows_between_yields); for old_edited_range in old_edited_ranges { @@ -819,19 +819,15 @@ impl Buffer { .flatten(); for (old_row, suggestion) in old_edited_range.zip(suggestions) { if let Some(suggestion) = suggestion { - let mut suggested_indent = old_to_new_rows + let suggested_indent = old_to_new_rows .get(&suggestion.basis_row) .and_then(|from_row| old_suggestions.get(from_row).copied()) .unwrap_or_else(|| { request .before_edit .indent_size_for_line(suggestion.basis_row) - }); - if suggestion.delta.is_gt() { - suggested_indent += request.indent_size; - } else if suggestion.delta.is_lt() { - suggested_indent -= request.indent_size; - } + }) + .with_delta(suggestion.delta, request.indent_size); old_suggestions .insert(*old_to_new_rows.get(&old_row).unwrap(), suggested_indent); } @@ -850,17 +846,13 @@ impl Buffer { .flatten(); for (new_row, suggestion) in new_edited_row_range.zip(suggestions) { if let Some(suggestion) = suggestion { - let mut suggested_indent = indent_sizes + let suggested_indent = indent_sizes .get(&suggestion.basis_row) .copied() .unwrap_or_else(|| { snapshot.indent_size_for_line(suggestion.basis_row) - }); - if suggestion.delta.is_gt() { - suggested_indent += request.indent_size; - } else if suggestion.delta.is_lt() { - suggested_indent -= request.indent_size; - } + }) + .with_delta(suggestion.delta, request.indent_size); if old_suggestions .get(&new_row) .map_or(true, |old_indentation| { @@ -874,38 +866,32 @@ impl Buffer { yield_now().await; } - if !request.inserted.is_empty() { - let inserted_row_ranges = contiguous_ranges( - request - .inserted - .iter() - .map(|range| range.to_point(&snapshot)) - .flat_map(|range| range.start.row..range.end.row + 1), - max_rows_between_yields, - ); - for inserted_row_range in inserted_row_ranges { - let suggestions = snapshot - .suggest_autoindents(inserted_row_range.clone()) - .into_iter() - .flatten(); - for (row, suggestion) in inserted_row_range.zip(suggestions) { - if let Some(suggestion) = suggestion { - let mut suggested_indent = indent_sizes - .get(&suggestion.basis_row) - .copied() - .unwrap_or_else(|| { - snapshot.indent_size_for_line(suggestion.basis_row) - }); - if suggestion.delta.is_gt() { - suggested_indent += request.indent_size; - } else if suggestion.delta.is_lt() { - suggested_indent -= request.indent_size; - } - indent_sizes.insert(row, suggested_indent); - } + let inserted_row_ranges = contiguous_ranges( + request + .inserted + .iter() + .map(|range| range.to_point(&snapshot)) + .flat_map(|range| range.start.row..range.end.row + 1), + max_rows_between_yields, + ); + for inserted_row_range in inserted_row_ranges { + let suggestions = snapshot + .suggest_autoindents(inserted_row_range.clone()) + .into_iter() + .flatten(); + for (row, suggestion) in inserted_row_range.zip(suggestions) { + if let Some(suggestion) = suggestion { + let suggested_indent = indent_sizes + .get(&suggestion.basis_row) + .copied() + .unwrap_or_else(|| { + snapshot.indent_size_for_line(suggestion.basis_row) + }) + .with_delta(suggestion.delta, request.indent_size); + indent_sizes.insert(row, suggested_indent); } - yield_now().await; } + yield_now().await; } } @@ -2513,23 +2499,24 @@ impl IndentSize { IndentKind::Tab => '\t', } } -} -impl std::ops::AddAssign for IndentSize { - fn add_assign(&mut self, other: IndentSize) { - if self.len == 0 { - *self = other; - } else if self.kind == other.kind { - self.len += other.len; - } - } -} - -impl std::ops::SubAssign for IndentSize { - fn sub_assign(&mut self, other: IndentSize) { - if self.kind == other.kind && self.len >= other.len { - self.len -= other.len; + pub fn with_delta(mut self, direction: Ordering, size: IndentSize) -> Self { + match direction { + Ordering::Less => { + if self.kind == size.kind && self.len >= size.len { + self.len -= size.len; + } + } + Ordering::Equal => {} + Ordering::Greater => { + if self.len == 0 { + self = size; + } else if self.kind == size.kind { + self.len += size.len; + } + } } + self } } From f547c268ced1b1a84b57cf4f29574084298f66e7 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Tue, 26 Jul 2022 17:43:43 -0700 Subject: [PATCH 15/42] Restructure autoindent to preserve relative indentation of inserted text --- crates/language/src/buffer.rs | 172 ++++++++++++++++++---------------- crates/language/src/tests.rs | 47 ++++++++++ 2 files changed, 137 insertions(+), 82 deletions(-) diff --git a/crates/language/src/buffer.rs b/crates/language/src/buffer.rs index c349ed50f2..ef9b266315 100644 --- a/crates/language/src/buffer.rs +++ b/crates/language/src/buffer.rs @@ -231,11 +231,16 @@ struct SyntaxTree { #[derive(Clone)] struct AutoindentRequest { before_edit: BufferSnapshot, - edited: Vec, - inserted: Vec>, + entries: Vec, indent_size: IndentSize, } +#[derive(Clone)] +struct AutoindentRequestEntry { + range: Range, + first_line_is_new: bool, +} + #[derive(Debug)] struct IndentSuggestion { basis_row: u32, @@ -796,17 +801,20 @@ impl Buffer { Some(async move { let mut indent_sizes = BTreeMap::new(); for request in autoindent_requests { - let old_to_new_rows = request - .edited - .iter() - .map(|anchor| anchor.summary::(&request.before_edit).row) - .zip( - request - .edited - .iter() - .map(|anchor| anchor.summary::(&snapshot).row), - ) - .collect::>(); + let mut row_ranges = Vec::new(); + let mut old_to_new_rows = BTreeMap::new(); + for entry in &request.entries { + let position = entry.range.start; + let new_row = position.to_point(&snapshot).row; + let new_end_row = entry.range.end.to_point(&snapshot).row + 1; + if !entry.first_line_is_new { + let old_row = position.to_point(&request.before_edit).row; + old_to_new_rows.insert(old_row, new_row); + } + if new_end_row > new_row { + row_ranges.push(new_row..new_end_row); + } + } let mut old_suggestions = BTreeMap::::default(); let old_edited_ranges = @@ -835,10 +843,13 @@ impl Buffer { yield_now().await; } - // At this point, old_suggestions contains the suggested indentation for all edited lines with respect to the state of the - // buffer before the edit, but keyed by the row for these lines after the edits were applied. - let new_edited_row_ranges = - contiguous_ranges(old_to_new_rows.values().copied(), max_rows_between_yields); + // At this point, old_suggestions contains the suggested indentation for all edited lines + // with respect to the state of the buffer before the edit, but keyed by the row for these + // lines after the edits were applied. + let new_edited_row_ranges = contiguous_ranges( + row_ranges.iter().map(|range| range.start), + max_rows_between_yields, + ); for new_edited_row_range in new_edited_row_ranges { let suggestions = snapshot .suggest_autoindents(new_edited_row_range.clone()) @@ -866,32 +877,31 @@ impl Buffer { yield_now().await; } - let inserted_row_ranges = contiguous_ranges( - request - .inserted - .iter() - .map(|range| range.to_point(&snapshot)) - .flat_map(|range| range.start.row..range.end.row + 1), - max_rows_between_yields, - ); - for inserted_row_range in inserted_row_ranges { - let suggestions = snapshot - .suggest_autoindents(inserted_row_range.clone()) - .into_iter() - .flatten(); - for (row, suggestion) in inserted_row_range.zip(suggestions) { - if let Some(suggestion) = suggestion { - let suggested_indent = indent_sizes - .get(&suggestion.basis_row) - .copied() - .unwrap_or_else(|| { - snapshot.indent_size_for_line(suggestion.basis_row) - }) - .with_delta(suggestion.delta, request.indent_size); - indent_sizes.insert(row, suggested_indent); + for row_range in row_ranges { + if row_range.len() > 1 { + if let Some(new_indent_size) = indent_sizes.get(&row_range.start).copied() { + let old_indent_size = snapshot.indent_size_for_line(row_range.start); + if new_indent_size.kind == old_indent_size.kind { + let delta = new_indent_size.len as i64 - old_indent_size.len as i64; + if delta != 0 { + for row in row_range.skip(1) { + indent_sizes.entry(row).or_insert_with(|| { + let mut size = snapshot.indent_size_for_line(row); + if size.kind == new_indent_size.kind { + if delta > 0 { + size.len += delta as u32; + } else if delta < 0 { + size.len = + size.len.saturating_sub(-delta as u32); + } + } + size + }); + } + } + } } } - yield_now().await; } } @@ -1200,56 +1210,54 @@ impl Buffer { let edit_id = edit_operation.local_timestamp(); if let Some((before_edit, size)) = autoindent_request { - let mut inserted = Vec::new(); - let mut edited = Vec::new(); - let mut delta = 0isize; - for ((range, _), new_text) in edits + let entries = edits .into_iter() .zip(&edit_operation.as_edit().unwrap().new_text) - { - let new_text_len = new_text.len(); - let first_newline_ix = new_text.find('\n'); - let old_start = range.start.to_point(&before_edit); + .map(|((range, _), new_text)| { + let new_text_len = new_text.len(); + let first_newline_ix = new_text.find('\n'); + let old_start = range.start.to_point(&before_edit); + let new_start = (delta + range.start as isize) as usize; + delta += new_text_len as isize - (range.end as isize - range.start as isize); - let start = (delta + range.start as isize) as usize; - delta += new_text_len as isize - (range.end as isize - range.start as isize); + let mut relative_range = 0..new_text_len; + let mut first_line_is_new = false; - // When inserting multiple lines of text at the beginning of a line, - // treat all of the affected lines as newly-inserted. - if first_newline_ix.is_some() - && old_start.column < before_edit.indent_size_for_line(old_start.row).len - { - inserted - .push(self.anchor_before(start)..self.anchor_after(start + new_text_len)); - continue; - } + // When inserting multiple lines of text at the beginning of a line, + // treat the insertion as new. + if first_newline_ix.is_some() + && old_start.column < before_edit.indent_size_for_line(old_start.row).len + { + first_line_is_new = true; + } + // When inserting a newline at the end of an existing line, avoid + // auto-indenting that existing line, but treat the subsequent text as new. + else if first_newline_ix == Some(0) + && old_start.column == before_edit.line_len(old_start.row) + { + relative_range.start += 1; + first_line_is_new = true; + } + // Avoid auto-indenting subsequent lines when inserting text with trailing + // newlines + while !relative_range.is_empty() + && new_text[relative_range.clone()].ends_with('\n') + { + relative_range.end -= 1; + } - // When inserting a newline at the end of an existing line, treat the following - // line as newly-inserted. - if first_newline_ix == Some(0) - && old_start.column == before_edit.line_len(old_start.row) - { - inserted.push( - self.anchor_before(start + 1)..self.anchor_after(start + new_text_len), - ); - continue; - } - - // Otherwise, mark the start of the edit as edited, and any subsequent - // lines as newly inserted. - edited.push(before_edit.anchor_before(range.start)); - if let Some(ix) = first_newline_ix { - inserted.push( - self.anchor_before(start + ix + 1)..self.anchor_after(start + new_text_len), - ); - } - } + AutoindentRequestEntry { + first_line_is_new, + range: before_edit.anchor_before(new_start + relative_range.start) + ..self.anchor_after(new_start + relative_range.end), + } + }) + .collect(); self.autoindent_requests.push(Arc::new(AutoindentRequest { before_edit, - edited, - inserted, + entries, indent_size: size, })); } diff --git a/crates/language/src/tests.rs b/crates/language/src/tests.rs index 3f1f0f1b8c..3f721256cf 100644 --- a/crates/language/src/tests.rs +++ b/crates/language/src/tests.rs @@ -899,6 +899,53 @@ fn test_autoindent_multi_line_insertion(cx: &mut MutableAppContext) { }); } +#[gpui::test] +fn test_autoindent_preserves_relative_indentation_in_multi_line_insertion( + cx: &mut MutableAppContext, +) { + cx.add_model(|cx| { + let text = " + fn a() { + b(); + } + " + .unindent(); + let mut buffer = Buffer::new(0, text, cx).with_language(Arc::new(rust_lang()), cx); + + let pasted_text = r#" + " + c + d + e + " + "# + .unindent(); + + // insert at the beginning of a line + buffer.edit_with_autoindent( + [(Point::new(2, 0)..Point::new(2, 0), pasted_text.clone())], + IndentSize::spaces(4), + cx, + ); + assert_eq!( + buffer.text(), + r#" + fn a() { + b(); + " + c + d + e + " + } + "# + .unindent() + ); + + buffer + }); +} + #[gpui::test] fn test_autoindent_disabled(cx: &mut MutableAppContext) { cx.add_model(|cx| { From fe7ba09d528221c67adf7532f800638c0f2e2d6a Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Wed, 27 Jul 2022 09:28:56 +0200 Subject: [PATCH 16/42] Honor shift when converting keystroke to native key equivalent --- crates/gpui/src/platform/mac/platform.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/gpui/src/platform/mac/platform.rs b/crates/gpui/src/platform/mac/platform.rs index 5dc10c7b57..8895757539 100644 --- a/crates/gpui/src/platform/mac/platform.rs +++ b/crates/gpui/src/platform/mac/platform.rs @@ -184,6 +184,7 @@ impl MacForegroundPlatform { (keystroke.cmd, NSEventModifierFlags::NSCommandKeyMask), (keystroke.ctrl, NSEventModifierFlags::NSControlKeyMask), (keystroke.alt, NSEventModifierFlags::NSAlternateKeyMask), + (keystroke.shift, NSEventModifierFlags::NSShiftKeyMask), ] { if *modifier { mask |= *flag; From 5c5e7db587864e7e48a79f21c521247802b918f4 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Wed, 27 Jul 2022 09:31:47 +0200 Subject: [PATCH 17/42] Fix vim keybindings containing the `shift` modifier They regressed as part of https://github.com/zed-industries/zed/pull/1405. --- assets/keymaps/vim.json | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/assets/keymaps/vim.json b/assets/keymaps/vim.json index 2da8c26342..5262daab5f 100644 --- a/assets/keymaps/vim.json +++ b/assets/keymaps/vim.json @@ -14,30 +14,30 @@ "k": "vim::Up", "l": "vim::Right", "0": "vim::StartOfLine", - "shift-$": "vim::EndOfLine", - "shift-G": "vim::EndOfDocument", + "$": "vim::EndOfLine", + "shift-g": "vim::EndOfDocument", "w": "vim::NextWordStart", - "shift-W": [ + "shift-w": [ "vim::NextWordStart", { "ignorePunctuation": true } ], "e": "vim::NextWordEnd", - "shift-E": [ + "shift-e": [ "vim::NextWordEnd", { "ignorePunctuation": true } ], "b": "vim::PreviousWordStart", - "shift-B": [ + "shift-b": [ "vim::PreviousWordStart", { "ignorePunctuation": true } ], - "shift-%": "vim::Matching", + "%": "vim::Matching", "escape": "editor::Cancel" } }, @@ -48,12 +48,12 @@ "vim::PushOperator", "Change" ], - "shift-C": "vim::ChangeToEndOfLine", + "shift-c": "vim::ChangeToEndOfLine", "d": [ "vim::PushOperator", "Delete" ], - "shift-D": "vim::DeleteToEndOfLine", + "shift-d": "vim::DeleteToEndOfLine", "y": [ "vim::PushOperator", "Yank" @@ -62,14 +62,14 @@ "vim::SwitchMode", "Insert" ], - "shift-I": "vim::InsertFirstNonWhitespace", + "shift-i": "vim::InsertFirstNonWhitespace", "a": "vim::InsertAfter", - "shift-A": "vim::InsertEndOfLine", + "shift-a": "vim::InsertEndOfLine", "x": "vim::DeleteRight", - "shift-X": "vim::DeleteLeft", - "shift-^": "vim::FirstNonWhitespace", + "shift-x": "vim::DeleteLeft", + "^": "vim::FirstNonWhitespace", "o": "vim::InsertLineBelow", - "shift-O": "vim::InsertLineAbove", + "shift-o": "vim::InsertLineAbove", "v": [ "vim::SwitchMode", { @@ -78,7 +78,7 @@ } } ], - "shift-V": [ + "shift-v": [ "vim::SwitchMode", { "Visual": { @@ -113,7 +113,7 @@ "context": "Editor && vim_operator == c", "bindings": { "w": "vim::ChangeWord", - "shift-W": [ + "shift-w": [ "vim::ChangeWord", { "ignorePunctuation": true From d3f14fb1c21a927338d2405d4d63f76a78df69d0 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Wed, 27 Jul 2022 11:46:47 +0200 Subject: [PATCH 18/42] Dispatch application menu events if custom key equivalent wasn't found --- crates/gpui/src/platform/mac/window.rs | 80 +++++++++++++++++--------- 1 file changed, 54 insertions(+), 26 deletions(-) diff --git a/crates/gpui/src/platform/mac/window.rs b/crates/gpui/src/platform/mac/window.rs index de175812ba..86af181765 100644 --- a/crates/gpui/src/platform/mac/window.rs +++ b/crates/gpui/src/platform/mac/window.rs @@ -154,6 +154,10 @@ unsafe fn build_classes() { sel!(performKeyEquivalent:), handle_key_equivalent as extern "C" fn(&Object, Sel, id) -> BOOL, ); + decl.add_method( + sel!(keyDown:), + handle_key_down as extern "C" fn(&Object, Sel, id), + ); decl.add_method( sel!(mouseDown:), handle_view_event as extern "C" fn(&Object, Sel, id), @@ -275,7 +279,8 @@ struct WindowState { should_close_callback: Option bool>>, close_callback: Option>, input_handler: Option>, - pending_key_down_event: Option, + pending_key_down: Option<(KeyDownEvent, Option)>, + performed_key_equivalent: bool, synthetic_drag_counter: usize, executor: Rc, scene_to_render: Option, @@ -287,6 +292,11 @@ struct WindowState { previous_modifiers_changed_event: Option, } +struct InsertText { + replacement_range: Option>, + text: String, +} + impl Window { pub fn open( id: usize, @@ -359,7 +369,8 @@ impl Window { close_callback: None, activate_callback: None, input_handler: None, - pending_key_down_event: None, + pending_key_down: None, + performed_key_equivalent: false, synthetic_drag_counter: 0, executor, scene_to_render: Default::default(), @@ -689,13 +700,26 @@ extern "C" fn dealloc_view(this: &Object, _: Sel) { } extern "C" fn handle_key_equivalent(this: &Object, _: Sel, native_event: id) -> BOOL { + handle_key_event(this, native_event, true) +} + +extern "C" fn handle_key_down(this: &Object, _: Sel, native_event: id) { + handle_key_event(this, native_event, false); +} + +extern "C" fn handle_key_event(this: &Object, native_event: id, key_equivalent: bool) -> BOOL { let window_state = unsafe { get_window_state(this) }; let mut window_state_borrow = window_state.as_ref().borrow_mut(); + if key_equivalent { + window_state_borrow.performed_key_equivalent = true; + } else if window_state_borrow.performed_key_equivalent { + return NO; + } let event = unsafe { Event::from_native(native_event, Some(window_state_borrow.size().y())) }; if let Some(event) = event { - window_state_borrow.pending_key_down_event = match event { + window_state_borrow.pending_key_down = match event { Event::KeyDown(event) => { let keydown = event.keystroke.clone(); // Ignore events from held-down keys after some of the initially-pressed keys @@ -708,7 +732,7 @@ extern "C" fn handle_key_equivalent(this: &Object, _: Sel, native_event: id) -> window_state_borrow.last_fresh_keydown = Some(keydown); } - Some(event) + Some((event, None)) } _ => return NO, }; @@ -719,8 +743,9 @@ extern "C" fn handle_key_equivalent(this: &Object, _: Sel, native_event: id) -> let _: BOOL = msg_send![input_context, handleEvent: native_event]; } + let mut handled = false; let mut window_state_borrow = window_state.borrow_mut(); - if let Some(event) = window_state_borrow.pending_key_down_event.take() { + if let Some((event, insert_text)) = window_state_borrow.pending_key_down.take() { if let Some(mut callback) = window_state_borrow.event_callback.take() { drop(window_state_borrow); @@ -729,14 +754,26 @@ extern "C" fn handle_key_equivalent(this: &Object, _: Sel, native_event: id) -> .flatten() .is_some(); if !is_composing { - callback(Event::KeyDown(event)); + handled = callback(Event::KeyDown(event)); + } + + if !handled { + if let Some(insert) = insert_text { + handled = true; + with_input_handler(this, |input_handler| { + input_handler + .replace_text_in_range(insert.replacement_range, &insert.text) + }); + } } window_state.borrow_mut().event_callback = Some(callback); } + } else { + handled = true; } - YES + handled as BOOL } else { NO } @@ -837,6 +874,7 @@ extern "C" fn cancel_operation(this: &Object, _sel: Sel, _sender: id) { extern "C" fn send_event(this: &Object, _: Sel, native_event: id) { unsafe { let () = msg_send![super(this, class!(NSWindow)), sendEvent: native_event]; + get_window_state(this).borrow_mut().performed_key_equivalent = false; } } @@ -1042,7 +1080,7 @@ extern "C" fn insert_text(this: &Object, _: Sel, text: id, replacement_range: NS unsafe { let window_state = get_window_state(this); let mut window_state_borrow = window_state.borrow_mut(); - let pending_key_down_event = window_state_borrow.pending_key_down_event.take(); + let pending_key_down = window_state_borrow.pending_key_down.take(); drop(window_state_borrow); let is_attributed_string: BOOL = @@ -1062,24 +1100,17 @@ extern "C" fn insert_text(this: &Object, _: Sel, text: id, replacement_range: NS .flatten() .is_some(); - if is_composing || text.chars().count() > 1 || pending_key_down_event.is_none() { + if is_composing || text.chars().count() > 1 || pending_key_down.is_none() { with_input_handler(this, |input_handler| { input_handler.replace_text_in_range(replacement_range, text) }); } else { - let mut handled = false; - - let event_callback = window_state.borrow_mut().event_callback.take(); - if let Some(mut event_callback) = event_callback { - handled = event_callback(Event::KeyDown(pending_key_down_event.unwrap())); - window_state.borrow_mut().event_callback = Some(event_callback); - } - - if !handled { - with_input_handler(this, |input_handler| { - input_handler.replace_text_in_range(replacement_range, text) - }); - } + let mut pending_key_down = pending_key_down.unwrap(); + pending_key_down.1 = Some(InsertText { + replacement_range, + text: text.to_string(), + }); + window_state.borrow_mut().pending_key_down = Some(pending_key_down); } } } @@ -1092,10 +1123,7 @@ extern "C" fn set_marked_text( replacement_range: NSRange, ) { unsafe { - get_window_state(this) - .borrow_mut() - .pending_key_down_event - .take(); + get_window_state(this).borrow_mut().pending_key_down.take(); let is_attributed_string: BOOL = msg_send![text, isKindOfClass: [class!(NSAttributedString)]]; From 6a718dc4da4f6bd1bf5cfea95956b454a951205f Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Wed, 27 Jul 2022 12:01:44 +0200 Subject: [PATCH 19/42] Don't insert input when the `fn` key is held --- crates/gpui/src/keymap.rs | 7 +++++++ crates/gpui/src/platform/mac/event.rs | 20 +++++++++++++------- crates/gpui/src/platform/mac/window.rs | 22 ++++++++++++++-------- crates/terminal/src/mappings/keys.rs | 1 + 4 files changed, 35 insertions(+), 15 deletions(-) diff --git a/crates/gpui/src/keymap.rs b/crates/gpui/src/keymap.rs index 22cb3ba5de..d32344b0db 100644 --- a/crates/gpui/src/keymap.rs +++ b/crates/gpui/src/keymap.rs @@ -41,6 +41,7 @@ pub struct Keystroke { pub alt: bool, pub shift: bool, pub cmd: bool, + pub function: bool, pub key: String, } @@ -277,6 +278,7 @@ impl Keystroke { let mut alt = false; let mut shift = false; let mut cmd = false; + let mut function = false; let mut key = None; let mut components = source.split("-").peekable(); @@ -286,6 +288,7 @@ impl Keystroke { "alt" => alt = true, "shift" => shift = true, "cmd" => cmd = true, + "fn" => function = true, _ => { if let Some(component) = components.peek() { if component.is_empty() && source.ends_with('-') { @@ -306,6 +309,7 @@ impl Keystroke { alt, shift, cmd, + function, key: key.unwrap(), }) } @@ -464,6 +468,7 @@ mod tests { alt: false, shift: false, cmd: false, + function: false, } ); @@ -475,6 +480,7 @@ mod tests { alt: true, shift: true, cmd: false, + function: false, } ); @@ -486,6 +492,7 @@ mod tests { alt: false, shift: true, cmd: true, + function: false, } ); diff --git a/crates/gpui/src/platform/mac/event.rs b/crates/gpui/src/platform/mac/event.rs index e0e178aa8c..5aedc63b2e 100644 --- a/crates/gpui/src/platform/mac/event.rs +++ b/crates/gpui/src/platform/mac/event.rs @@ -210,19 +210,24 @@ impl Event { unsafe fn parse_keystroke(native_event: id) -> Keystroke { use cocoa::appkit::*; - let modifiers = native_event.modifierFlags(); - let ctrl = modifiers.contains(NSEventModifierFlags::NSControlKeyMask); - let alt = modifiers.contains(NSEventModifierFlags::NSAlternateKeyMask); - let mut shift = modifiers.contains(NSEventModifierFlags::NSShiftKeyMask); - let cmd = modifiers.contains(NSEventModifierFlags::NSCommandKeyMask); - let mut chars_ignoring_modifiers = CStr::from_ptr(native_event.charactersIgnoringModifiers().UTF8String() as *mut c_char) .to_str() .unwrap(); + let first_char = chars_ignoring_modifiers.chars().next().map(|ch| ch as u16); + let modifiers = native_event.modifierFlags(); + + let ctrl = modifiers.contains(NSEventModifierFlags::NSControlKeyMask); + let alt = modifiers.contains(NSEventModifierFlags::NSAlternateKeyMask); + let mut shift = modifiers.contains(NSEventModifierFlags::NSShiftKeyMask); + let cmd = modifiers.contains(NSEventModifierFlags::NSCommandKeyMask); + let function = modifiers.contains(NSEventModifierFlags::NSFunctionKeyMask) + && first_char.map_or(true, |ch| { + ch < NSUpArrowFunctionKey || ch > NSModeSwitchFunctionKey + }); #[allow(non_upper_case_globals)] - let key = match chars_ignoring_modifiers.chars().next().map(|ch| ch as u16) { + let key = match first_char { Some(SPACE_KEY) => "space", Some(BACKSPACE_KEY) => "backspace", Some(ENTER_KEY) | Some(NUMPAD_ENTER_KEY) => "enter", @@ -282,6 +287,7 @@ unsafe fn parse_keystroke(native_event: id) -> Keystroke { alt, shift, cmd, + function, key: key.into(), } } diff --git a/crates/gpui/src/platform/mac/window.rs b/crates/gpui/src/platform/mac/window.rs index 86af181765..05cc542b98 100644 --- a/crates/gpui/src/platform/mac/window.rs +++ b/crates/gpui/src/platform/mac/window.rs @@ -711,14 +711,16 @@ extern "C" fn handle_key_event(this: &Object, native_event: id, key_equivalent: let window_state = unsafe { get_window_state(this) }; let mut window_state_borrow = window_state.as_ref().borrow_mut(); - if key_equivalent { - window_state_borrow.performed_key_equivalent = true; - } else if window_state_borrow.performed_key_equivalent { - return NO; - } let event = unsafe { Event::from_native(native_event, Some(window_state_borrow.size().y())) }; if let Some(event) = event { + if key_equivalent { + window_state_borrow.performed_key_equivalent = true; + } else if window_state_borrow.performed_key_equivalent { + return NO; + } + + let function_is_held; window_state_borrow.pending_key_down = match event { Event::KeyDown(event) => { let keydown = event.keystroke.clone(); @@ -732,15 +734,18 @@ extern "C" fn handle_key_event(this: &Object, native_event: id, key_equivalent: window_state_borrow.last_fresh_keydown = Some(keydown); } + function_is_held = event.keystroke.function; Some((event, None)) } _ => return NO, }; drop(window_state_borrow); - unsafe { - let input_context: id = msg_send![this, inputContext]; - let _: BOOL = msg_send![input_context, handleEvent: native_event]; + if !function_is_held { + unsafe { + let input_context: id = msg_send![this, inputContext]; + let _: BOOL = msg_send![input_context, handleEvent: native_event]; + } } let mut handled = false; @@ -856,6 +861,7 @@ extern "C" fn cancel_operation(this: &Object, _sel: Sel, _sender: id) { ctrl: false, alt: false, shift: false, + function: false, key: ".".into(), }; let event = Event::KeyDown(KeyDownEvent { diff --git a/crates/terminal/src/mappings/keys.rs b/crates/terminal/src/mappings/keys.rs index f88bfa927a..3ed46de459 100644 --- a/crates/terminal/src/mappings/keys.rs +++ b/crates/terminal/src/mappings/keys.rs @@ -311,6 +311,7 @@ mod test { alt: false, shift: false, cmd: false, + function: false, key: "🖖🏻".to_string(), //2 char string }; assert_eq!(to_esc_str(&ks, &TermMode::NONE), None); From f0814c7784cb32bfc62a42149ff1d0399c89f88a Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Wed, 27 Jul 2022 12:19:01 +0200 Subject: [PATCH 20/42] Fix vim tests using old key bindings --- crates/vim/src/normal.rs | 22 +++++++++++----------- crates/vim/src/normal/change.rs | 10 +++++----- crates/vim/src/normal/delete.rs | 10 +++++----- crates/vim/src/visual.rs | 8 ++++---- 4 files changed, 25 insertions(+), 25 deletions(-) diff --git a/crates/vim/src/normal.rs b/crates/vim/src/normal.rs index 65909702ba..9ece9605b7 100644 --- a/crates/vim/src/normal.rs +++ b/crates/vim/src/normal.rs @@ -427,7 +427,7 @@ mod test { #[gpui::test] async fn test_jump_to_line_boundaries(cx: &mut gpui::TestAppContext) { let cx = VimTestContext::new(cx, true).await; - let mut cx = cx.binding(["shift-$"]); + let mut cx = cx.binding(["$"]); cx.assert("T|est test", "Test tes|t"); cx.assert("Test tes|t", "Test tes|t"); cx.assert( @@ -471,7 +471,7 @@ mod test { #[gpui::test] async fn test_jump_to_end(cx: &mut gpui::TestAppContext) { let cx = VimTestContext::new(cx, true).await; - let mut cx = cx.binding(["shift-G"]); + let mut cx = cx.binding(["shift-g"]); cx.assert( indoc! {" @@ -561,7 +561,7 @@ mod test { ); for cursor_offset in cursor_offsets { - cx.simulate_keystroke("shift-W"); + cx.simulate_keystroke("shift-w"); cx.assert_editor_selections(vec![Selection::from_offset(cursor_offset)]); } } @@ -607,7 +607,7 @@ mod test { Mode::Normal, ); for cursor_offset in cursor_offsets { - cx.simulate_keystroke("shift-E"); + cx.simulate_keystroke("shift-e"); cx.assert_editor_selections(vec![Selection::from_offset(cursor_offset)]); } } @@ -653,7 +653,7 @@ mod test { Mode::Normal, ); for cursor_offset in cursor_offsets.into_iter().rev() { - cx.simulate_keystroke("shift-B"); + cx.simulate_keystroke("shift-b"); cx.assert_editor_selections(vec![Selection::from_offset(cursor_offset)]); } } @@ -740,7 +740,7 @@ mod test { #[gpui::test] async fn test_insert_end_of_line(cx: &mut gpui::TestAppContext) { let cx = VimTestContext::new(cx, true).await; - let mut cx = cx.binding(["shift-A"]).mode_after(Mode::Insert); + let mut cx = cx.binding(["shift-a"]).mode_after(Mode::Insert); cx.assert("The q|uick", "The quick|"); cx.assert("The q|uick ", "The quick |"); cx.assert("|", "|"); @@ -765,7 +765,7 @@ mod test { #[gpui::test] async fn test_jump_to_first_non_whitespace(cx: &mut gpui::TestAppContext) { let cx = VimTestContext::new(cx, true).await; - let mut cx = cx.binding(["shift-^"]); + let mut cx = cx.binding(["^"]); cx.assert("The q|uick", "|The quick"); cx.assert(" The q|uick", " |The quick"); cx.assert("|", "|"); @@ -792,7 +792,7 @@ mod test { #[gpui::test] async fn test_insert_first_non_whitespace(cx: &mut gpui::TestAppContext) { let cx = VimTestContext::new(cx, true).await; - let mut cx = cx.binding(["shift-I"]).mode_after(Mode::Insert); + let mut cx = cx.binding(["shift-i"]).mode_after(Mode::Insert); cx.assert("The q|uick", "|The quick"); cx.assert(" The q|uick", " |The quick"); cx.assert("|", "|"); @@ -817,7 +817,7 @@ mod test { #[gpui::test] async fn test_delete_to_end_of_line(cx: &mut gpui::TestAppContext) { let cx = VimTestContext::new(cx, true).await; - let mut cx = cx.binding(["shift-D"]); + let mut cx = cx.binding(["shift-d"]); cx.assert( indoc! {" The q|uick @@ -858,7 +858,7 @@ mod test { #[gpui::test] async fn test_delete_left(cx: &mut gpui::TestAppContext) { let cx = VimTestContext::new(cx, true).await; - let mut cx = cx.binding(["shift-X"]); + let mut cx = cx.binding(["shift-x"]); cx.assert("Te|st", "T|st"); cx.assert("T|est", "|est"); cx.assert("|Test", "|Test"); @@ -956,7 +956,7 @@ mod test { #[gpui::test] async fn test_insert_line_above(cx: &mut gpui::TestAppContext) { let cx = VimTestContext::new(cx, true).await; - let mut cx = cx.binding(["shift-O"]).mode_after(Mode::Insert); + let mut cx = cx.binding(["shift-o"]).mode_after(Mode::Insert); cx.assert( "|", diff --git a/crates/vim/src/normal/change.rs b/crates/vim/src/normal/change.rs index 55445c930e..03f213b584 100644 --- a/crates/vim/src/normal/change.rs +++ b/crates/vim/src/normal/change.rs @@ -139,7 +139,7 @@ mod test { test"}, ); - let mut cx = cx.binding(["c", "shift-W"]); + let mut cx = cx.binding(["c", "shift-w"]); cx.assert("Test te|st-test test", "Test te| test"); } @@ -174,7 +174,7 @@ mod test { test"}, ); - let mut cx = cx.binding(["c", "shift-E"]); + let mut cx = cx.binding(["c", "shift-e"]); cx.assert("Test te|st-test test", "Test te| test"); } @@ -204,14 +204,14 @@ mod test { test"}, ); - let mut cx = cx.binding(["c", "shift-B"]); + let mut cx = cx.binding(["c", "shift-b"]); cx.assert("Test test-test |test", "Test |test"); } #[gpui::test] async fn test_change_end_of_line(cx: &mut gpui::TestAppContext) { let cx = VimTestContext::new(cx, true).await; - let mut cx = cx.binding(["c", "shift-$"]).mode_after(Mode::Insert); + let mut cx = cx.binding(["c", "$"]).mode_after(Mode::Insert); cx.assert( indoc! {" The q|uick @@ -347,7 +347,7 @@ mod test { #[gpui::test] async fn test_change_end_of_document(cx: &mut gpui::TestAppContext) { let cx = VimTestContext::new(cx, true).await; - let mut cx = cx.binding(["c", "shift-G"]).mode_after(Mode::Insert); + let mut cx = cx.binding(["c", "shift-g"]).mode_after(Mode::Insert); cx.assert( indoc! {" The quick diff --git a/crates/vim/src/normal/delete.rs b/crates/vim/src/normal/delete.rs index c5c823c79e..ca2c27cf70 100644 --- a/crates/vim/src/normal/delete.rs +++ b/crates/vim/src/normal/delete.rs @@ -109,7 +109,7 @@ mod test { test"}, ); - let mut cx = cx.binding(["d", "shift-W"]); + let mut cx = cx.binding(["d", "shift-w"]); cx.assert("Test te|st-test test", "Test te|test"); } @@ -144,7 +144,7 @@ mod test { test"}, ); - let mut cx = cx.binding(["d", "shift-E"]); + let mut cx = cx.binding(["d", "shift-e"]); cx.assert("Test te|st-test test", "Test te| test"); } @@ -176,14 +176,14 @@ mod test { test"}, ); - let mut cx = cx.binding(["d", "shift-B"]); + let mut cx = cx.binding(["d", "shift-b"]); cx.assert("Test test-test |test", "Test |test"); } #[gpui::test] async fn test_delete_end_of_line(cx: &mut gpui::TestAppContext) { let cx = VimTestContext::new(cx, true).await; - let mut cx = cx.binding(["d", "shift-$"]); + let mut cx = cx.binding(["d", "$"]); cx.assert( indoc! {" The q|uick @@ -304,7 +304,7 @@ mod test { #[gpui::test] async fn test_delete_end_of_document(cx: &mut gpui::TestAppContext) { let cx = VimTestContext::new(cx, true).await; - let mut cx = cx.binding(["d", "shift-G"]); + let mut cx = cx.binding(["d", "shift-g"]); cx.assert( indoc! {" The quick diff --git a/crates/vim/src/visual.rs b/crates/vim/src/visual.rs index fedb999cad..02fcdabd09 100644 --- a/crates/vim/src/visual.rs +++ b/crates/vim/src/visual.rs @@ -422,7 +422,7 @@ mod test { #[gpui::test] async fn test_visual_line_delete(cx: &mut gpui::TestAppContext) { let cx = VimTestContext::new(cx, true).await; - let mut cx = cx.binding(["shift-V", "x"]); + let mut cx = cx.binding(["shift-v", "x"]); cx.assert( indoc! {" The qu|ick brown @@ -457,7 +457,7 @@ mod test { The quick brown fox ju|mps over"}, ); - let mut cx = cx.binding(["shift-V", "j", "x"]); + let mut cx = cx.binding(["shift-v", "j", "x"]); cx.assert( indoc! {" The qu|ick brown @@ -558,7 +558,7 @@ mod test { #[gpui::test] async fn test_visual_line_change(cx: &mut gpui::TestAppContext) { let cx = VimTestContext::new(cx, true).await; - let mut cx = cx.binding(["shift-V", "c"]).mode_after(Mode::Insert); + let mut cx = cx.binding(["shift-v", "c"]).mode_after(Mode::Insert); cx.assert( indoc! {" The qu|ick brown @@ -597,7 +597,7 @@ mod test { fox jumps over |"}, ); - let mut cx = cx.binding(["shift-V", "j", "c"]).mode_after(Mode::Insert); + let mut cx = cx.binding(["shift-v", "j", "c"]).mode_after(Mode::Insert); cx.assert( indoc! {" The qu|ick brown From 92de5ab000e9d457950e8a9b35720f6b96bd5a9f Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Wed, 27 Jul 2022 13:39:29 +0200 Subject: [PATCH 21/42] v0.49.1 --- Cargo.lock | 2 +- crates/zed/Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index ed860b9ecf..e4d454c0de 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6993,7 +6993,7 @@ dependencies = [ [[package]] name = "zed" -version = "0.49.0" +version = "0.49.1" dependencies = [ "activity_indicator", "anyhow", diff --git a/crates/zed/Cargo.toml b/crates/zed/Cargo.toml index d56305cee9..be20be2fd7 100644 --- a/crates/zed/Cargo.toml +++ b/crates/zed/Cargo.toml @@ -3,7 +3,7 @@ authors = ["Nathan Sobo "] description = "The fast, collaborative code editor." edition = "2021" name = "zed" -version = "0.49.0" +version = "0.49.1" [lib] name = "zed" From 20d45221c96050b858f5ff49feac56347efd6889 Mon Sep 17 00:00:00 2001 From: K Simmons Date: Wed, 27 Jul 2022 11:00:54 -0700 Subject: [PATCH 22/42] Move terminal modal keymap context to the connected view --- crates/terminal/src/connected_view.rs | 8 ++++++++ crates/terminal/src/terminal.rs | 8 -------- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/crates/terminal/src/connected_view.rs b/crates/terminal/src/connected_view.rs index 187b018249..7693b39d81 100644 --- a/crates/terminal/src/connected_view.rs +++ b/crates/terminal/src/connected_view.rs @@ -173,4 +173,12 @@ impl View for ConnectedView { self.terminal .update(cx, |terminal, _| terminal.write_to_pty(text.into())); } + + fn keymap_context(&self, _: &gpui::AppContext) -> gpui::keymap::Context { + let mut context = Self::default_keymap_context(); + if self.modal { + context.set.insert("ModalTerminal".into()); + } + context + } } diff --git a/crates/terminal/src/terminal.rs b/crates/terminal/src/terminal.rs index 27339d2d00..028a8f23b0 100644 --- a/crates/terminal/src/terminal.rs +++ b/crates/terminal/src/terminal.rs @@ -171,14 +171,6 @@ impl View for TerminalView { cx.focus(view.content.handle()); }); } - - fn keymap_context(&self, _: &gpui::AppContext) -> gpui::keymap::Context { - let mut context = Self::default_keymap_context(); - if self.modal { - context.set.insert("ModalTerminal".into()); - } - context - } } impl View for ErrorView { From b20aefc8421a12cba07c8f86a27249f634ce18d9 Mon Sep 17 00:00:00 2001 From: Isaac Clayton Date: Fri, 22 Jul 2022 09:43:15 +0200 Subject: [PATCH 23/42] First draft for opening Zed.log --- crates/zed/src/zed.rs | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index 867913fc7b..709dc31f64 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -52,6 +52,7 @@ actions!( Quit, DebugElements, OpenSettings, + OpenLog, OpenKeymap, OpenDefaultSettings, OpenDefaultKeymap, @@ -65,9 +66,10 @@ actions!( const MIN_FONT_SIZE: f32 = 6.0; lazy_static! { - pub static ref ROOT_PATH: PathBuf = dirs::home_dir() - .expect("failed to determine home directory") - .join(".zed"); + pub static ref HOME_PATH: PathBuf = + dirs::home_dir().expect("failed to determine home directory"); + pub static ref LOG_PATH: PathBuf = HOME_PATH.join("Library/Logs/Zed/Zed.log"); + pub static ref ROOT_PATH: PathBuf = HOME_PATH.join(".zed"); pub static ref SETTINGS_PATH: PathBuf = ROOT_PATH.join("settings.json"); pub static ref KEYMAP_PATH: PathBuf = ROOT_PATH.join("keymap.json"); } @@ -120,6 +122,12 @@ pub fn init(app_state: &Arc, cx: &mut gpui::MutableAppContext) { }); } }); + cx.add_action({ + let app_state = app_state.clone(); + move |_: &mut Workspace, _: &OpenLog, cx: &mut ViewContext| { + open_config_file(&LOG_PATH, app_state.clone(), cx, || Default::default()); + } + }); cx.add_action({ let app_state = app_state.clone(); move |_: &mut Workspace, _: &OpenKeymap, cx: &mut ViewContext| { From de35c3f99d709c5f763af174abc12667d43a4f59 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Thu, 28 Jul 2022 10:02:38 +0200 Subject: [PATCH 24/42] Prevent log file from becoming larger than 1MB --- crates/zed/src/main.rs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/crates/zed/src/main.rs b/crates/zed/src/main.rs index 337e3afc2d..2ca1403b6c 100644 --- a/crates/zed/src/main.rs +++ b/crates/zed/src/main.rs @@ -57,6 +57,7 @@ fn main() { fs::create_dir_all(&logs_dir_path).expect("could not create logs path"); init_logger(&logs_dir_path); + log::info!("========== starting zed =========="); let mut app = gpui::App::new(Assets).unwrap(); let app_version = ZED_APP_VERSION .or_else(|| app.platform().app_version().ok()) @@ -210,6 +211,13 @@ fn init_logger(logs_dir_path: &Path) { } else { let level = LevelFilter::Info; let log_file_path = logs_dir_path.join("Zed.log"); + + // Prevent log file from becoming too large. + const MAX_LOG_BYTES: u64 = 1 * 1024 * 1024; + if fs::metadata(&log_file_path).map_or(false, |metadata| metadata.len() > MAX_LOG_BYTES) { + let _ = fs::rename(&log_file_path, logs_dir_path.join("Zed.log.old")); + } + let log_file = OpenOptions::new() .create(true) .append(true) From df6e733e687bf6e7bb26adf7252e82735ca2801e Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Thu, 28 Jul 2022 10:25:40 +0200 Subject: [PATCH 25/42] Open both old and new log files, but just retain the last 1000 lines --- crates/workspace/src/workspace.rs | 4 +-- crates/zed/src/zed.rs | 60 +++++++++++++++++++++++++++++-- 2 files changed, 60 insertions(+), 4 deletions(-) diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index e8ff1932d4..ac3d1c3696 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -949,11 +949,11 @@ impl Workspace { &mut self, cx: &mut ViewContext, app_state: Arc, - mut callback: F, + callback: F, ) -> T where T: 'static, - F: FnMut(&mut Workspace, &mut ViewContext) -> T, + F: FnOnce(&mut Workspace, &mut ViewContext) -> T, { if self.project.read(cx).is_local() { callback(self, cx) diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index 709dc31f64..f7ccefec2a 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -9,6 +9,7 @@ use anyhow::{anyhow, Context, Result}; use assets::Assets; use breadcrumbs::Breadcrumbs; pub use client; +use collections::VecDeque; pub use contacts_panel; use contacts_panel::ContactsPanel; pub use editor; @@ -69,6 +70,7 @@ lazy_static! { pub static ref HOME_PATH: PathBuf = dirs::home_dir().expect("failed to determine home directory"); pub static ref LOG_PATH: PathBuf = HOME_PATH.join("Library/Logs/Zed/Zed.log"); + pub static ref OLD_LOG_PATH: PathBuf = HOME_PATH.join("Library/Logs/Zed/Zed.log.old"); pub static ref ROOT_PATH: PathBuf = HOME_PATH.join(".zed"); pub static ref SETTINGS_PATH: PathBuf = ROOT_PATH.join("settings.json"); pub static ref KEYMAP_PATH: PathBuf = ROOT_PATH.join("keymap.json"); @@ -124,8 +126,8 @@ pub fn init(app_state: &Arc, cx: &mut gpui::MutableAppContext) { }); cx.add_action({ let app_state = app_state.clone(); - move |_: &mut Workspace, _: &OpenLog, cx: &mut ViewContext| { - open_config_file(&LOG_PATH, app_state.clone(), cx, || Default::default()); + move |workspace: &mut Workspace, _: &OpenLog, cx: &mut ViewContext| { + open_log_file(workspace, app_state.clone(), cx); } }); cx.add_action({ @@ -415,6 +417,60 @@ fn open_config_file( .detach_and_log_err(cx) } +fn open_log_file( + workspace: &mut Workspace, + app_state: Arc, + cx: &mut ViewContext, +) { + const MAX_LINES: usize = 1000; + + workspace.with_local_workspace(cx, app_state.clone(), |_, cx| { + cx.spawn_weak(|workspace, mut cx| async move { + let (old_log, new_log) = futures::join!( + app_state.fs.load(&OLD_LOG_PATH), + app_state.fs.load(&LOG_PATH) + ); + + if let Some(workspace) = workspace.upgrade(&cx) { + let mut lines = VecDeque::with_capacity(MAX_LINES); + for line in old_log + .iter() + .flat_map(|log| log.lines()) + .chain(new_log.iter().flat_map(|log| log.lines())) + { + if lines.len() == MAX_LINES { + lines.pop_front(); + } + lines.push_back(line); + } + let log = lines + .into_iter() + .flat_map(|line| [line, "\n"]) + .collect::(); + + workspace.update(&mut cx, |workspace, cx| { + let project = workspace.project().clone(); + let buffer = project + .update(cx, |project, cx| project.create_buffer("", None, cx)) + .expect("creating buffers on a local workspace always succeeds"); + buffer.update(cx, |buffer, cx| buffer.edit([(0..0, log)], cx)); + + let buffer = cx.add_model(|cx| { + MultiBuffer::singleton(buffer, cx).with_title("Log".into()) + }); + workspace.add_item( + Box::new( + cx.add_view(|cx| Editor::for_multibuffer(buffer, Some(project), cx)), + ), + cx, + ); + }); + } + }) + .detach(); + }); +} + fn open_bundled_config_file( workspace: &mut Workspace, app_state: Arc, From b1b252ee45fedb2e6b7a7cff051282163d5ce997 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Thu, 28 Jul 2022 11:51:03 -0700 Subject: [PATCH 26/42] Fix error in autoindent range calculation --- crates/language/src/buffer.rs | 36 +++++++++++++++++------------------ 1 file changed, 17 insertions(+), 19 deletions(-) diff --git a/crates/language/src/buffer.rs b/crates/language/src/buffer.rs index ef9b266315..8eef0a5274 100644 --- a/crates/language/src/buffer.rs +++ b/crates/language/src/buffer.rs @@ -237,7 +237,12 @@ struct AutoindentRequest { #[derive(Clone)] struct AutoindentRequestEntry { + /// A range of the buffer whose indentation should be adjusted. range: Range, + /// Whether or not these lines should be considered brand new, for the + /// purpose of auto-indent. When text is not new, its indentation will + /// only be adjusted if the suggested indentation level has *changed* + /// since the edit was made. first_line_is_new: bool, } @@ -1216,41 +1221,34 @@ impl Buffer { .zip(&edit_operation.as_edit().unwrap().new_text) .map(|((range, _), new_text)| { let new_text_len = new_text.len(); - let first_newline_ix = new_text.find('\n'); let old_start = range.start.to_point(&before_edit); let new_start = (delta + range.start as isize) as usize; delta += new_text_len as isize - (range.end as isize - range.start as isize); - let mut relative_range = 0..new_text_len; + let mut range_of_insertion_to_indent = 0..new_text_len; let mut first_line_is_new = false; - // When inserting multiple lines of text at the beginning of a line, + // When inserting an entire line at the beginning of an existing line, // treat the insertion as new. - if first_newline_ix.is_some() - && old_start.column < before_edit.indent_size_for_line(old_start.row).len + if new_text.contains('\n') + && old_start.column <= before_edit.indent_size_for_line(old_start.row).len { first_line_is_new = true; } - // When inserting a newline at the end of an existing line, avoid - // auto-indenting that existing line, but treat the subsequent text as new. - else if first_newline_ix == Some(0) - && old_start.column == before_edit.line_len(old_start.row) - { - relative_range.start += 1; + + // Avoid auto-indenting lines before and after the insertion. + if new_text[range_of_insertion_to_indent.clone()].starts_with('\n') { + range_of_insertion_to_indent.start += 1; first_line_is_new = true; } - // Avoid auto-indenting subsequent lines when inserting text with trailing - // newlines - while !relative_range.is_empty() - && new_text[relative_range.clone()].ends_with('\n') - { - relative_range.end -= 1; + if new_text[range_of_insertion_to_indent.clone()].ends_with('\n') { + range_of_insertion_to_indent.end -= 1; } AutoindentRequestEntry { first_line_is_new, - range: before_edit.anchor_before(new_start + relative_range.start) - ..self.anchor_after(new_start + relative_range.end), + range: self.anchor_before(new_start + range_of_insertion_to_indent.start) + ..self.anchor_after(new_start + range_of_insertion_to_indent.end), } }) .collect(); From cdf6ae25bbdb21ea1cb1906faa72631bd2f3a183 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Thu, 28 Jul 2022 13:00:05 -0700 Subject: [PATCH 27/42] Remove `indent_size` parameter from `Buffer::edit_with_autoindent` Instead, compute the indent size by reading the settings inside that method. Co-authored-by: Mikayla Maki --- Cargo.lock | 1 + crates/editor/src/multi_buffer.rs | 23 ++----- crates/language/Cargo.toml | 1 + crates/language/src/buffer.rs | 9 ++- crates/language/src/tests.rs | 98 +++++++++++------------------- crates/zed/src/languages/c.rs | 15 +++-- crates/zed/src/languages/python.rs | 13 ++-- crates/zed/src/languages/rust.rs | 21 ++++--- 8 files changed, 77 insertions(+), 104 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index ed860b9ecf..6a6ba400c7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2761,6 +2761,7 @@ dependencies = [ "rpc", "serde", "serde_json", + "settings", "similar", "smallvec", "smol", diff --git a/crates/editor/src/multi_buffer.rs b/crates/editor/src/multi_buffer.rs index 00ef7b11a0..fa48cabf05 100644 --- a/crates/editor/src/multi_buffer.rs +++ b/crates/editor/src/multi_buffer.rs @@ -11,7 +11,6 @@ use language::{ IndentSize, Language, OffsetRangeExt, Outline, OutlineItem, Selection, ToOffset as _, ToOffsetUtf16 as _, ToPoint as _, ToPointUtf16 as _, TransactionId, }; -use settings::Settings; use smallvec::SmallVec; use std::{ borrow::Cow, @@ -347,14 +346,7 @@ impl MultiBuffer { if let Some(buffer) = self.as_singleton() { return buffer.update(cx, |buffer, cx| { if autoindent { - let language_name = buffer.language().map(|language| language.name()); - let settings = cx.global::(); - let indent_size = if settings.hard_tabs(language_name.as_deref()) { - IndentSize::tab() - } else { - IndentSize::spaces(settings.tab_size(language_name.as_deref()).get()) - }; - buffer.edit_with_autoindent(edits, indent_size, cx); + buffer.edit_with_autoindent(edits, cx); } else { buffer.edit(edits, cx); } @@ -471,18 +463,10 @@ impl MultiBuffer { )); } } - let language_name = buffer.language().map(|l| l.name()); if autoindent { - let settings = cx.global::(); - let indent_size = if settings.hard_tabs(language_name.as_deref()) { - IndentSize::tab() - } else { - IndentSize::spaces(settings.tab_size(language_name.as_deref()).get()) - }; - - buffer.edit_with_autoindent(deletions, indent_size, cx); - buffer.edit_with_autoindent(insertions, indent_size, cx); + buffer.edit_with_autoindent(deletions, cx); + buffer.edit_with_autoindent(insertions, cx); } else { buffer.edit(deletions, cx); buffer.edit(insertions, cx); @@ -3220,6 +3204,7 @@ mod tests { use gpui::MutableAppContext; use language::{Buffer, Rope}; use rand::prelude::*; + use settings::Settings; use std::{env, rc::Rc}; use text::{Point, RandomCharIter}; use util::test::sample_text; diff --git a/crates/language/Cargo.toml b/crates/language/Cargo.toml index c70ad6b731..0dc8165bfb 100644 --- a/crates/language/Cargo.toml +++ b/crates/language/Cargo.toml @@ -27,6 +27,7 @@ fuzzy = { path = "../fuzzy" } gpui = { path = "../gpui" } lsp = { path = "../lsp" } rpc = { path = "../rpc" } +settings = { path = "../settings" } sum_tree = { path = "../sum_tree" } text = { path = "../text" } theme = { path = "../theme" } diff --git a/crates/language/src/buffer.rs b/crates/language/src/buffer.rs index 8eef0a5274..4b0d2566c3 100644 --- a/crates/language/src/buffer.rs +++ b/crates/language/src/buffer.rs @@ -14,6 +14,7 @@ use futures::FutureExt as _; use gpui::{fonts::HighlightStyle, AppContext, Entity, ModelContext, MutableAppContext, Task}; use lazy_static::lazy_static; use parking_lot::Mutex; +use settings::Settings; use similar::{ChangeTag, TextDiff}; use smol::future::yield_now; use std::{ @@ -1156,7 +1157,6 @@ impl Buffer { pub fn edit_with_autoindent( &mut self, edits_iter: I, - indent_size: IndentSize, cx: &mut ModelContext, ) -> Option where @@ -1164,6 +1164,13 @@ impl Buffer { S: ToOffset, T: Into>, { + let language_name = self.language().map(|language| language.name()); + let settings = cx.global::(); + let indent_size = if settings.hard_tabs(language_name.as_deref()) { + IndentSize::tab() + } else { + IndentSize::spaces(settings.tab_size(language_name.as_deref()).get()) + }; self.edit_internal(edits_iter, Some(indent_size), cx) } diff --git a/crates/language/src/tests.rs b/crates/language/src/tests.rs index 3f721256cf..0a6fb2ce02 100644 --- a/crates/language/src/tests.rs +++ b/crates/language/src/tests.rs @@ -3,6 +3,7 @@ use clock::ReplicaId; use collections::BTreeMap; use gpui::{ModelHandle, MutableAppContext}; use rand::prelude::*; +use settings::Settings; use std::{ cell::RefCell, env, @@ -24,6 +25,7 @@ fn init_logger() { #[gpui::test] fn test_line_endings(cx: &mut gpui::MutableAppContext) { + cx.set_global(Settings::test(cx)); cx.add_model(|cx| { let mut buffer = Buffer::new(0, "one\r\ntwo\rthree", cx).with_language(Arc::new(rust_lang()), cx); @@ -31,11 +33,7 @@ fn test_line_endings(cx: &mut gpui::MutableAppContext) { assert_eq!(buffer.line_ending(), LineEnding::Windows); buffer.check_invariants(); - buffer.edit_with_autoindent( - [(buffer.len()..buffer.len(), "\r\nfour")], - IndentSize::spaces(2), - cx, - ); + buffer.edit_with_autoindent([(buffer.len()..buffer.len(), "\r\nfour")], cx); buffer.edit([(0..0, "zero\r\n")], cx); assert_eq!(buffer.text(), "zero\none\ntwo\nthree\nfour"); assert_eq!(buffer.line_ending(), LineEnding::Windows); @@ -545,6 +543,7 @@ async fn test_symbols_containing(cx: &mut gpui::TestAppContext) { #[gpui::test] fn test_enclosing_bracket_ranges(cx: &mut MutableAppContext) { + cx.set_global(Settings::test(cx)); let buffer = cx.add_model(|cx| { let text = " mod x { @@ -620,36 +619,27 @@ fn test_range_for_syntax_ancestor(cx: &mut MutableAppContext) { #[gpui::test] fn test_autoindent_with_soft_tabs(cx: &mut MutableAppContext) { + let settings = Settings::test(cx); + cx.set_global(settings); + cx.add_model(|cx| { let text = "fn a() {}"; let mut buffer = Buffer::new(0, text, cx).with_language(Arc::new(rust_lang()), cx); - buffer.edit_with_autoindent([(8..8, "\n\n")], IndentSize::spaces(4), cx); + buffer.edit_with_autoindent([(8..8, "\n\n")], cx); assert_eq!(buffer.text(), "fn a() {\n \n}"); - buffer.edit_with_autoindent( - [(Point::new(1, 4)..Point::new(1, 4), "b()\n")], - IndentSize::spaces(4), - cx, - ); + buffer.edit_with_autoindent([(Point::new(1, 4)..Point::new(1, 4), "b()\n")], cx); assert_eq!(buffer.text(), "fn a() {\n b()\n \n}"); // Create a field expression on a new line, causing that line // to be indented. - buffer.edit_with_autoindent( - [(Point::new(2, 4)..Point::new(2, 4), ".c")], - IndentSize::spaces(4), - cx, - ); + buffer.edit_with_autoindent([(Point::new(2, 4)..Point::new(2, 4), ".c")], cx); assert_eq!(buffer.text(), "fn a() {\n b()\n .c\n}"); // Remove the dot so that the line is no longer a field expression, // causing the line to be outdented. - buffer.edit_with_autoindent( - [(Point::new(2, 8)..Point::new(2, 9), "")], - IndentSize::spaces(4), - cx, - ); + buffer.edit_with_autoindent([(Point::new(2, 8)..Point::new(2, 9), "")], cx); assert_eq!(buffer.text(), "fn a() {\n b()\n c\n}"); buffer @@ -658,36 +648,28 @@ fn test_autoindent_with_soft_tabs(cx: &mut MutableAppContext) { #[gpui::test] fn test_autoindent_with_hard_tabs(cx: &mut MutableAppContext) { + let mut settings = Settings::test(cx); + settings.editor_overrides.hard_tabs = Some(true); + cx.set_global(settings); + cx.add_model(|cx| { let text = "fn a() {}"; let mut buffer = Buffer::new(0, text, cx).with_language(Arc::new(rust_lang()), cx); - buffer.edit_with_autoindent([(8..8, "\n\n")], IndentSize::tab(), cx); + buffer.edit_with_autoindent([(8..8, "\n\n")], cx); assert_eq!(buffer.text(), "fn a() {\n\t\n}"); - buffer.edit_with_autoindent( - [(Point::new(1, 1)..Point::new(1, 1), "b()\n")], - IndentSize::tab(), - cx, - ); + buffer.edit_with_autoindent([(Point::new(1, 1)..Point::new(1, 1), "b()\n")], cx); assert_eq!(buffer.text(), "fn a() {\n\tb()\n\t\n}"); // Create a field expression on a new line, causing that line // to be indented. - buffer.edit_with_autoindent( - [(Point::new(2, 1)..Point::new(2, 1), ".c")], - IndentSize::tab(), - cx, - ); + buffer.edit_with_autoindent([(Point::new(2, 1)..Point::new(2, 1), ".c")], cx); assert_eq!(buffer.text(), "fn a() {\n\tb()\n\t\t.c\n}"); // Remove the dot so that the line is no longer a field expression, // causing the line to be outdented. - buffer.edit_with_autoindent( - [(Point::new(2, 2)..Point::new(2, 3), "")], - IndentSize::tab(), - cx, - ); + buffer.edit_with_autoindent([(Point::new(2, 2)..Point::new(2, 3), "")], cx); assert_eq!(buffer.text(), "fn a() {\n\tb()\n\tc\n}"); buffer @@ -696,6 +678,9 @@ fn test_autoindent_with_hard_tabs(cx: &mut MutableAppContext) { #[gpui::test] fn test_autoindent_does_not_adjust_lines_with_unchanged_suggestion(cx: &mut MutableAppContext) { + let settings = Settings::test(cx); + cx.set_global(settings); + cx.add_model(|cx| { let text = " fn a() { @@ -714,7 +699,6 @@ fn test_autoindent_does_not_adjust_lines_with_unchanged_suggestion(cx: &mut Muta (empty(Point::new(1, 1)), "()"), (empty(Point::new(2, 1)), "()"), ], - IndentSize::spaces(4), cx, ); assert_eq!( @@ -735,7 +719,6 @@ fn test_autoindent_does_not_adjust_lines_with_unchanged_suggestion(cx: &mut Muta (empty(Point::new(1, 1)), "\n.f\n.g"), (empty(Point::new(2, 1)), "\n.f\n.g"), ], - IndentSize::spaces(4), cx, ); assert_eq!( @@ -768,11 +751,7 @@ fn test_autoindent_does_not_adjust_lines_with_unchanged_suggestion(cx: &mut Muta let mut buffer = Buffer::new(0, text, cx).with_language(Arc::new(rust_lang()), cx); // Delete a closing curly brace changes the suggested indent for the line. - buffer.edit_with_autoindent( - [(Point::new(3, 4)..Point::new(3, 5), "")], - IndentSize::spaces(4), - cx, - ); + buffer.edit_with_autoindent([(Point::new(3, 4)..Point::new(3, 5), "")], cx); assert_eq!( buffer.text(), " @@ -788,11 +767,7 @@ fn test_autoindent_does_not_adjust_lines_with_unchanged_suggestion(cx: &mut Muta ); // Manually editing the leading whitespace - buffer.edit_with_autoindent( - [(Point::new(3, 0)..Point::new(3, 12), "")], - IndentSize::spaces(4), - cx, - ); + buffer.edit_with_autoindent([(Point::new(3, 0)..Point::new(3, 12), "")], cx); assert_eq!( buffer.text(), " @@ -811,6 +786,7 @@ fn test_autoindent_does_not_adjust_lines_with_unchanged_suggestion(cx: &mut Muta #[gpui::test] fn test_autoindent_adjusts_lines_when_only_text_changes(cx: &mut MutableAppContext) { + cx.set_global(Settings::test(cx)); cx.add_model(|cx| { let text = " fn a() {} @@ -819,7 +795,7 @@ fn test_autoindent_adjusts_lines_when_only_text_changes(cx: &mut MutableAppConte let mut buffer = Buffer::new(0, text, cx).with_language(Arc::new(rust_lang()), cx); - buffer.edit_with_autoindent([(5..5, "\nb")], IndentSize::spaces(4), cx); + buffer.edit_with_autoindent([(5..5, "\nb")], cx); assert_eq!( buffer.text(), " @@ -831,11 +807,7 @@ fn test_autoindent_adjusts_lines_when_only_text_changes(cx: &mut MutableAppConte // The indentation suggestion changed because `@end` node (a close paren) // is now at the beginning of the line. - buffer.edit_with_autoindent( - [(Point::new(1, 4)..Point::new(1, 5), "")], - IndentSize::spaces(4), - cx, - ); + buffer.edit_with_autoindent([(Point::new(1, 4)..Point::new(1, 5), "")], cx); assert_eq!( buffer.text(), " @@ -851,10 +823,11 @@ fn test_autoindent_adjusts_lines_when_only_text_changes(cx: &mut MutableAppConte #[gpui::test] fn test_autoindent_with_edit_at_end_of_buffer(cx: &mut MutableAppContext) { + cx.set_global(Settings::test(cx)); cx.add_model(|cx| { let text = "a\nb"; let mut buffer = Buffer::new(0, text, cx).with_language(Arc::new(rust_lang()), cx); - buffer.edit_with_autoindent([(0..1, "\n"), (2..3, "\n")], IndentSize::spaces(4), cx); + buffer.edit_with_autoindent([(0..1, "\n"), (2..3, "\n")], cx); assert_eq!(buffer.text(), "\n\n\n"); buffer }); @@ -862,6 +835,7 @@ fn test_autoindent_with_edit_at_end_of_buffer(cx: &mut MutableAppContext) { #[gpui::test] fn test_autoindent_multi_line_insertion(cx: &mut MutableAppContext) { + cx.set_global(Settings::test(cx)); cx.add_model(|cx| { let text = " const a: usize = 1; @@ -876,7 +850,6 @@ fn test_autoindent_multi_line_insertion(cx: &mut MutableAppContext) { let mut buffer = Buffer::new(0, text, cx).with_language(Arc::new(rust_lang()), cx); buffer.edit_with_autoindent( [(Point::new(3, 0)..Point::new(3, 0), "e(\n f()\n);\n")], - IndentSize::spaces(4), cx, ); assert_eq!( @@ -903,6 +876,7 @@ fn test_autoindent_multi_line_insertion(cx: &mut MutableAppContext) { fn test_autoindent_preserves_relative_indentation_in_multi_line_insertion( cx: &mut MutableAppContext, ) { + cx.set_global(Settings::test(cx)); cx.add_model(|cx| { let text = " fn a() { @@ -924,7 +898,6 @@ fn test_autoindent_preserves_relative_indentation_in_multi_line_insertion( // insert at the beginning of a line buffer.edit_with_autoindent( [(Point::new(2, 0)..Point::new(2, 0), pasted_text.clone())], - IndentSize::spaces(4), cx, ); assert_eq!( @@ -947,7 +920,8 @@ fn test_autoindent_preserves_relative_indentation_in_multi_line_insertion( } #[gpui::test] -fn test_autoindent_disabled(cx: &mut MutableAppContext) { +fn test_autoindent_language_without_indents_query(cx: &mut MutableAppContext) { + cx.set_global(Settings::test(cx)); cx.add_model(|cx| { let text = " * one @@ -967,11 +941,7 @@ fn test_autoindent_disabled(cx: &mut MutableAppContext) { )), cx, ); - buffer.edit_with_autoindent( - [(Point::new(3, 0)..Point::new(3, 0), "\n")], - IndentSize::spaces(4), - cx, - ); + buffer.edit_with_autoindent([(Point::new(3, 0)..Point::new(3, 0), "\n")], cx); assert_eq!( buffer.text(), " diff --git a/crates/zed/src/languages/c.rs b/crates/zed/src/languages/c.rs index 54554beaf6..71fde5bd5e 100644 --- a/crates/zed/src/languages/c.rs +++ b/crates/zed/src/languages/c.rs @@ -249,34 +249,37 @@ impl super::LspAdapter for CLspAdapter { #[cfg(test)] mod tests { use gpui::MutableAppContext; - use language::{Buffer, IndentSize}; + use language::Buffer; + use settings::Settings; use std::sync::Arc; #[gpui::test] fn test_c_autoindent(cx: &mut MutableAppContext) { cx.foreground().set_block_on_ticks(usize::MAX..=usize::MAX); + let mut settings = Settings::test(cx); + settings.editor_overrides.tab_size = Some(2.try_into().unwrap()); + cx.set_global(settings); let language = crate::languages::language("c", tree_sitter_c::language(), None); cx.add_model(|cx| { let mut buffer = Buffer::new(0, "", cx).with_language(Arc::new(language), cx); - let size = IndentSize::spaces(2); // empty function - buffer.edit_with_autoindent([(0..0, "int main() {}")], size, cx); + buffer.edit_with_autoindent([(0..0, "int main() {}")], cx); // indent inside braces let ix = buffer.len() - 1; - buffer.edit_with_autoindent([(ix..ix, "\n\n")], size, cx); + buffer.edit_with_autoindent([(ix..ix, "\n\n")], cx); assert_eq!(buffer.text(), "int main() {\n \n}"); // indent body of single-statement if statement let ix = buffer.len() - 2; - buffer.edit_with_autoindent([(ix..ix, "if (a)\nb;")], size, cx); + buffer.edit_with_autoindent([(ix..ix, "if (a)\nb;")], cx); assert_eq!(buffer.text(), "int main() {\n if (a)\n b;\n}"); // indent inside field expression let ix = buffer.len() - 3; - buffer.edit_with_autoindent([(ix..ix, "\n.c")], size, cx); + buffer.edit_with_autoindent([(ix..ix, "\n.c")], cx); assert_eq!(buffer.text(), "int main() {\n if (a)\n b\n .c;\n}"); buffer diff --git a/crates/zed/src/languages/python.rs b/crates/zed/src/languages/python.rs index ca0b24bda7..644d9ece40 100644 --- a/crates/zed/src/languages/python.rs +++ b/crates/zed/src/languages/python.rs @@ -147,20 +147,23 @@ impl LspAdapter for PythonLspAdapter { #[cfg(test)] mod tests { use gpui::{ModelContext, MutableAppContext}; - use language::{Buffer, IndentSize}; + use language::Buffer; + use settings::Settings; use std::sync::Arc; #[gpui::test] fn test_python_autoindent(cx: &mut MutableAppContext) { cx.foreground().set_block_on_ticks(usize::MAX..=usize::MAX); let language = crate::languages::language("python", tree_sitter_python::language(), None); + let mut settings = Settings::test(cx); + settings.editor_overrides.tab_size = Some(2.try_into().unwrap()); + cx.set_global(settings); cx.add_model(|cx| { let mut buffer = Buffer::new(0, "", cx).with_language(Arc::new(language), cx); - let size = IndentSize::spaces(2); let append = |buffer: &mut Buffer, text: &str, cx: &mut ModelContext| { let ix = buffer.len(); - buffer.edit_with_autoindent([(ix..ix, text)], size, cx); + buffer.edit_with_autoindent([(ix..ix, text)], cx); }; // indent after "def():" @@ -204,7 +207,7 @@ mod tests { // dedent the closing paren if it is shifted to the beginning of the line let argument_ix = buffer.text().find("1").unwrap(); - buffer.edit_with_autoindent([(argument_ix..argument_ix + 1, "")], size, cx); + buffer.edit_with_autoindent([(argument_ix..argument_ix + 1, "")], cx); assert_eq!( buffer.text(), "def a():\n \n if a:\n b()\n else:\n foo(\n )" @@ -219,7 +222,7 @@ mod tests { // manually outdent the last line let end_whitespace_ix = buffer.len() - 4; - buffer.edit_with_autoindent([(end_whitespace_ix..buffer.len(), "")], size, cx); + buffer.edit_with_autoindent([(end_whitespace_ix..buffer.len(), "")], cx); assert_eq!( buffer.text(), "def a():\n \n if a:\n b()\n else:\n foo(\n )\n" diff --git a/crates/zed/src/languages/rust.rs b/crates/zed/src/languages/rust.rs index 18d49f78d4..856986b1eb 100644 --- a/crates/zed/src/languages/rust.rs +++ b/crates/zed/src/languages/rust.rs @@ -257,6 +257,7 @@ mod tests { use super::*; use crate::languages::{language, CachedLspAdapter}; use gpui::{color::Color, MutableAppContext}; + use settings::Settings; use theme::SyntaxTheme; #[gpui::test] @@ -433,37 +434,39 @@ mod tests { fn test_rust_autoindent(cx: &mut MutableAppContext) { cx.foreground().set_block_on_ticks(usize::MAX..=usize::MAX); let language = crate::languages::language("rust", tree_sitter_rust::language(), None); + let mut settings = Settings::test(cx); + settings.editor_overrides.tab_size = Some(2.try_into().unwrap()); + cx.set_global(settings); cx.add_model(|cx| { let mut buffer = Buffer::new(0, "", cx).with_language(Arc::new(language), cx); - let size = IndentSize::spaces(2); // indent between braces buffer.set_text("fn a() {}", cx); let ix = buffer.len() - 1; - buffer.edit_with_autoindent([(ix..ix, "\n\n")], size, cx); + buffer.edit_with_autoindent([(ix..ix, "\n\n")], cx); assert_eq!(buffer.text(), "fn a() {\n \n}"); // indent between braces, even after empty lines buffer.set_text("fn a() {\n\n\n}", cx); let ix = buffer.len() - 2; - buffer.edit_with_autoindent([(ix..ix, "\n")], size, cx); + buffer.edit_with_autoindent([(ix..ix, "\n")], cx); assert_eq!(buffer.text(), "fn a() {\n\n\n \n}"); // indent a line that continues a field expression buffer.set_text("fn a() {\n \n}", cx); let ix = buffer.len() - 2; - buffer.edit_with_autoindent([(ix..ix, "b\n.c")], size, cx); + buffer.edit_with_autoindent([(ix..ix, "b\n.c")], cx); assert_eq!(buffer.text(), "fn a() {\n b\n .c\n}"); // indent further lines that continue the field expression, even after empty lines let ix = buffer.len() - 2; - buffer.edit_with_autoindent([(ix..ix, "\n\n.d")], size, cx); + buffer.edit_with_autoindent([(ix..ix, "\n\n.d")], cx); assert_eq!(buffer.text(), "fn a() {\n b\n .c\n \n .d\n}"); // dedent the line after the field expression let ix = buffer.len() - 2; - buffer.edit_with_autoindent([(ix..ix, ";\ne")], size, cx); + buffer.edit_with_autoindent([(ix..ix, ";\ne")], cx); assert_eq!( buffer.text(), "fn a() {\n b\n .c\n \n .d;\n e\n}" @@ -472,17 +475,17 @@ mod tests { // indent inside a struct within a call buffer.set_text("const a: B = c(D {});", cx); let ix = buffer.len() - 3; - buffer.edit_with_autoindent([(ix..ix, "\n\n")], size, cx); + buffer.edit_with_autoindent([(ix..ix, "\n\n")], cx); assert_eq!(buffer.text(), "const a: B = c(D {\n \n});"); // indent further inside a nested call let ix = buffer.len() - 4; - buffer.edit_with_autoindent([(ix..ix, "e: f(\n\n)")], size, cx); + buffer.edit_with_autoindent([(ix..ix, "e: f(\n\n)")], cx); assert_eq!(buffer.text(), "const a: B = c(D {\n e: f(\n \n )\n});"); // keep that indent after an empty line let ix = buffer.len() - 8; - buffer.edit_with_autoindent([(ix..ix, "\n")], size, cx); + buffer.edit_with_autoindent([(ix..ix, "\n")], cx); assert_eq!( buffer.text(), "const a: B = c(D {\n e: f(\n \n \n )\n});" From fa5af4383df896164f0de7fe8e5135f269d5d863 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Thu, 28 Jul 2022 14:03:31 -0700 Subject: [PATCH 28/42] Introduce AutoindentMode parameter to Buffer::edit This controls whether or not we preserve the relative indentation of inserted text blocks. Co-authored-by: Mikayla Maki --- .../src/activity_indicator.rs | 1 + crates/collab/src/integration_tests.rs | 22 +-- crates/editor/src/display_map.rs | 3 +- crates/editor/src/display_map/block_map.rs | 2 +- crates/editor/src/display_map/fold_map.rs | 7 +- crates/editor/src/editor.rs | 70 ++++++---- crates/editor/src/multi_buffer.rs | 76 ++++------- crates/language/Cargo.toml | 2 + crates/language/src/buffer.rs | 128 +++++++++--------- crates/language/src/tests.rs | 114 +++++++++++----- crates/project/src/project.rs | 6 +- crates/project/src/project_tests.rs | 51 ++++--- crates/search/src/buffer_search.rs | 2 +- crates/vim/src/normal.rs | 4 +- crates/vim/src/visual.rs | 4 +- crates/zed/src/languages/c.rs | 14 +- crates/zed/src/languages/python.rs | 18 ++- crates/zed/src/languages/rust.rs | 20 +-- 18 files changed, 308 insertions(+), 236 deletions(-) diff --git a/crates/activity_indicator/src/activity_indicator.rs b/crates/activity_indicator/src/activity_indicator.rs index 3730b5d21a..02ad100df8 100644 --- a/crates/activity_indicator/src/activity_indicator.rs +++ b/crates/activity_indicator/src/activity_indicator.rs @@ -82,6 +82,7 @@ impl ActivityIndicator { buffer.update(cx, |buffer, cx| { buffer.edit( [(0..0, format!("Language server error: {}\n\n", lsp_name))], + None, cx, ); }); diff --git a/crates/collab/src/integration_tests.rs b/crates/collab/src/integration_tests.rs index 9471967eee..1f3ccef0be 100644 --- a/crates/collab/src/integration_tests.rs +++ b/crates/collab/src/integration_tests.rs @@ -842,8 +842,8 @@ async fn test_propagate_saves_and_fs_changes( .update(cx_c, |p, cx| p.open_buffer((worktree_id, "file1"), cx)) .await .unwrap(); - buffer_b.update(cx_b, |buf, cx| buf.edit([(0..0, "i-am-b, ")], cx)); - buffer_c.update(cx_c, |buf, cx| buf.edit([(0..0, "i-am-c, ")], cx)); + buffer_b.update(cx_b, |buf, cx| buf.edit([(0..0, "i-am-b, ")], None, cx)); + buffer_c.update(cx_c, |buf, cx| buf.edit([(0..0, "i-am-c, ")], None, cx)); // Open and edit that buffer as the host. let buffer_a = project_a @@ -855,7 +855,7 @@ async fn test_propagate_saves_and_fs_changes( .condition(cx_a, |buf, _| buf.text() == "i-am-c, i-am-b, ") .await; buffer_a.update(cx_a, |buf, cx| { - buf.edit([(buf.len()..buf.len(), "i-am-a")], cx) + buf.edit([(buf.len()..buf.len(), "i-am-a")], None, cx) }); // Wait for edits to propagate @@ -871,7 +871,7 @@ async fn test_propagate_saves_and_fs_changes( // Edit the buffer as the host and concurrently save as guest B. let save_b = buffer_b.update(cx_b, |buf, cx| buf.save(cx)); - buffer_a.update(cx_a, |buf, cx| buf.edit([(0..0, "hi-a, ")], cx)); + buffer_a.update(cx_a, |buf, cx| buf.edit([(0..0, "hi-a, ")], None, cx)); save_b.await.unwrap(); assert_eq!( client_a.fs.load("/a/file1".as_ref()).await.unwrap(), @@ -1237,7 +1237,7 @@ async fn test_buffer_conflict_after_save(cx_a: &mut TestAppContext, cx_b: &mut T .await .unwrap(); - buffer_b.update(cx_b, |buf, cx| buf.edit([(0..0, "world ")], cx)); + buffer_b.update(cx_b, |buf, cx| buf.edit([(0..0, "world ")], None, cx)); buffer_b.read_with(cx_b, |buf, _| { assert!(buf.is_dirty()); assert!(!buf.has_conflict()); @@ -1251,7 +1251,7 @@ async fn test_buffer_conflict_after_save(cx_a: &mut TestAppContext, cx_b: &mut T assert!(!buf.has_conflict()); }); - buffer_b.update(cx_b, |buf, cx| buf.edit([(0..0, "hello ")], cx)); + buffer_b.update(cx_b, |buf, cx| buf.edit([(0..0, "hello ")], None, cx)); buffer_b.read_with(cx_b, |buf, _| { assert!(buf.is_dirty()); assert!(!buf.has_conflict()); @@ -1342,9 +1342,9 @@ async fn test_editing_while_guest_opens_buffer( // Edit the buffer as client A while client B is still opening it. cx_b.background().simulate_random_delay().await; - buffer_a.update(cx_a, |buf, cx| buf.edit([(0..0, "X")], cx)); + buffer_a.update(cx_a, |buf, cx| buf.edit([(0..0, "X")], None, cx)); cx_b.background().simulate_random_delay().await; - buffer_a.update(cx_a, |buf, cx| buf.edit([(1..1, "Y")], cx)); + buffer_a.update(cx_a, |buf, cx| buf.edit([(1..1, "Y")], None, cx)); let text = buffer_a.read_with(cx_a, |buf, _| buf.text()); let buffer_b = buffer_b.await.unwrap(); @@ -1882,8 +1882,8 @@ async fn test_reloading_buffer_manually(cx_a: &mut TestAppContext, cx_b: &mut Te .await .unwrap(); buffer_b.update(cx_b, |buffer, cx| { - buffer.edit([(4..7, "six")], cx); - buffer.edit([(10..11, "6")], cx); + buffer.edit([(4..7, "six")], None, cx); + buffer.edit([(10..11, "6")], None, cx); assert_eq!(buffer.text(), "let six = 6;"); assert!(buffer.is_dirty()); assert!(!buffer.has_conflict()); @@ -2964,7 +2964,7 @@ async fn test_collaborating_with_renames(cx_a: &mut TestAppContext, cx_b: &mut T ); rename.editor.update(cx, |rename_editor, cx| { rename_editor.buffer().update(cx, |rename_buffer, cx| { - rename_buffer.edit([(0..3, "THREE")], cx); + rename_buffer.edit([(0..3, "THREE")], None, cx); }); }); }); diff --git a/crates/editor/src/display_map.rs b/crates/editor/src/display_map.rs index 6f50da6a7d..9b12df60d9 100644 --- a/crates/editor/src/display_map.rs +++ b/crates/editor/src/display_map.rs @@ -897,7 +897,7 @@ pub mod tests { let ix = snapshot.buffer_snapshot.text().find("seven").unwrap(); buffer.update(cx, |buffer, cx| { - buffer.edit([(ix..ix, "and ")], cx); + buffer.edit([(ix..ix, "and ")], None, cx); }); let snapshot = map.update(cx, |map, cx| map.snapshot(cx)); @@ -936,6 +936,7 @@ pub mod tests { (Point::new(1, 1)..Point::new(1, 1), "\t"), (Point::new(2, 1)..Point::new(2, 1), "\t"), ], + None, cx, ) }); diff --git a/crates/editor/src/display_map/block_map.rs b/crates/editor/src/display_map/block_map.rs index ed0df25d69..5237901170 100644 --- a/crates/editor/src/display_map/block_map.rs +++ b/crates/editor/src/display_map/block_map.rs @@ -1164,7 +1164,7 @@ mod tests { // Insert a line break, separating two block decorations into separate lines. let buffer_snapshot = buffer.update(cx, |buffer, cx| { - buffer.edit([(Point::new(1, 1)..Point::new(1, 1), "!!!\n")], cx); + buffer.edit([(Point::new(1, 1)..Point::new(1, 1), "!!!\n")], None, cx); buffer.snapshot(cx) }); diff --git a/crates/editor/src/display_map/fold_map.rs b/crates/editor/src/display_map/fold_map.rs index 95c3abb252..a6e5536d15 100644 --- a/crates/editor/src/display_map/fold_map.rs +++ b/crates/editor/src/display_map/fold_map.rs @@ -1240,6 +1240,7 @@ mod tests { (Point::new(0, 0)..Point::new(0, 1), "123"), (Point::new(2, 3)..Point::new(2, 3), "123"), ], + None, cx, ); buffer.snapshot(cx) @@ -1262,7 +1263,7 @@ mod tests { ); let buffer_snapshot = buffer.update(cx, |buffer, cx| { - buffer.edit([(Point::new(2, 6)..Point::new(4, 3), "456")], cx); + buffer.edit([(Point::new(2, 6)..Point::new(4, 3), "456")], None, cx); buffer.snapshot(cx) }); let (snapshot4, _) = map.read(buffer_snapshot.clone(), subscription.consume().into_inner()); @@ -1318,7 +1319,7 @@ mod tests { // Edit within one of the folds. let buffer_snapshot = buffer.update(cx, |buffer, cx| { - buffer.edit([(0..1, "12345")], cx); + buffer.edit([(0..1, "12345")], None, cx); buffer.snapshot(cx) }); let (snapshot, _) = @@ -1360,7 +1361,7 @@ mod tests { assert_eq!(snapshot.text(), "aa…cccc\nd…eeeee"); let buffer_snapshot = buffer.update(cx, |buffer, cx| { - buffer.edit([(Point::new(2, 2)..Point::new(3, 1), "")], cx); + buffer.edit([(Point::new(2, 2)..Point::new(3, 1), "")], None, cx); buffer.snapshot(cx) }); let (snapshot, _) = map.read(buffer_snapshot.clone(), subscription.consume().into_inner()); diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index f570fb2fb6..4543aa3b27 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -38,9 +38,9 @@ use hover_popover::{hide_hover, HoverState}; pub use items::MAX_TAB_TITLE_LEN; pub use language::{char_kind, CharKind}; use language::{ - BracketPair, Buffer, CodeAction, CodeLabel, Completion, Diagnostic, DiagnosticSeverity, - IndentKind, IndentSize, Language, OffsetRangeExt, OffsetUtf16, Point, Selection, SelectionGoal, - TransactionId, + AutoindentMode, BracketPair, Buffer, CodeAction, CodeLabel, Completion, Diagnostic, + DiagnosticSeverity, IndentKind, IndentSize, Language, OffsetRangeExt, OffsetUtf16, Point, + Selection, SelectionGoal, TransactionId, }; use link_go_to_definition::LinkGoToDefinitionState; pub use multi_buffer::{ @@ -1464,7 +1464,8 @@ impl Editor { S: ToOffset, T: Into>, { - self.buffer.update(cx, |buffer, cx| buffer.edit(edits, cx)); + self.buffer + .update(cx, |buffer, cx| buffer.edit(edits, None, cx)); } pub fn edit_with_autoindent(&mut self, edits: I, cx: &mut ViewContext) @@ -1473,8 +1474,9 @@ impl Editor { S: ToOffset, T: Into>, { - self.buffer - .update(cx, |buffer, cx| buffer.edit_with_autoindent(edits, cx)); + self.buffer.update(cx, |buffer, cx| { + buffer.edit(edits, Some(AutoindentMode::Independent), cx) + }); } fn select(&mut self, Select(phase): &Select, cx: &mut ViewContext) { @@ -1887,9 +1889,7 @@ impl Editor { .unzip() }; - this.buffer.update(cx, |buffer, cx| { - buffer.edit_with_autoindent(edits, cx); - }); + this.edit_with_autoindent(edits, cx); let buffer = this.buffer.read(cx).snapshot(cx); let new_selections = selection_fixup_info .into_iter() @@ -1922,10 +1922,11 @@ impl Editor { }) .collect::>() }; - buffer.edit_with_autoindent( + buffer.edit( old_selections .iter() .map(|s| (s.start..s.end, text.clone())), + Some(AutoindentMode::Block), cx, ); anchors @@ -1986,6 +1987,7 @@ impl Editor { (s.end.clone()..s.end.clone(), pair_end.clone()), ] }), + None, cx, ); }); @@ -2061,6 +2063,7 @@ impl Editor { selection_ranges .iter() .map(|range| (range.clone(), pair_end.clone())), + None, cx, ); snapshot = buffer.snapshot(cx); @@ -2363,8 +2366,11 @@ impl Editor { this.insert_snippet(&ranges, snippet, cx).log_err(); } else { this.buffer.update(cx, |buffer, cx| { - buffer - .edit_with_autoindent(ranges.iter().map(|range| (range.clone(), text)), cx); + buffer.edit( + ranges.iter().map(|range| (range.clone(), text)), + Some(AutoindentMode::Block), + cx, + ); }); } }); @@ -2725,11 +2731,12 @@ impl Editor { ) -> Result<()> { let tabstops = self.buffer.update(cx, |buffer, cx| { let snippet_text: Arc = snippet.text.clone().into(); - buffer.edit_with_autoindent( + buffer.edit( insertion_ranges .iter() .cloned() .map(|range| (range, snippet_text.clone())), + Some(AutoindentMode::Independent), cx, ); @@ -2933,7 +2940,11 @@ impl Editor { let chars_to_next_tab_stop = tab_size - (char_column as u32 % tab_size); IndentSize::spaces(chars_to_next_tab_stop) }; - buffer.edit([(cursor..cursor, tab_size.chars().collect::())], cx); + buffer.edit( + [(cursor..cursor, tab_size.chars().collect::())], + None, + cx, + ); cursor.column += tab_size.len; selection.start = cursor; selection.end = cursor; @@ -3006,6 +3017,7 @@ impl Editor { row_start..row_start, indent_delta.chars().collect::(), )], + None, cx, ); @@ -3080,6 +3092,7 @@ impl Editor { deletion_ranges .into_iter() .map(|range| (range, empty_str.clone())), + None, cx, ); }); @@ -3145,6 +3158,7 @@ impl Editor { edit_ranges .into_iter() .map(|range| (range, empty_str.clone())), + None, cx, ); buffer.snapshot(cx) @@ -3202,7 +3216,7 @@ impl Editor { self.transact(cx, |this, cx| { this.buffer.update(cx, |buffer, cx| { - buffer.edit(edits, cx); + buffer.edit(edits, None, cx); }); this.request_autoscroll(Autoscroll::Fit, cx); @@ -3311,7 +3325,7 @@ impl Editor { this.unfold_ranges(unfold_ranges, true, cx); this.buffer.update(cx, |buffer, cx| { for (range, text) in edits { - buffer.edit([(range, text)], cx); + buffer.edit([(range, text)], None, cx); } }); this.fold_ranges(refold_ranges, cx); @@ -3416,7 +3430,7 @@ impl Editor { this.unfold_ranges(unfold_ranges, true, cx); this.buffer.update(cx, |buffer, cx| { for (range, text) in edits { - buffer.edit([(range, text)], cx); + buffer.edit([(range, text)], None, cx); } }); this.fold_ranges(refold_ranges, cx); @@ -3467,7 +3481,8 @@ impl Editor { }); edits }); - this.buffer.update(cx, |buffer, cx| buffer.edit(edits, cx)); + this.buffer + .update(cx, |buffer, cx| buffer.edit(edits, None, cx)); let selections = this.selections.all::(cx); this.change_selections(Some(Autoscroll::Fit), cx, |s| { s.select(selections); @@ -3597,7 +3612,7 @@ impl Editor { edits.push((range, to_insert)); } drop(snapshot); - buffer.edit_with_autoindent(edits, cx); + buffer.edit(edits, Some(AutoindentMode::Block), cx); }); let selections = this.selections.all::(cx); @@ -4432,6 +4447,7 @@ impl Editor { .iter() .cloned() .map(|range| (range, empty_str.clone())), + None, cx, ); } else { @@ -4441,7 +4457,7 @@ impl Editor { let position = Point::new(range.start.row, min_column); (position..position, full_comment_prefix.clone()) }); - buffer.edit(edits, cx); + buffer.edit(edits, None, cx); } } } @@ -4875,9 +4891,9 @@ impl Editor { editor.override_text_style = Some(Box::new(move |style| old_highlight_id.style(&style.syntax))); } - editor - .buffer - .update(cx, |buffer, cx| buffer.edit([(0..0, old_name.clone())], cx)); + editor.buffer.update(cx, |buffer, cx| { + buffer.edit([(0..0, old_name.clone())], None, cx) + }); editor.select_all(&SelectAll, cx); editor }); @@ -6658,8 +6674,8 @@ mod tests { // Simulate an edit in another editor buffer.update(cx, |buffer, cx| { buffer.start_transaction_at(now, cx); - buffer.edit([(0..1, "a")], cx); - buffer.edit([(1..1, "b")], cx); + buffer.edit([(0..1, "a")], None, cx); + buffer.edit([(1..1, "b")], None, cx); buffer.end_transaction_at(now, cx); }); @@ -7200,6 +7216,7 @@ mod tests { (Point::new(1, 0)..Point::new(1, 0), "\t"), (Point::new(1, 1)..Point::new(1, 1), "\t"), ], + None, cx, ); }); @@ -7836,6 +7853,7 @@ mod tests { (Point::new(1, 2)..Point::new(3, 0), ""), (Point::new(4, 2)..Point::new(6, 0), ""), ], + None, cx, ); assert_eq!( @@ -7894,7 +7912,7 @@ mod tests { // Edit the buffer directly, deleting ranges surrounding the editor's selections buffer.update(cx, |buffer, cx| { - buffer.edit([(2..5, ""), (10..13, ""), (18..21, "")], cx); + buffer.edit([(2..5, ""), (10..13, ""), (18..21, "")], None, cx); assert_eq!(buffer.read(cx).text(), "a(), b(), c()".unindent()); }); diff --git a/crates/editor/src/multi_buffer.rs b/crates/editor/src/multi_buffer.rs index fa48cabf05..a12285dbbe 100644 --- a/crates/editor/src/multi_buffer.rs +++ b/crates/editor/src/multi_buffer.rs @@ -7,9 +7,9 @@ use collections::{Bound, HashMap, HashSet}; use gpui::{AppContext, Entity, ModelContext, ModelHandle, Task}; pub use language::Completion; use language::{ - char_kind, Buffer, BufferChunks, BufferSnapshot, CharKind, Chunk, DiagnosticEntry, Event, File, - IndentSize, Language, OffsetRangeExt, Outline, OutlineItem, Selection, ToOffset as _, - ToOffsetUtf16 as _, ToPoint as _, ToPointUtf16 as _, TransactionId, + char_kind, AutoindentMode, Buffer, BufferChunks, BufferSnapshot, CharKind, Chunk, + DiagnosticEntry, Event, File, IndentSize, Language, OffsetRangeExt, Outline, OutlineItem, + Selection, ToOffset as _, ToOffsetUtf16 as _, ToPoint as _, ToPointUtf16 as _, TransactionId, }; use smallvec::SmallVec; use std::{ @@ -302,28 +302,10 @@ impl MultiBuffer { self.read(cx).symbols_containing(offset, theme) } - pub fn edit(&mut self, edits: I, cx: &mut ModelContext) - where - I: IntoIterator, T)>, - S: ToOffset, - T: Into>, - { - self.edit_internal(edits, false, cx) - } - - pub fn edit_with_autoindent(&mut self, edits: I, cx: &mut ModelContext) - where - I: IntoIterator, T)>, - S: ToOffset, - T: Into>, - { - self.edit_internal(edits, true, cx) - } - - pub fn edit_internal( + pub fn edit( &mut self, edits: I, - autoindent: bool, + autoindent_mode: Option, cx: &mut ModelContext, ) where I: IntoIterator, T)>, @@ -345,11 +327,7 @@ impl MultiBuffer { if let Some(buffer) = self.as_singleton() { return buffer.update(cx, |buffer, cx| { - if autoindent { - buffer.edit_with_autoindent(edits, cx); - } else { - buffer.edit(edits, cx); - } + buffer.edit(edits, autoindent_mode, cx); }); } @@ -464,13 +442,8 @@ impl MultiBuffer { } } - if autoindent { - buffer.edit_with_autoindent(deletions, cx); - buffer.edit_with_autoindent(insertions, cx); - } else { - buffer.edit(deletions, cx); - buffer.edit(insertions, cx); - } + buffer.edit(deletions, autoindent_mode, cx); + buffer.edit(insertions, autoindent_mode, cx); }) } } @@ -1386,7 +1359,7 @@ impl MultiBuffer { log::info!("mutating multi-buffer with {:?}", edits); drop(snapshot); - self.edit(edits, cx); + self.edit(edits, None, cx); } pub fn randomly_edit_excerpts( @@ -3224,7 +3197,7 @@ mod tests { .collect::>() ); - buffer.update(cx, |buffer, cx| buffer.edit([(1..3, "XXX\n")], cx)); + buffer.update(cx, |buffer, cx| buffer.edit([(1..3, "XXX\n")], None, cx)); let snapshot = multibuffer.read(cx).snapshot(cx); assert_eq!(snapshot.text(), buffer.read(cx).text()); @@ -3247,11 +3220,11 @@ mod tests { let snapshot = multibuffer.read(cx).snapshot(cx); assert_eq!(snapshot.text(), "a"); - guest_buffer.update(cx, |buffer, cx| buffer.edit([(1..1, "b")], cx)); + guest_buffer.update(cx, |buffer, cx| buffer.edit([(1..1, "b")], None, cx)); let snapshot = multibuffer.read(cx).snapshot(cx); assert_eq!(snapshot.text(), "ab"); - guest_buffer.update(cx, |buffer, cx| buffer.edit([(2..2, "c")], cx)); + guest_buffer.update(cx, |buffer, cx| buffer.edit([(2..2, "c")], None, cx)); let snapshot = multibuffer.read(cx).snapshot(cx); assert_eq!(snapshot.text(), "abc"); } @@ -3392,6 +3365,7 @@ mod tests { (Point::new(0, 0)..Point::new(0, 0), text), (Point::new(2, 1)..Point::new(2, 3), text), ], + None, cx, ); }); @@ -3529,8 +3503,8 @@ mod tests { let multibuffer = cx.add_model(|cx| MultiBuffer::singleton(buffer.clone(), cx)); let old_snapshot = multibuffer.read(cx).snapshot(cx); buffer.update(cx, |buffer, cx| { - buffer.edit([(0..0, "X")], cx); - buffer.edit([(5..5, "Y")], cx); + buffer.edit([(0..0, "X")], None, cx); + buffer.edit([(5..5, "Y")], None, cx); }); let new_snapshot = multibuffer.read(cx).snapshot(cx); @@ -3577,12 +3551,12 @@ mod tests { assert_eq!(Anchor::max().to_offset(&old_snapshot), 10); buffer_1.update(cx, |buffer, cx| { - buffer.edit([(0..0, "W")], cx); - buffer.edit([(5..5, "X")], cx); + buffer.edit([(0..0, "W")], None, cx); + buffer.edit([(5..5, "X")], None, cx); }); buffer_2.update(cx, |buffer, cx| { - buffer.edit([(0..0, "Y")], cx); - buffer.edit([(6..6, "Z")], cx); + buffer.edit([(0..0, "Y")], None, cx); + buffer.edit([(6..6, "Z")], None, cx); }); let new_snapshot = multibuffer.read(cx).snapshot(cx); @@ -3611,7 +3585,7 @@ mod tests { // 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)); + buffer_1.update(cx, |buffer, cx| buffer.edit([(4..4, "123")], None, cx)); let excerpt_id_1 = multibuffer.update(cx, |multibuffer, cx| { multibuffer .push_excerpts( @@ -4184,6 +4158,7 @@ mod tests { (Point::new(0, 0)..Point::new(0, 0), "A"), (Point::new(1, 0)..Point::new(1, 0), "A"), ], + None, cx, ); multibuffer.edit( @@ -4191,6 +4166,7 @@ mod tests { (Point::new(0, 1)..Point::new(0, 1), "B"), (Point::new(1, 1)..Point::new(1, 1), "B"), ], + None, cx, ); multibuffer.end_transaction_at(now, cx); @@ -4199,19 +4175,19 @@ mod tests { // Edit buffer 1 through the multibuffer now += 2 * group_interval; multibuffer.start_transaction_at(now, cx); - multibuffer.edit([(2..2, "C")], cx); + multibuffer.edit([(2..2, "C")], None, cx); multibuffer.end_transaction_at(now, cx); assert_eq!(multibuffer.read(cx).text(), "ABC1234\nAB5678"); // Edit buffer 1 independently buffer_1.update(cx, |buffer_1, cx| { buffer_1.start_transaction_at(now); - buffer_1.edit([(3..3, "D")], cx); + buffer_1.edit([(3..3, "D")], None, cx); buffer_1.end_transaction_at(now, cx); now += 2 * group_interval; buffer_1.start_transaction_at(now); - buffer_1.edit([(4..4, "E")], cx); + buffer_1.edit([(4..4, "E")], None, cx); buffer_1.end_transaction_at(now, cx); }); assert_eq!(multibuffer.read(cx).text(), "ABCDE1234\nAB5678"); @@ -4252,7 +4228,7 @@ mod tests { // Redo stack gets cleared after an edit. now += 2 * group_interval; multibuffer.start_transaction_at(now, cx); - multibuffer.edit([(0..0, "X")], cx); + multibuffer.edit([(0..0, "X")], None, cx); multibuffer.end_transaction_at(now, cx); assert_eq!(multibuffer.read(cx).text(), "XABCD1234\nAB5678"); multibuffer.redo(cx); diff --git a/crates/language/Cargo.toml b/crates/language/Cargo.toml index 0dc8165bfb..6e9f368e77 100644 --- a/crates/language/Cargo.toml +++ b/crates/language/Cargo.toml @@ -16,6 +16,7 @@ test-support = [ "text/test-support", "tree-sitter-rust", "tree-sitter-typescript", + "settings/test-support", "util/test-support", ] @@ -57,6 +58,7 @@ collections = { path = "../collections", features = ["test-support"] } gpui = { path = "../gpui", features = ["test-support"] } lsp = { path = "../lsp", features = ["test-support"] } text = { path = "../text", features = ["test-support"] } +settings = { path = "../settings", features = ["test-support"] } util = { path = "../util", features = ["test-support"] } ctor = "0.1" env_logger = "0.9" diff --git a/crates/language/src/buffer.rs b/crates/language/src/buffer.rs index 4b0d2566c3..8f0f7d40ec 100644 --- a/crates/language/src/buffer.rs +++ b/crates/language/src/buffer.rs @@ -229,11 +229,18 @@ struct SyntaxTree { version: clock::Global, } +#[derive(Clone, Copy)] +pub enum AutoindentMode { + Block, + Independent, +} + #[derive(Clone)] struct AutoindentRequest { before_edit: BufferSnapshot, entries: Vec, indent_size: IndentSize, + mode: AutoindentMode, } #[derive(Clone)] @@ -852,8 +859,12 @@ impl Buffer { // At this point, old_suggestions contains the suggested indentation for all edited lines // with respect to the state of the buffer before the edit, but keyed by the row for these // lines after the edits were applied. + let new_edited_row_ranges = contiguous_ranges( - row_ranges.iter().map(|range| range.start), + row_ranges.iter().flat_map(|range| match request.mode { + AutoindentMode::Block => range.start..range.start + 1, + AutoindentMode::Independent => range.clone(), + }), max_rows_between_yields, ); for new_edited_row_range in new_edited_row_ranges { @@ -883,26 +894,32 @@ impl Buffer { yield_now().await; } - for row_range in row_ranges { - if row_range.len() > 1 { - if let Some(new_indent_size) = indent_sizes.get(&row_range.start).copied() { - let old_indent_size = snapshot.indent_size_for_line(row_range.start); - if new_indent_size.kind == old_indent_size.kind { - let delta = new_indent_size.len as i64 - old_indent_size.len as i64; - if delta != 0 { - for row in row_range.skip(1) { - indent_sizes.entry(row).or_insert_with(|| { - let mut size = snapshot.indent_size_for_line(row); - if size.kind == new_indent_size.kind { - if delta > 0 { - size.len += delta as u32; - } else if delta < 0 { - size.len = - size.len.saturating_sub(-delta as u32); + if matches!(request.mode, AutoindentMode::Block) { + for row_range in row_ranges { + if row_range.len() > 1 { + if let Some(new_indent_size) = + indent_sizes.get(&row_range.start).copied() + { + let old_indent_size = + snapshot.indent_size_for_line(row_range.start); + if new_indent_size.kind == old_indent_size.kind { + let delta = + new_indent_size.len as i64 - old_indent_size.len as i64; + if delta != 0 { + for row in row_range.skip(1) { + indent_sizes.entry(row).or_insert_with(|| { + let mut size = snapshot.indent_size_for_line(row); + if size.kind == new_indent_size.kind { + if delta > 0 { + size.len += delta as u32; + } else if delta < 0 { + size.len = + size.len.saturating_sub(-delta as u32); + } } - } - size - }); + size + }); + } } } } @@ -948,6 +965,7 @@ impl Buffer { .take((size.len - current_size.len) as usize) .collect::(), )], + None, cx, ); } else if size.len < current_size.len { @@ -956,6 +974,7 @@ impl Buffer { Point::new(row, 0)..Point::new(row, current_size.len - size.len), "", )], + None, cx, ); } @@ -993,7 +1012,7 @@ impl Buffer { match tag { ChangeTag::Equal => offset += len, ChangeTag::Delete => { - self.edit([(range, "")], cx); + self.edit([(range, "")], None, cx); } ChangeTag::Insert => { self.edit( @@ -1002,6 +1021,7 @@ impl Buffer { &diff.new_text[range.start - diff.start_offset ..range.end - diff.start_offset], )], + None, cx, ); offset += len; @@ -1138,46 +1158,13 @@ impl Buffer { where T: Into>, { - self.edit_internal([(0..self.len(), text)], None, cx) + self.edit([(0..self.len(), text)], None, cx) } pub fn edit( &mut self, edits_iter: I, - cx: &mut ModelContext, - ) -> Option - where - I: IntoIterator, T)>, - S: ToOffset, - T: Into>, - { - self.edit_internal(edits_iter, None, cx) - } - - pub fn edit_with_autoindent( - &mut self, - edits_iter: I, - cx: &mut ModelContext, - ) -> Option - where - I: IntoIterator, T)>, - S: ToOffset, - T: Into>, - { - let language_name = self.language().map(|language| language.name()); - let settings = cx.global::(); - let indent_size = if settings.hard_tabs(language_name.as_deref()) { - IndentSize::tab() - } else { - IndentSize::spaces(settings.tab_size(language_name.as_deref()).get()) - }; - self.edit_internal(edits_iter, Some(indent_size), cx) - } - - pub fn edit_internal( - &mut self, - edits_iter: I, - autoindent_size: Option, + autoindent_mode: Option, cx: &mut ModelContext, ) -> Option where @@ -1212,16 +1199,21 @@ impl Buffer { self.start_transaction(); self.pending_autoindent.take(); - let autoindent_request = self - .language - .as_ref() - .and_then(|_| autoindent_size) - .map(|autoindent_size| (self.snapshot(), autoindent_size)); + let autoindent_request = autoindent_mode + .and_then(|mode| self.language.as_ref().map(|_| (self.snapshot(), mode))); let edit_operation = self.text.edit(edits.iter().cloned()); let edit_id = edit_operation.local_timestamp(); - if let Some((before_edit, size)) = autoindent_request { + if let Some((before_edit, mode)) = autoindent_request { + let language_name = self.language().map(|language| language.name()); + let settings = cx.global::(); + let indent_size = if settings.hard_tabs(language_name.as_deref()) { + IndentSize::tab() + } else { + IndentSize::spaces(settings.tab_size(language_name.as_deref()).get()) + }; + let mut delta = 0isize; let entries = edits .into_iter() @@ -1248,8 +1240,11 @@ impl Buffer { range_of_insertion_to_indent.start += 1; first_line_is_new = true; } - if new_text[range_of_insertion_to_indent.clone()].ends_with('\n') { - range_of_insertion_to_indent.end -= 1; + + if matches!(mode, AutoindentMode::Block) { + if new_text[range_of_insertion_to_indent.clone()].ends_with('\n') { + range_of_insertion_to_indent.end -= 1; + } } AutoindentRequestEntry { @@ -1263,7 +1258,8 @@ impl Buffer { self.autoindent_requests.push(Arc::new(AutoindentRequest { before_edit, entries, - indent_size: size, + indent_size, + mode, })); } @@ -1550,7 +1546,7 @@ impl Buffer { edits.push((range, new_text)); } log::info!("mutating buffer {} with {:?}", self.replica_id(), edits); - self.edit(edits, cx); + self.edit(edits, None, cx); } pub fn randomly_undo_redo(&mut self, rng: &mut impl rand::Rng, cx: &mut ModelContext) { diff --git a/crates/language/src/tests.rs b/crates/language/src/tests.rs index 0a6fb2ce02..acfaf5980d 100644 --- a/crates/language/src/tests.rs +++ b/crates/language/src/tests.rs @@ -33,8 +33,12 @@ fn test_line_endings(cx: &mut gpui::MutableAppContext) { assert_eq!(buffer.line_ending(), LineEnding::Windows); buffer.check_invariants(); - buffer.edit_with_autoindent([(buffer.len()..buffer.len(), "\r\nfour")], cx); - buffer.edit([(0..0, "zero\r\n")], cx); + buffer.edit( + [(buffer.len()..buffer.len(), "\r\nfour")], + Some(AutoindentMode::Independent), + cx, + ); + buffer.edit([(0..0, "zero\r\n")], None, cx); assert_eq!(buffer.text(), "zero\none\ntwo\nthree\nfour"); assert_eq!(buffer.line_ending(), LineEnding::Windows); buffer.check_invariants(); @@ -114,7 +118,7 @@ fn test_edit_events(cx: &mut gpui::MutableAppContext) { // An edit emits an edited event, followed by a dirty changed event, // since the buffer was previously in a clean state. - buffer.edit([(2..4, "XYZ")], cx); + buffer.edit([(2..4, "XYZ")], None, cx); // An empty transaction does not emit any events. buffer.start_transaction(); @@ -123,8 +127,8 @@ fn test_edit_events(cx: &mut gpui::MutableAppContext) { // A transaction containing two edits emits one edited event. now += Duration::from_secs(1); buffer.start_transaction_at(now); - buffer.edit([(5..5, "u")], cx); - buffer.edit([(6..6, "w")], cx); + buffer.edit([(5..5, "u")], None, cx); + buffer.edit([(6..6, "w")], None, cx); buffer.end_transaction_at(now, cx); // Undoing a transaction emits one edited event. @@ -224,11 +228,11 @@ async fn test_reparse(cx: &mut gpui::TestAppContext) { buf.start_transaction(); let offset = buf.text().find(")").unwrap(); - buf.edit([(offset..offset, "b: C")], cx); + buf.edit([(offset..offset, "b: C")], None, cx); assert!(!buf.is_parsing()); let offset = buf.text().find("}").unwrap(); - buf.edit([(offset..offset, " d; ")], cx); + buf.edit([(offset..offset, " d; ")], None, cx); assert!(!buf.is_parsing()); buf.end_transaction(cx); @@ -253,19 +257,19 @@ async fn test_reparse(cx: &mut gpui::TestAppContext) { // * add a turbofish to the method call buffer.update(cx, |buf, cx| { let offset = buf.text().find(";").unwrap(); - buf.edit([(offset..offset, ".e")], cx); + buf.edit([(offset..offset, ".e")], None, cx); assert_eq!(buf.text(), "fn a(b: C) { d.e; }"); assert!(buf.is_parsing()); }); buffer.update(cx, |buf, cx| { let offset = buf.text().find(";").unwrap(); - buf.edit([(offset..offset, "(f)")], cx); + buf.edit([(offset..offset, "(f)")], None, cx); assert_eq!(buf.text(), "fn a(b: C) { d.e(f); }"); assert!(buf.is_parsing()); }); buffer.update(cx, |buf, cx| { let offset = buf.text().find("(f)").unwrap(); - buf.edit([(offset..offset, "::")], cx); + buf.edit([(offset..offset, "::")], None, cx); assert_eq!(buf.text(), "fn a(b: C) { d.e::(f); }"); assert!(buf.is_parsing()); }); @@ -626,20 +630,32 @@ fn test_autoindent_with_soft_tabs(cx: &mut MutableAppContext) { let text = "fn a() {}"; let mut buffer = Buffer::new(0, text, cx).with_language(Arc::new(rust_lang()), cx); - buffer.edit_with_autoindent([(8..8, "\n\n")], cx); + buffer.edit([(8..8, "\n\n")], Some(AutoindentMode::Independent), cx); assert_eq!(buffer.text(), "fn a() {\n \n}"); - buffer.edit_with_autoindent([(Point::new(1, 4)..Point::new(1, 4), "b()\n")], cx); + buffer.edit( + [(Point::new(1, 4)..Point::new(1, 4), "b()\n")], + Some(AutoindentMode::Independent), + cx, + ); assert_eq!(buffer.text(), "fn a() {\n b()\n \n}"); // Create a field expression on a new line, causing that line // to be indented. - buffer.edit_with_autoindent([(Point::new(2, 4)..Point::new(2, 4), ".c")], cx); + buffer.edit( + [(Point::new(2, 4)..Point::new(2, 4), ".c")], + Some(AutoindentMode::Independent), + cx, + ); assert_eq!(buffer.text(), "fn a() {\n b()\n .c\n}"); // Remove the dot so that the line is no longer a field expression, // causing the line to be outdented. - buffer.edit_with_autoindent([(Point::new(2, 8)..Point::new(2, 9), "")], cx); + buffer.edit( + [(Point::new(2, 8)..Point::new(2, 9), "")], + Some(AutoindentMode::Independent), + cx, + ); assert_eq!(buffer.text(), "fn a() {\n b()\n c\n}"); buffer @@ -656,20 +672,32 @@ fn test_autoindent_with_hard_tabs(cx: &mut MutableAppContext) { let text = "fn a() {}"; let mut buffer = Buffer::new(0, text, cx).with_language(Arc::new(rust_lang()), cx); - buffer.edit_with_autoindent([(8..8, "\n\n")], cx); + buffer.edit([(8..8, "\n\n")], Some(AutoindentMode::Independent), cx); assert_eq!(buffer.text(), "fn a() {\n\t\n}"); - buffer.edit_with_autoindent([(Point::new(1, 1)..Point::new(1, 1), "b()\n")], cx); + buffer.edit( + [(Point::new(1, 1)..Point::new(1, 1), "b()\n")], + Some(AutoindentMode::Independent), + cx, + ); assert_eq!(buffer.text(), "fn a() {\n\tb()\n\t\n}"); // Create a field expression on a new line, causing that line // to be indented. - buffer.edit_with_autoindent([(Point::new(2, 1)..Point::new(2, 1), ".c")], cx); + buffer.edit( + [(Point::new(2, 1)..Point::new(2, 1), ".c")], + Some(AutoindentMode::Independent), + cx, + ); assert_eq!(buffer.text(), "fn a() {\n\tb()\n\t\t.c\n}"); // Remove the dot so that the line is no longer a field expression, // causing the line to be outdented. - buffer.edit_with_autoindent([(Point::new(2, 2)..Point::new(2, 3), "")], cx); + buffer.edit( + [(Point::new(2, 2)..Point::new(2, 3), "")], + Some(AutoindentMode::Independent), + cx, + ); assert_eq!(buffer.text(), "fn a() {\n\tb()\n\tc\n}"); buffer @@ -694,11 +722,12 @@ fn test_autoindent_does_not_adjust_lines_with_unchanged_suggestion(cx: &mut Muta // Lines 2 and 3 don't match the indentation suggestion. When editing these lines, // their indentation is not adjusted. - buffer.edit_with_autoindent( + buffer.edit( [ (empty(Point::new(1, 1)), "()"), (empty(Point::new(2, 1)), "()"), ], + Some(AutoindentMode::Independent), cx, ); assert_eq!( @@ -714,11 +743,12 @@ fn test_autoindent_does_not_adjust_lines_with_unchanged_suggestion(cx: &mut Muta // When appending new content after these lines, the indentation is based on the // preceding lines' actual indentation. - buffer.edit_with_autoindent( + buffer.edit( [ (empty(Point::new(1, 1)), "\n.f\n.g"), (empty(Point::new(2, 1)), "\n.f\n.g"), ], + Some(AutoindentMode::Independent), cx, ); assert_eq!( @@ -751,7 +781,11 @@ fn test_autoindent_does_not_adjust_lines_with_unchanged_suggestion(cx: &mut Muta let mut buffer = Buffer::new(0, text, cx).with_language(Arc::new(rust_lang()), cx); // Delete a closing curly brace changes the suggested indent for the line. - buffer.edit_with_autoindent([(Point::new(3, 4)..Point::new(3, 5), "")], cx); + buffer.edit( + [(Point::new(3, 4)..Point::new(3, 5), "")], + Some(AutoindentMode::Independent), + cx, + ); assert_eq!( buffer.text(), " @@ -767,7 +801,11 @@ fn test_autoindent_does_not_adjust_lines_with_unchanged_suggestion(cx: &mut Muta ); // Manually editing the leading whitespace - buffer.edit_with_autoindent([(Point::new(3, 0)..Point::new(3, 12), "")], cx); + buffer.edit( + [(Point::new(3, 0)..Point::new(3, 12), "")], + Some(AutoindentMode::Independent), + cx, + ); assert_eq!( buffer.text(), " @@ -795,7 +833,7 @@ fn test_autoindent_adjusts_lines_when_only_text_changes(cx: &mut MutableAppConte let mut buffer = Buffer::new(0, text, cx).with_language(Arc::new(rust_lang()), cx); - buffer.edit_with_autoindent([(5..5, "\nb")], cx); + buffer.edit([(5..5, "\nb")], Some(AutoindentMode::Independent), cx); assert_eq!( buffer.text(), " @@ -807,7 +845,11 @@ fn test_autoindent_adjusts_lines_when_only_text_changes(cx: &mut MutableAppConte // The indentation suggestion changed because `@end` node (a close paren) // is now at the beginning of the line. - buffer.edit_with_autoindent([(Point::new(1, 4)..Point::new(1, 5), "")], cx); + buffer.edit( + [(Point::new(1, 4)..Point::new(1, 5), "")], + Some(AutoindentMode::Independent), + cx, + ); assert_eq!( buffer.text(), " @@ -827,7 +869,11 @@ fn test_autoindent_with_edit_at_end_of_buffer(cx: &mut MutableAppContext) { cx.add_model(|cx| { let text = "a\nb"; let mut buffer = Buffer::new(0, text, cx).with_language(Arc::new(rust_lang()), cx); - buffer.edit_with_autoindent([(0..1, "\n"), (2..3, "\n")], cx); + buffer.edit( + [(0..1, "\n"), (2..3, "\n")], + Some(AutoindentMode::Independent), + cx, + ); assert_eq!(buffer.text(), "\n\n\n"); buffer }); @@ -848,8 +894,9 @@ fn test_autoindent_multi_line_insertion(cx: &mut MutableAppContext) { .unindent(); let mut buffer = Buffer::new(0, text, cx).with_language(Arc::new(rust_lang()), cx); - buffer.edit_with_autoindent( + buffer.edit( [(Point::new(3, 0)..Point::new(3, 0), "e(\n f()\n);\n")], + Some(AutoindentMode::Independent), cx, ); assert_eq!( @@ -896,8 +943,9 @@ fn test_autoindent_preserves_relative_indentation_in_multi_line_insertion( .unindent(); // insert at the beginning of a line - buffer.edit_with_autoindent( + buffer.edit( [(Point::new(2, 0)..Point::new(2, 0), pasted_text.clone())], + Some(AutoindentMode::Block), cx, ); assert_eq!( @@ -941,7 +989,11 @@ fn test_autoindent_language_without_indents_query(cx: &mut MutableAppContext) { )), cx, ); - buffer.edit_with_autoindent([(Point::new(3, 0)..Point::new(3, 0), "\n")], cx); + buffer.edit( + [(Point::new(3, 0)..Point::new(3, 0), "\n")], + Some(AutoindentMode::Independent), + cx, + ); assert_eq!( buffer.text(), " @@ -963,18 +1015,18 @@ fn test_serialization(cx: &mut gpui::MutableAppContext) { let buffer1 = cx.add_model(|cx| { let mut buffer = Buffer::new(0, "abc", cx); - buffer.edit([(3..3, "D")], cx); + buffer.edit([(3..3, "D")], None, cx); now += Duration::from_secs(1); buffer.start_transaction_at(now); - buffer.edit([(4..4, "E")], cx); + buffer.edit([(4..4, "E")], None, cx); buffer.end_transaction_at(now, cx); assert_eq!(buffer.text(), "abcDE"); buffer.undo(cx); assert_eq!(buffer.text(), "abcD"); - buffer.edit([(4..4, "F")], cx); + buffer.edit([(4..4, "F")], None, cx); assert_eq!(buffer.text(), "abcDF"); buffer }); diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 29106b5889..898dbb5a2f 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -3168,7 +3168,7 @@ impl Project { buffer.finalize_last_transaction(); buffer.start_transaction(); for (range, text) in edits { - buffer.edit([(range, text)], cx); + buffer.edit([(range, text)], None, cx); } if buffer.end_transaction(cx).is_some() { let transaction = buffer.finalize_last_transaction().unwrap().clone(); @@ -3663,7 +3663,7 @@ impl Project { buffer.finalize_last_transaction(); buffer.start_transaction(); for (range, text) in edits { - buffer.edit([(range, text)], cx); + buffer.edit([(range, text)], None, cx); } let transaction = if buffer.end_transaction(cx).is_some() { let transaction = buffer.finalize_last_transaction().unwrap().clone(); @@ -4023,7 +4023,7 @@ impl Project { buffer.finalize_last_transaction(); buffer.start_transaction(); for (range, text) in edits { - buffer.edit([(range, text)], cx); + buffer.edit([(range, text)], None, cx); } let transaction = if buffer.end_transaction(cx).is_some() { let transaction = buffer.finalize_last_transaction().unwrap().clone(); diff --git a/crates/project/src/project_tests.rs b/crates/project/src/project_tests.rs index e36bd2c75a..4c5e9ef8e1 100644 --- a/crates/project/src/project_tests.rs +++ b/crates/project/src/project_tests.rs @@ -169,7 +169,7 @@ async fn test_managing_language_servers( }); // Edit a buffer. The changes are reported to the language server. - rust_buffer.update(cx, |buffer, cx| buffer.edit([(16..16, "2")], cx)); + rust_buffer.update(cx, |buffer, cx| buffer.edit([(16..16, "2")], None, cx)); assert_eq!( fake_rust_server .receive_notification::() @@ -226,8 +226,10 @@ async fn test_managing_language_servers( }); // Changes are reported only to servers matching the buffer's language. - toml_buffer.update(cx, |buffer, cx| buffer.edit([(5..5, "23")], cx)); - rust_buffer2.update(cx, |buffer, cx| buffer.edit([(0..0, "let x = 1;")], cx)); + toml_buffer.update(cx, |buffer, cx| buffer.edit([(5..5, "23")], None, cx)); + rust_buffer2.update(cx, |buffer, cx| { + buffer.edit([(0..0, "let x = 1;")], None, cx) + }); assert_eq!( fake_rust_server .receive_notification::() @@ -348,7 +350,7 @@ async fn test_managing_language_servers( }); // The renamed file's version resets after changing language server. - rust_buffer2.update(cx, |buffer, cx| buffer.edit([(0..0, "// ")], cx)); + rust_buffer2.update(cx, |buffer, cx| buffer.edit([(0..0, "// ")], None, cx)); assert_eq!( fake_json_server .receive_notification::() @@ -972,7 +974,7 @@ async fn test_transforming_diagnostics(cx: &mut gpui::TestAppContext) { .await; // Edit the buffer, moving the content down - buffer.update(cx, |buffer, cx| buffer.edit([(0..0, "\n\n")], cx)); + buffer.update(cx, |buffer, cx| buffer.edit([(0..0, "\n\n")], None, cx)); let change_notification_1 = fake_server .receive_notification::() .await; @@ -1137,9 +1139,13 @@ async fn test_transforming_diagnostics(cx: &mut gpui::TestAppContext) { // Keep editing the buffer and ensure disk-based diagnostics get translated according to the // changes since the last save. buffer.update(cx, |buffer, cx| { - buffer.edit([(Point::new(2, 0)..Point::new(2, 0), " ")], cx); - buffer.edit([(Point::new(2, 8)..Point::new(2, 10), "(x: usize)")], cx); - buffer.edit([(Point::new(3, 10)..Point::new(3, 10), "xxx")], cx); + buffer.edit([(Point::new(2, 0)..Point::new(2, 0), " ")], None, cx); + buffer.edit( + [(Point::new(2, 8)..Point::new(2, 10), "(x: usize)")], + None, + cx, + ); + buffer.edit([(Point::new(3, 10)..Point::new(3, 10), "xxx")], None, cx); }); let change_notification_2 = fake_server .receive_notification::() @@ -1330,6 +1336,7 @@ async fn test_edits_from_lsp_with_past_version(cx: &mut gpui::TestAppContext) { Point::new(0, 0)..Point::new(0, 0), "// above first function\n", )], + None, cx, ); buffer.edit( @@ -1337,6 +1344,7 @@ async fn test_edits_from_lsp_with_past_version(cx: &mut gpui::TestAppContext) { Point::new(2, 0)..Point::new(2, 0), " // inside first function\n", )], + None, cx, ); buffer.edit( @@ -1344,6 +1352,7 @@ async fn test_edits_from_lsp_with_past_version(cx: &mut gpui::TestAppContext) { Point::new(6, 4)..Point::new(6, 4), "// inside second function ", )], + None, cx, ); @@ -1405,7 +1414,7 @@ async fn test_edits_from_lsp_with_past_version(cx: &mut gpui::TestAppContext) { buffer.update(cx, |buffer, cx| { for (range, new_text) in edits { - buffer.edit([(range, new_text)], cx); + buffer.edit([(range, new_text)], None, cx); } assert_eq!( buffer.text(), @@ -1517,7 +1526,7 @@ async fn test_edits_from_lsp_with_edits_on_adjacent_lines(cx: &mut gpui::TestApp ); for (range, new_text) in edits { - buffer.edit([(range, new_text)], cx); + buffer.edit([(range, new_text)], None, cx); } assert_eq!( buffer.text(), @@ -1620,7 +1629,7 @@ async fn test_invalid_edits_from_lsp(cx: &mut gpui::TestAppContext) { ); for (range, new_text) in edits { - buffer.edit([(range, new_text)], cx); + buffer.edit([(range, new_text)], None, cx); } assert_eq!( buffer.text(), @@ -2025,7 +2034,7 @@ async fn test_save_file(cx: &mut gpui::TestAppContext) { buffer .update(cx, |buffer, cx| { assert_eq!(buffer.text(), "the old contents"); - buffer.edit([(0..0, "a line of text.\n".repeat(10 * 1024))], cx); + buffer.edit([(0..0, "a line of text.\n".repeat(10 * 1024))], None, cx); buffer.save(cx) }) .await @@ -2053,7 +2062,7 @@ async fn test_save_in_single_file_worktree(cx: &mut gpui::TestAppContext) { .unwrap(); buffer .update(cx, |buffer, cx| { - buffer.edit([(0..0, "a line of text.\n".repeat(10 * 1024))], cx); + buffer.edit([(0..0, "a line of text.\n".repeat(10 * 1024))], None, cx); buffer.save(cx) }) .await @@ -2073,7 +2082,7 @@ async fn test_save_as(cx: &mut gpui::TestAppContext) { project.create_buffer("", None, cx).unwrap() }); buffer.update(cx, |buffer, cx| { - buffer.edit([(0..0, "abc")], cx); + buffer.edit([(0..0, "abc")], None, cx); assert!(buffer.is_dirty()); assert!(!buffer.has_conflict()); }); @@ -2329,7 +2338,7 @@ async fn test_buffer_is_dirty(cx: &mut gpui::TestAppContext) { assert!(!buffer.is_dirty()); assert!(events.borrow().is_empty()); - buffer.edit([(1..2, "")], cx); + buffer.edit([(1..2, "")], None, cx); }); // after the first edit, the buffer is dirty, and emits a dirtied event. @@ -2356,8 +2365,8 @@ async fn test_buffer_is_dirty(cx: &mut gpui::TestAppContext) { assert_eq!(*events.borrow(), &[language::Event::Saved]); events.borrow_mut().clear(); - buffer.edit([(1..1, "B")], cx); - buffer.edit([(2..2, "D")], cx); + buffer.edit([(1..1, "B")], None, cx); + buffer.edit([(2..2, "D")], None, cx); }); // after editing again, the buffer is dirty, and emits another dirty event. @@ -2376,7 +2385,7 @@ async fn test_buffer_is_dirty(cx: &mut gpui::TestAppContext) { // After restoring the buffer to its previously-saved state, // the buffer is not considered dirty anymore. - buffer.edit([(1..3, "")], cx); + buffer.edit([(1..3, "")], None, cx); assert!(buffer.text() == "ac"); assert!(!buffer.is_dirty()); }); @@ -2427,7 +2436,7 @@ async fn test_buffer_is_dirty(cx: &mut gpui::TestAppContext) { }); buffer3.update(cx, |buffer, cx| { - buffer.edit([(0..0, "x")], cx); + buffer.edit([(0..0, "x")], None, cx); }); events.borrow_mut().clear(); fs.remove_file("/dir/file3".as_ref(), Default::default()) @@ -2495,7 +2504,7 @@ async fn test_buffer_file_changes_on_disk(cx: &mut gpui::TestAppContext) { // Modify the buffer buffer.update(cx, |buffer, cx| { - buffer.edit([(0..0, " ")], cx); + buffer.edit([(0..0, " ")], None, cx); assert!(buffer.is_dirty()); assert!(!buffer.has_conflict()); }); @@ -2986,7 +2995,7 @@ async fn test_search(cx: &mut gpui::TestAppContext) { .unwrap(); buffer_4.update(cx, |buffer, cx| { let text = "two::TWO"; - buffer.edit([(20..28, text), (31..43, text)], cx); + buffer.edit([(20..28, text), (31..43, text)], None, cx); }); assert_eq!( diff --git a/crates/search/src/buffer_search.rs b/crates/search/src/buffer_search.rs index 529da6f7b6..52631e71b4 100644 --- a/crates/search/src/buffer_search.rs +++ b/crates/search/src/buffer_search.rs @@ -260,7 +260,7 @@ impl BufferSearchBar { self.query_editor.update(cx, |query_editor, cx| { query_editor.buffer().update(cx, |query_buffer, cx| { let len = query_buffer.len(cx); - query_buffer.edit([(0..len, query)], cx); + query_buffer.edit([(0..len, query)], None, cx); }); }); } diff --git a/crates/vim/src/normal.rs b/crates/vim/src/normal.rs index 65909702ba..8ec0fea9a9 100644 --- a/crates/vim/src/normal.rs +++ b/crates/vim/src/normal.rs @@ -13,7 +13,7 @@ use change::init as change_init; use collections::HashSet; use editor::{Autoscroll, Bias, ClipboardSelection, DisplayPoint}; use gpui::{actions, MutableAppContext, ViewContext}; -use language::{Point, SelectionGoal}; +use language::{AutoindentMode, Point, SelectionGoal}; use workspace::Workspace; use self::{change::change_over, delete::delete_over, yank::yank_over}; @@ -278,7 +278,7 @@ fn paste(_: &mut Workspace, _: &Paste, cx: &mut ViewContext) { } } drop(snapshot); - buffer.edit_with_autoindent(edits, cx); + buffer.edit(edits, Some(AutoindentMode::Independent), cx); }); editor.change_selections(Some(Autoscroll::Fit), cx, |s| { diff --git a/crates/vim/src/visual.rs b/crates/vim/src/visual.rs index fedb999cad..a81ac1c455 100644 --- a/crates/vim/src/visual.rs +++ b/crates/vim/src/visual.rs @@ -3,7 +3,7 @@ use std::borrow::Cow; use collections::HashMap; use editor::{display_map::ToDisplayPoint, Autoscroll, Bias, ClipboardSelection}; use gpui::{actions, MutableAppContext, ViewContext}; -use language::SelectionGoal; +use language::{AutoindentMode, SelectionGoal}; use workspace::Workspace; use crate::{motion::Motion, state::Mode, utils::copy_selections_content, Vim}; @@ -254,7 +254,7 @@ pub fn paste(_: &mut Workspace, _: &VisualPaste, cx: &mut ViewContext } } drop(snapshot); - buffer.edit_with_autoindent(edits, cx); + buffer.edit(edits, Some(AutoindentMode::Independent), cx); }); editor.change_selections(Some(Autoscroll::Fit), cx, |s| { diff --git a/crates/zed/src/languages/c.rs b/crates/zed/src/languages/c.rs index 71fde5bd5e..074f51bc8f 100644 --- a/crates/zed/src/languages/c.rs +++ b/crates/zed/src/languages/c.rs @@ -249,7 +249,7 @@ impl super::LspAdapter for CLspAdapter { #[cfg(test)] mod tests { use gpui::MutableAppContext; - use language::Buffer; + use language::{AutoindentMode, Buffer}; use settings::Settings; use std::sync::Arc; @@ -265,21 +265,25 @@ mod tests { let mut buffer = Buffer::new(0, "", cx).with_language(Arc::new(language), cx); // empty function - buffer.edit_with_autoindent([(0..0, "int main() {}")], cx); + buffer.edit([(0..0, "int main() {}")], None, cx); // indent inside braces let ix = buffer.len() - 1; - buffer.edit_with_autoindent([(ix..ix, "\n\n")], cx); + buffer.edit([(ix..ix, "\n\n")], Some(AutoindentMode::Independent), cx); assert_eq!(buffer.text(), "int main() {\n \n}"); // indent body of single-statement if statement let ix = buffer.len() - 2; - buffer.edit_with_autoindent([(ix..ix, "if (a)\nb;")], cx); + buffer.edit( + [(ix..ix, "if (a)\nb;")], + Some(AutoindentMode::Independent), + cx, + ); assert_eq!(buffer.text(), "int main() {\n if (a)\n b;\n}"); // indent inside field expression let ix = buffer.len() - 3; - buffer.edit_with_autoindent([(ix..ix, "\n.c")], cx); + buffer.edit([(ix..ix, "\n.c")], Some(AutoindentMode::Independent), cx); assert_eq!(buffer.text(), "int main() {\n if (a)\n b\n .c;\n}"); buffer diff --git a/crates/zed/src/languages/python.rs b/crates/zed/src/languages/python.rs index 644d9ece40..f76b4e0fba 100644 --- a/crates/zed/src/languages/python.rs +++ b/crates/zed/src/languages/python.rs @@ -147,7 +147,7 @@ impl LspAdapter for PythonLspAdapter { #[cfg(test)] mod tests { use gpui::{ModelContext, MutableAppContext}; - use language::Buffer; + use language::{AutoindentMode, Buffer}; use settings::Settings; use std::sync::Arc; @@ -163,7 +163,7 @@ mod tests { let mut buffer = Buffer::new(0, "", cx).with_language(Arc::new(language), cx); let append = |buffer: &mut Buffer, text: &str, cx: &mut ModelContext| { let ix = buffer.len(); - buffer.edit_with_autoindent([(ix..ix, text)], cx); + buffer.edit([(ix..ix, text)], Some(AutoindentMode::Independent), cx); }; // indent after "def():" @@ -207,7 +207,11 @@ mod tests { // dedent the closing paren if it is shifted to the beginning of the line let argument_ix = buffer.text().find("1").unwrap(); - buffer.edit_with_autoindent([(argument_ix..argument_ix + 1, "")], cx); + buffer.edit( + [(argument_ix..argument_ix + 1, "")], + Some(AutoindentMode::Independent), + cx, + ); assert_eq!( buffer.text(), "def a():\n \n if a:\n b()\n else:\n foo(\n )" @@ -222,7 +226,11 @@ mod tests { // manually outdent the last line let end_whitespace_ix = buffer.len() - 4; - buffer.edit_with_autoindent([(end_whitespace_ix..buffer.len(), "")], cx); + buffer.edit( + [(end_whitespace_ix..buffer.len(), "")], + Some(AutoindentMode::Independent), + cx, + ); assert_eq!( buffer.text(), "def a():\n \n if a:\n b()\n else:\n foo(\n )\n" @@ -236,7 +244,7 @@ mod tests { ); // reset to a simple if statement - buffer.edit([(0..buffer.len(), "if a:\n b(\n )")], cx); + buffer.edit([(0..buffer.len(), "if a:\n b(\n )")], None, cx); // dedent "else" on the line after a closing paren append(&mut buffer, "\n else:\n", cx); diff --git a/crates/zed/src/languages/rust.rs b/crates/zed/src/languages/rust.rs index 856986b1eb..8e804f0c7f 100644 --- a/crates/zed/src/languages/rust.rs +++ b/crates/zed/src/languages/rust.rs @@ -444,29 +444,29 @@ mod tests { // indent between braces buffer.set_text("fn a() {}", cx); let ix = buffer.len() - 1; - buffer.edit_with_autoindent([(ix..ix, "\n\n")], cx); + buffer.edit([(ix..ix, "\n\n")], Some(AutoindentMode::Independent), cx); assert_eq!(buffer.text(), "fn a() {\n \n}"); // indent between braces, even after empty lines buffer.set_text("fn a() {\n\n\n}", cx); let ix = buffer.len() - 2; - buffer.edit_with_autoindent([(ix..ix, "\n")], cx); + buffer.edit([(ix..ix, "\n")], Some(AutoindentMode::Independent), cx); assert_eq!(buffer.text(), "fn a() {\n\n\n \n}"); // indent a line that continues a field expression buffer.set_text("fn a() {\n \n}", cx); let ix = buffer.len() - 2; - buffer.edit_with_autoindent([(ix..ix, "b\n.c")], cx); + buffer.edit([(ix..ix, "b\n.c")], Some(AutoindentMode::Independent), cx); assert_eq!(buffer.text(), "fn a() {\n b\n .c\n}"); // indent further lines that continue the field expression, even after empty lines let ix = buffer.len() - 2; - buffer.edit_with_autoindent([(ix..ix, "\n\n.d")], cx); + buffer.edit([(ix..ix, "\n\n.d")], Some(AutoindentMode::Independent), cx); assert_eq!(buffer.text(), "fn a() {\n b\n .c\n \n .d\n}"); // dedent the line after the field expression let ix = buffer.len() - 2; - buffer.edit_with_autoindent([(ix..ix, ";\ne")], cx); + buffer.edit([(ix..ix, ";\ne")], Some(AutoindentMode::Independent), cx); assert_eq!( buffer.text(), "fn a() {\n b\n .c\n \n .d;\n e\n}" @@ -475,17 +475,21 @@ mod tests { // indent inside a struct within a call buffer.set_text("const a: B = c(D {});", cx); let ix = buffer.len() - 3; - buffer.edit_with_autoindent([(ix..ix, "\n\n")], cx); + buffer.edit([(ix..ix, "\n\n")], Some(AutoindentMode::Independent), cx); assert_eq!(buffer.text(), "const a: B = c(D {\n \n});"); // indent further inside a nested call let ix = buffer.len() - 4; - buffer.edit_with_autoindent([(ix..ix, "e: f(\n\n)")], cx); + buffer.edit( + [(ix..ix, "e: f(\n\n)")], + Some(AutoindentMode::Independent), + cx, + ); assert_eq!(buffer.text(), "const a: B = c(D {\n e: f(\n \n )\n});"); // keep that indent after an empty line let ix = buffer.len() - 8; - buffer.edit_with_autoindent([(ix..ix, "\n")], cx); + buffer.edit([(ix..ix, "\n")], Some(AutoindentMode::Independent), cx); assert_eq!( buffer.text(), "const a: B = c(D {\n e: f(\n \n \n )\n});" From 2d05f906f1abc77a459790940174b382d6910f09 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Thu, 28 Jul 2022 17:29:42 -0700 Subject: [PATCH 29/42] Start work on adjusting pasted text based on old start column --- crates/language/src/buffer.rs | 89 +++++++++++++++++++++-------------- crates/language/src/tests.rs | 38 +++++++++++---- 2 files changed, 84 insertions(+), 43 deletions(-) diff --git a/crates/language/src/buffer.rs b/crates/language/src/buffer.rs index 8f0f7d40ec..ddacefec06 100644 --- a/crates/language/src/buffer.rs +++ b/crates/language/src/buffer.rs @@ -252,6 +252,8 @@ struct AutoindentRequestEntry { /// only be adjusted if the suggested indentation level has *changed* /// since the edit was made. first_line_is_new: bool, + /// The original indentation of the text that was inserted into this range. + original_indent: Option, } #[derive(Debug)] @@ -814,6 +816,8 @@ impl Buffer { Some(async move { let mut indent_sizes = BTreeMap::new(); for request in autoindent_requests { + // Resolve each edited range to its row in the current buffer and in the + // buffer before this batch of edits. let mut row_ranges = Vec::new(); let mut old_to_new_rows = BTreeMap::new(); for entry in &request.entries { @@ -824,11 +828,12 @@ impl Buffer { let old_row = position.to_point(&request.before_edit).row; old_to_new_rows.insert(old_row, new_row); } - if new_end_row > new_row { - row_ranges.push(new_row..new_end_row); - } + row_ranges.push((new_row..new_end_row, entry.original_indent)); } + // Build a map containing the suggested indentation for each of the edited lines + // with respect to the state of the buffer before these edits. This map is keyed + // by the rows for these lines in the current state of the buffer. let mut old_suggestions = BTreeMap::::default(); let old_edited_ranges = contiguous_ranges(old_to_new_rows.keys().copied(), max_rows_between_yields); @@ -856,17 +861,18 @@ impl Buffer { yield_now().await; } - // At this point, old_suggestions contains the suggested indentation for all edited lines - // with respect to the state of the buffer before the edit, but keyed by the row for these - // lines after the edits were applied. - + // In block mode, only compute indentation suggestions for the first line + // of each insertion. Otherwise, compute suggestions for every inserted line. let new_edited_row_ranges = contiguous_ranges( - row_ranges.iter().flat_map(|range| match request.mode { + row_ranges.iter().flat_map(|(range, _)| match request.mode { AutoindentMode::Block => range.start..range.start + 1, AutoindentMode::Independent => range.clone(), }), max_rows_between_yields, ); + + // Compute new suggestions for each line, but only include them in the result + // if they differ from the old suggestion for that line. for new_edited_row_range in new_edited_row_ranges { let suggestions = snapshot .suggest_autoindents(new_edited_row_range.clone()) @@ -894,34 +900,38 @@ impl Buffer { yield_now().await; } + // For each block of inserted text, adjust the indentation of the remaining + // lines of the block by the same amount as the first line was adjusted. if matches!(request.mode, AutoindentMode::Block) { - for row_range in row_ranges { - if row_range.len() > 1 { - if let Some(new_indent_size) = - indent_sizes.get(&row_range.start).copied() - { - let old_indent_size = - snapshot.indent_size_for_line(row_range.start); - if new_indent_size.kind == old_indent_size.kind { - let delta = - new_indent_size.len as i64 - old_indent_size.len as i64; - if delta != 0 { - for row in row_range.skip(1) { - indent_sizes.entry(row).or_insert_with(|| { - let mut size = snapshot.indent_size_for_line(row); - if size.kind == new_indent_size.kind { - if delta > 0 { - size.len += delta as u32; - } else if delta < 0 { - size.len = - size.len.saturating_sub(-delta as u32); - } - } - size - }); + for (row_range, original_indent) in + row_ranges + .into_iter() + .filter_map(|(range, original_indent)| { + if range.len() > 1 { + Some((range, original_indent?)) + } else { + None + } + }) + { + let new_indent = indent_sizes + .get(&row_range.start) + .copied() + .unwrap_or_else(|| snapshot.indent_size_for_line(row_range.start)); + let delta = new_indent.len as i64 - original_indent.len as i64; + if new_indent.kind == original_indent.kind && delta != 0 { + for row in row_range.skip(1) { + indent_sizes.entry(row).or_insert_with(|| { + let mut size = snapshot.indent_size_for_line(row); + if size.kind == new_indent.kind { + if delta > 0 { + size.len = size.len + delta as u32; + } else if delta < 0 { + size.len = size.len.saturating_sub(-delta as u32); } } - } + size + }); } } } @@ -1226,6 +1236,7 @@ impl Buffer { let mut range_of_insertion_to_indent = 0..new_text_len; let mut first_line_is_new = false; + let mut original_indent = None; // When inserting an entire line at the beginning of an existing line, // treat the insertion as new. @@ -1235,13 +1246,16 @@ impl Buffer { first_line_is_new = true; } - // Avoid auto-indenting lines before and after the insertion. + // When inserting text starting with a newline, avoid auto-indenting the + // previous line. if new_text[range_of_insertion_to_indent.clone()].starts_with('\n') { range_of_insertion_to_indent.start += 1; first_line_is_new = true; } + // Avoid auto-indenting before the insertion. if matches!(mode, AutoindentMode::Block) { + original_indent = Some(indent_size_for_text(new_text.chars())); if new_text[range_of_insertion_to_indent.clone()].ends_with('\n') { range_of_insertion_to_indent.end -= 1; } @@ -1249,6 +1263,7 @@ impl Buffer { AutoindentRequestEntry { first_line_is_new, + original_indent, range: self.anchor_before(new_start + range_of_insertion_to_indent.start) ..self.anchor_after(new_start + range_of_insertion_to_indent.end), } @@ -2144,8 +2159,12 @@ impl BufferSnapshot { } pub fn indent_size_for_line(text: &text::BufferSnapshot, row: u32) -> IndentSize { + indent_size_for_text(text.chars_at(Point::new(row, 0))) +} + +pub fn indent_size_for_text(text: impl Iterator) -> IndentSize { let mut result = IndentSize::spaces(0); - for c in text.chars_at(Point::new(row, 0)) { + for c in text { let kind = match c { ' ' => IndentKind::Space, '\t' => IndentKind::Tab, diff --git a/crates/language/src/tests.rs b/crates/language/src/tests.rs index acfaf5980d..0657e25604 100644 --- a/crates/language/src/tests.rs +++ b/crates/language/src/tests.rs @@ -920,20 +920,18 @@ fn test_autoindent_multi_line_insertion(cx: &mut MutableAppContext) { } #[gpui::test] -fn test_autoindent_preserves_relative_indentation_in_multi_line_insertion( - cx: &mut MutableAppContext, -) { +fn test_autoindent_block_mode(cx: &mut MutableAppContext) { cx.set_global(Settings::test(cx)); cx.add_model(|cx| { - let text = " + let text = r#" fn a() { b(); } - " + "# .unindent(); let mut buffer = Buffer::new(0, text, cx).with_language(Arc::new(rust_lang()), cx); - let pasted_text = r#" + let inserted_text = r#" " c d @@ -942,9 +940,33 @@ fn test_autoindent_preserves_relative_indentation_in_multi_line_insertion( "# .unindent(); - // insert at the beginning of a line + // Insert the block at column zero. The entire block is indented + // so that the first line matches the previous line's indentation. buffer.edit( - [(Point::new(2, 0)..Point::new(2, 0), pasted_text.clone())], + [(Point::new(2, 0)..Point::new(2, 0), inserted_text.clone())], + Some(AutoindentMode::Block), + cx, + ); + assert_eq!( + buffer.text(), + r#" + fn a() { + b(); + " + c + d + e + " + } + "# + .unindent() + ); + + // Insert the block at a deeper indent level. The entire block is outdented. + buffer.undo(cx); + buffer.edit([(Point::new(2, 0)..Point::new(2, 0), " ")], None, cx); + buffer.edit( + [(Point::new(2, 8)..Point::new(2, 8), inserted_text.clone())], Some(AutoindentMode::Block), cx, ); From 7a26fa18c7fee3fe031b991e18b55fd8f9c4eb1b Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Thu, 28 Jul 2022 18:09:24 -0700 Subject: [PATCH 30/42] Record start columns when writing to the clipboard from Zed --- crates/editor/src/editor.rs | 132 +++++++++++++++++++++++++++++- crates/editor/src/multi_buffer.rs | 51 +++++++++--- crates/language/src/buffer.rs | 63 +++++++------- crates/language/src/tests.rs | 8 +- crates/vim/src/utils.rs | 5 ++ 5 files changed, 214 insertions(+), 45 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 4543aa3b27..1f65451a83 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -879,6 +879,7 @@ struct ActiveDiagnosticGroup { pub struct ClipboardSelection { pub len: usize, pub is_entire_line: bool, + pub first_line_indent: u32, } #[derive(Debug)] @@ -1926,7 +1927,7 @@ impl Editor { old_selections .iter() .map(|s| (s.start..s.end, text.clone())), - Some(AutoindentMode::Block), + Some(AutoindentMode::Independent), cx, ); anchors @@ -2368,7 +2369,7 @@ impl Editor { this.buffer.update(cx, |buffer, cx| { buffer.edit( ranges.iter().map(|range| (range.clone(), text)), - Some(AutoindentMode::Block), + Some(AutoindentMode::Independent), cx, ); }); @@ -3512,6 +3513,10 @@ impl Editor { clipboard_selections.push(ClipboardSelection { len, is_entire_line, + first_line_indent: cmp::min( + selection.start.column, + buffer.indent_size_for_line(selection.start.row).len, + ), }); } } @@ -3549,6 +3554,10 @@ impl Editor { clipboard_selections.push(ClipboardSelection { len, is_entire_line, + first_line_indent: cmp::min( + start.column, + buffer.indent_size_for_line(start.row).len, + ), }); } } @@ -3583,18 +3592,22 @@ impl Editor { let snapshot = buffer.read(cx); let mut start_offset = 0; let mut edits = Vec::new(); + let mut start_columns = Vec::new(); let line_mode = this.selections.line_mode; for (ix, selection) in old_selections.iter().enumerate() { let to_insert; let entire_line; + let start_column; if let Some(clipboard_selection) = clipboard_selections.get(ix) { let end_offset = start_offset + clipboard_selection.len; to_insert = &clipboard_text[start_offset..end_offset]; entire_line = clipboard_selection.is_entire_line; start_offset = end_offset; + start_column = clipboard_selection.first_line_indent; } else { to_insert = clipboard_text.as_str(); entire_line = all_selections_were_entire_line; + start_column = 0; } // If the corresponding selection was empty when this slice of the @@ -3610,9 +3623,10 @@ impl Editor { }; edits.push((range, to_insert)); + start_columns.push(start_column); } drop(snapshot); - buffer.edit(edits, Some(AutoindentMode::Block), cx); + buffer.edit(edits, Some(AutoindentMode::Block { start_columns }), cx); }); let selections = this.selections.all::(cx); @@ -8649,6 +8663,118 @@ mod tests { t|he lazy dog"}); } + #[gpui::test] + async fn test_paste_multiline(cx: &mut gpui::TestAppContext) { + let mut cx = EditorTestContext::new(cx).await; + let language = Arc::new(Language::new( + LanguageConfig::default(), + Some(tree_sitter_rust::language()), + )); + cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx)); + + // Cut an indented block, without the leading whitespace. + cx.set_state(indoc! {" + const a = ( + b(), + [c( + d, + e + )} + ); + "}); + cx.update_editor(|e, cx| e.cut(&Cut, cx)); + cx.assert_editor_state(indoc! {" + const a = ( + b(), + | + ); + "}); + + // Paste it at the same position. + cx.update_editor(|e, cx| e.paste(&Paste, cx)); + cx.assert_editor_state(indoc! {" + const a = ( + b(), + c( + d, + e + )| + ); + "}); + + // Paste it at a line with a lower indent level. + cx.update_editor(|e, cx| e.paste(&Paste, cx)); + cx.set_state(indoc! {" + | + const a = ( + b(), + ); + "}); + cx.update_editor(|e, cx| e.paste(&Paste, cx)); + cx.assert_editor_state(indoc! {" + c( + d, + e + )| + const a = ( + b(), + ); + "}); + + // Cut an indented block, with the leading whitespace. + cx.set_state(indoc! {" + const a = ( + b(), + [ c( + d, + e + ) + }); + "}); + cx.update_editor(|e, cx| e.cut(&Cut, cx)); + cx.assert_editor_state(indoc! {" + const a = ( + b(), + |); + "}); + + // Paste it at the same position. + cx.update_editor(|e, cx| e.paste(&Paste, cx)); + cx.assert_editor_state(indoc! {" + const a = ( + b(), + c( + d, + e + ) + |); + "}); + + // Paste it at a line with a higher indent level. + cx.set_state(indoc! {" + const a = ( + b(), + c( + d, + e| + ) + ); + "}); + cx.update_editor(|e, cx| e.paste(&Paste, cx)); + cx.set_state(indoc! {" + const a = ( + b(), + c( + d, + ec( + d, + e + )| + ) + ); + "}); + } + #[gpui::test] fn test_select_all(cx: &mut gpui::MutableAppContext) { cx.set_global(Settings::test(cx)); diff --git a/crates/editor/src/multi_buffer.rs b/crates/editor/src/multi_buffer.rs index a12285dbbe..be9972c3d5 100644 --- a/crates/editor/src/multi_buffer.rs +++ b/crates/editor/src/multi_buffer.rs @@ -305,7 +305,7 @@ impl MultiBuffer { pub fn edit( &mut self, edits: I, - autoindent_mode: Option, + mut autoindent_mode: Option, cx: &mut ModelContext, ) where I: IntoIterator, T)>, @@ -331,11 +331,17 @@ impl MultiBuffer { }); } - let mut buffer_edits: HashMap, Arc, bool)>> = + let indent_start_columns = match &mut autoindent_mode { + Some(AutoindentMode::Block { start_columns }) => mem::take(start_columns), + _ => Default::default(), + }; + + let mut buffer_edits: HashMap, Arc, bool, u32)>> = Default::default(); let mut cursor = snapshot.excerpts.cursor::(); - for (range, new_text) in edits { + for (ix, (range, new_text)) in edits.enumerate() { let new_text: Arc = new_text.into(); + let start_column = indent_start_columns.get(ix).copied().unwrap_or(0); cursor.seek(&range.start, Bias::Right, &()); if cursor.item().is_none() && range.start == *cursor.start() { cursor.prev(&()); @@ -366,7 +372,7 @@ impl MultiBuffer { buffer_edits .entry(start_excerpt.buffer_id) .or_insert(Vec::new()) - .push((buffer_start..buffer_end, new_text, true)); + .push((buffer_start..buffer_end, new_text, true, start_column)); } else { let start_excerpt_range = buffer_start ..start_excerpt @@ -383,11 +389,11 @@ impl MultiBuffer { buffer_edits .entry(start_excerpt.buffer_id) .or_insert(Vec::new()) - .push((start_excerpt_range, new_text.clone(), true)); + .push((start_excerpt_range, new_text.clone(), true, start_column)); buffer_edits .entry(end_excerpt.buffer_id) .or_insert(Vec::new()) - .push((end_excerpt_range, new_text.clone(), false)); + .push((end_excerpt_range, new_text.clone(), false, start_column)); cursor.seek(&range.start, Bias::Right, &()); cursor.next(&()); @@ -402,6 +408,7 @@ impl MultiBuffer { excerpt.range.context.to_offset(&excerpt.buffer), new_text.clone(), false, + start_column, )); cursor.next(&()); } @@ -409,19 +416,21 @@ impl MultiBuffer { } for (buffer_id, mut edits) in buffer_edits { - edits.sort_unstable_by_key(|(range, _, _)| range.start); + edits.sort_unstable_by_key(|(range, _, _, _)| range.start); self.buffers.borrow()[&buffer_id] .buffer .update(cx, |buffer, cx| { let mut edits = edits.into_iter().peekable(); let mut insertions = Vec::new(); + let mut insertion_start_columns = Vec::new(); let mut deletions = Vec::new(); let empty_str: Arc = "".into(); - while let Some((mut range, new_text, mut is_insertion)) = edits.next() { - while let Some((next_range, _, next_is_insertion)) = edits.peek() { + while let Some((mut range, new_text, mut is_insertion, start_column)) = + edits.next() + { + while let Some((next_range, _, next_is_insertion, _)) = edits.peek() { if range.end >= next_range.start { range.end = cmp::max(next_range.end, range.end); - is_insertion |= *next_is_insertion; edits.next(); } else { @@ -430,6 +439,7 @@ impl MultiBuffer { } if is_insertion { + insertion_start_columns.push(start_column); insertions.push(( buffer.anchor_before(range.start)..buffer.anchor_before(range.end), new_text.clone(), @@ -442,8 +452,25 @@ impl MultiBuffer { } } - buffer.edit(deletions, autoindent_mode, cx); - buffer.edit(insertions, autoindent_mode, cx); + let deletion_autoindent_mode = + if let Some(AutoindentMode::Block { .. }) = autoindent_mode { + Some(AutoindentMode::Block { + start_columns: Default::default(), + }) + } else { + None + }; + let insertion_autoindent_mode = + if let Some(AutoindentMode::Block { .. }) = autoindent_mode { + Some(AutoindentMode::Block { + start_columns: insertion_start_columns, + }) + } else { + None + }; + + buffer.edit(deletions, deletion_autoindent_mode, cx); + buffer.edit(insertions, insertion_autoindent_mode, cx); }) } } diff --git a/crates/language/src/buffer.rs b/crates/language/src/buffer.rs index ddacefec06..b2d45717f0 100644 --- a/crates/language/src/buffer.rs +++ b/crates/language/src/buffer.rs @@ -229,9 +229,9 @@ struct SyntaxTree { version: clock::Global, } -#[derive(Clone, Copy)] +#[derive(Clone, Debug)] pub enum AutoindentMode { - Block, + Block { start_columns: Vec }, Independent, } @@ -240,7 +240,7 @@ struct AutoindentRequest { before_edit: BufferSnapshot, entries: Vec, indent_size: IndentSize, - mode: AutoindentMode, + is_block_mode: bool, } #[derive(Clone)] @@ -252,8 +252,7 @@ struct AutoindentRequestEntry { /// only be adjusted if the suggested indentation level has *changed* /// since the edit was made. first_line_is_new: bool, - /// The original indentation of the text that was inserted into this range. - original_indent: Option, + start_column: Option, } #[derive(Debug)] @@ -828,7 +827,7 @@ impl Buffer { let old_row = position.to_point(&request.before_edit).row; old_to_new_rows.insert(old_row, new_row); } - row_ranges.push((new_row..new_end_row, entry.original_indent)); + row_ranges.push((new_row..new_end_row, entry.start_column)); } // Build a map containing the suggested indentation for each of the edited lines @@ -864,9 +863,12 @@ impl Buffer { // In block mode, only compute indentation suggestions for the first line // of each insertion. Otherwise, compute suggestions for every inserted line. let new_edited_row_ranges = contiguous_ranges( - row_ranges.iter().flat_map(|(range, _)| match request.mode { - AutoindentMode::Block => range.start..range.start + 1, - AutoindentMode::Independent => range.clone(), + row_ranges.iter().flat_map(|(range, _)| { + if request.is_block_mode { + range.start..range.start + 1 + } else { + range.clone() + } }), max_rows_between_yields, ); @@ -902,24 +904,22 @@ impl Buffer { // For each block of inserted text, adjust the indentation of the remaining // lines of the block by the same amount as the first line was adjusted. - if matches!(request.mode, AutoindentMode::Block) { - for (row_range, original_indent) in - row_ranges - .into_iter() - .filter_map(|(range, original_indent)| { - if range.len() > 1 { - Some((range, original_indent?)) - } else { - None - } - }) + if request.is_block_mode { + for (row_range, start_column) in + row_ranges.into_iter().filter_map(|(range, start_column)| { + if range.len() > 1 { + Some((range, start_column?)) + } else { + None + } + }) { let new_indent = indent_sizes .get(&row_range.start) .copied() .unwrap_or_else(|| snapshot.indent_size_for_line(row_range.start)); - let delta = new_indent.len as i64 - original_indent.len as i64; - if new_indent.kind == original_indent.kind && delta != 0 { + let delta = new_indent.len as i64 - start_column as i64; + if delta != 0 { for row in row_range.skip(1) { indent_sizes.entry(row).or_insert_with(|| { let mut size = snapshot.indent_size_for_line(row); @@ -1223,12 +1223,17 @@ impl Buffer { } else { IndentSize::spaces(settings.tab_size(language_name.as_deref()).get()) }; + let (start_columns, is_block_mode) = match mode { + AutoindentMode::Block { start_columns } => (start_columns, true), + AutoindentMode::Independent => (Default::default(), false), + }; let mut delta = 0isize; let entries = edits .into_iter() + .enumerate() .zip(&edit_operation.as_edit().unwrap().new_text) - .map(|((range, _), new_text)| { + .map(|((ix, (range, _)), new_text)| { let new_text_len = new_text.len(); let old_start = range.start.to_point(&before_edit); let new_start = (delta + range.start as isize) as usize; @@ -1236,7 +1241,7 @@ impl Buffer { let mut range_of_insertion_to_indent = 0..new_text_len; let mut first_line_is_new = false; - let mut original_indent = None; + let mut start_column = None; // When inserting an entire line at the beginning of an existing line, // treat the insertion as new. @@ -1254,8 +1259,10 @@ impl Buffer { } // Avoid auto-indenting before the insertion. - if matches!(mode, AutoindentMode::Block) { - original_indent = Some(indent_size_for_text(new_text.chars())); + if is_block_mode { + start_column = start_columns + .get(ix) + .map(|start| start + indent_size_for_text(new_text.chars()).len); if new_text[range_of_insertion_to_indent.clone()].ends_with('\n') { range_of_insertion_to_indent.end -= 1; } @@ -1263,7 +1270,7 @@ impl Buffer { AutoindentRequestEntry { first_line_is_new, - original_indent, + start_column, range: self.anchor_before(new_start + range_of_insertion_to_indent.start) ..self.anchor_after(new_start + range_of_insertion_to_indent.end), } @@ -1274,7 +1281,7 @@ impl Buffer { before_edit, entries, indent_size, - mode, + is_block_mode, })); } diff --git a/crates/language/src/tests.rs b/crates/language/src/tests.rs index 0657e25604..bee320a932 100644 --- a/crates/language/src/tests.rs +++ b/crates/language/src/tests.rs @@ -944,7 +944,9 @@ fn test_autoindent_block_mode(cx: &mut MutableAppContext) { // so that the first line matches the previous line's indentation. buffer.edit( [(Point::new(2, 0)..Point::new(2, 0), inserted_text.clone())], - Some(AutoindentMode::Block), + Some(AutoindentMode::Block { + start_columns: vec![0], + }), cx, ); assert_eq!( @@ -967,7 +969,9 @@ fn test_autoindent_block_mode(cx: &mut MutableAppContext) { buffer.edit([(Point::new(2, 0)..Point::new(2, 0), " ")], None, cx); buffer.edit( [(Point::new(2, 8)..Point::new(2, 8), inserted_text.clone())], - Some(AutoindentMode::Block), + Some(AutoindentMode::Block { + start_columns: vec![0], + }), cx, ); assert_eq!( diff --git a/crates/vim/src/utils.rs b/crates/vim/src/utils.rs index cb6a736c63..75bb7445ff 100644 --- a/crates/vim/src/utils.rs +++ b/crates/vim/src/utils.rs @@ -1,5 +1,6 @@ use editor::{ClipboardSelection, Editor}; use gpui::{ClipboardItem, MutableAppContext}; +use std::cmp; pub fn copy_selections_content(editor: &mut Editor, linewise: bool, cx: &mut MutableAppContext) { let selections = editor.selections.all_adjusted(cx); @@ -17,6 +18,10 @@ pub fn copy_selections_content(editor: &mut Editor, linewise: bool, cx: &mut Mut clipboard_selections.push(ClipboardSelection { len: text.len() - initial_len, is_entire_line: linewise, + first_line_indent: cmp::min( + start.column, + buffer.indent_size_for_line(start.row).len, + ), }); } } From 868c46062008bb0bcab2d41a38b4295996b9b958 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Thu, 28 Jul 2022 22:34:56 -0700 Subject: [PATCH 31/42] :art: Rename and simplify some autoindent stuff --- crates/editor/src/editor.rs | 26 ++++++++--------- crates/editor/src/multi_buffer.rs | 47 +++++++++++++++++++++--------- crates/language/src/buffer.rs | 47 ++++++++++++++++++------------ crates/language/src/tests.rs | 40 ++++++++++++------------- crates/vim/src/normal.rs | 2 +- crates/vim/src/utils.rs | 6 +--- crates/vim/src/visual.rs | 2 +- crates/zed/src/languages/c.rs | 10 ++----- crates/zed/src/languages/python.rs | 6 ++-- crates/zed/src/languages/rust.rs | 20 +++++-------- 10 files changed, 112 insertions(+), 94 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 1f65451a83..819aa5ccd5 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -1476,7 +1476,7 @@ impl Editor { T: Into>, { self.buffer.update(cx, |buffer, cx| { - buffer.edit(edits, Some(AutoindentMode::Independent), cx) + buffer.edit(edits, Some(AutoindentMode::EachLine), cx) }); } @@ -1927,7 +1927,7 @@ impl Editor { old_selections .iter() .map(|s| (s.start..s.end, text.clone())), - Some(AutoindentMode::Independent), + Some(AutoindentMode::EachLine), cx, ); anchors @@ -2369,7 +2369,7 @@ impl Editor { this.buffer.update(cx, |buffer, cx| { buffer.edit( ranges.iter().map(|range| (range.clone(), text)), - Some(AutoindentMode::Independent), + Some(AutoindentMode::EachLine), cx, ); }); @@ -2737,7 +2737,7 @@ impl Editor { .iter() .cloned() .map(|range| (range, snippet_text.clone())), - Some(AutoindentMode::Independent), + Some(AutoindentMode::EachLine), cx, ); @@ -3513,10 +3513,7 @@ impl Editor { clipboard_selections.push(ClipboardSelection { len, is_entire_line, - first_line_indent: cmp::min( - selection.start.column, - buffer.indent_size_for_line(selection.start.row).len, - ), + first_line_indent: buffer.indent_size_for_line(selection.start.row).len, }); } } @@ -3554,10 +3551,7 @@ impl Editor { clipboard_selections.push(ClipboardSelection { len, is_entire_line, - first_line_indent: cmp::min( - start.column, - buffer.indent_size_for_line(start.row).len, - ), + first_line_indent: buffer.indent_size_for_line(start.row).len, }); } } @@ -3626,7 +3620,13 @@ impl Editor { start_columns.push(start_column); } drop(snapshot); - buffer.edit(edits, Some(AutoindentMode::Block { start_columns }), cx); + buffer.edit( + edits, + Some(AutoindentMode::Block { + original_indent_columns: start_columns, + }), + cx, + ); }); let selections = this.selections.all::(cx); diff --git a/crates/editor/src/multi_buffer.rs b/crates/editor/src/multi_buffer.rs index be9972c3d5..1fc7cf0560 100644 --- a/crates/editor/src/multi_buffer.rs +++ b/crates/editor/src/multi_buffer.rs @@ -331,8 +331,10 @@ impl MultiBuffer { }); } - let indent_start_columns = match &mut autoindent_mode { - Some(AutoindentMode::Block { start_columns }) => mem::take(start_columns), + let original_indent_columns = match &mut autoindent_mode { + Some(AutoindentMode::Block { + original_indent_columns, + }) => mem::take(original_indent_columns), _ => Default::default(), }; @@ -341,7 +343,7 @@ impl MultiBuffer { let mut cursor = snapshot.excerpts.cursor::(); for (ix, (range, new_text)) in edits.enumerate() { let new_text: Arc = new_text.into(); - let start_column = indent_start_columns.get(ix).copied().unwrap_or(0); + let original_indent_column = original_indent_columns.get(ix).copied().unwrap_or(0); cursor.seek(&range.start, Bias::Right, &()); if cursor.item().is_none() && range.start == *cursor.start() { cursor.prev(&()); @@ -372,7 +374,12 @@ impl MultiBuffer { buffer_edits .entry(start_excerpt.buffer_id) .or_insert(Vec::new()) - .push((buffer_start..buffer_end, new_text, true, start_column)); + .push(( + buffer_start..buffer_end, + new_text, + true, + original_indent_column, + )); } else { let start_excerpt_range = buffer_start ..start_excerpt @@ -389,11 +396,21 @@ impl MultiBuffer { buffer_edits .entry(start_excerpt.buffer_id) .or_insert(Vec::new()) - .push((start_excerpt_range, new_text.clone(), true, start_column)); + .push(( + start_excerpt_range, + new_text.clone(), + true, + original_indent_column, + )); buffer_edits .entry(end_excerpt.buffer_id) .or_insert(Vec::new()) - .push((end_excerpt_range, new_text.clone(), false, start_column)); + .push(( + end_excerpt_range, + new_text.clone(), + false, + original_indent_column, + )); cursor.seek(&range.start, Bias::Right, &()); cursor.next(&()); @@ -408,7 +425,7 @@ impl MultiBuffer { excerpt.range.context.to_offset(&excerpt.buffer), new_text.clone(), false, - start_column, + original_indent_column, )); cursor.next(&()); } @@ -422,11 +439,15 @@ impl MultiBuffer { .update(cx, |buffer, cx| { let mut edits = edits.into_iter().peekable(); let mut insertions = Vec::new(); - let mut insertion_start_columns = Vec::new(); + let mut original_indent_columns = Vec::new(); let mut deletions = Vec::new(); let empty_str: Arc = "".into(); - while let Some((mut range, new_text, mut is_insertion, start_column)) = - edits.next() + while let Some(( + mut range, + new_text, + mut is_insertion, + original_indent_column, + )) = edits.next() { while let Some((next_range, _, next_is_insertion, _)) = edits.peek() { if range.end >= next_range.start { @@ -439,7 +460,7 @@ impl MultiBuffer { } if is_insertion { - insertion_start_columns.push(start_column); + original_indent_columns.push(original_indent_column); insertions.push(( buffer.anchor_before(range.start)..buffer.anchor_before(range.end), new_text.clone(), @@ -455,7 +476,7 @@ impl MultiBuffer { let deletion_autoindent_mode = if let Some(AutoindentMode::Block { .. }) = autoindent_mode { Some(AutoindentMode::Block { - start_columns: Default::default(), + original_indent_columns: Default::default(), }) } else { None @@ -463,7 +484,7 @@ impl MultiBuffer { let insertion_autoindent_mode = if let Some(AutoindentMode::Block { .. }) = autoindent_mode { Some(AutoindentMode::Block { - start_columns: insertion_start_columns, + original_indent_columns, }) } else { None diff --git a/crates/language/src/buffer.rs b/crates/language/src/buffer.rs index b2d45717f0..e6b0d48820 100644 --- a/crates/language/src/buffer.rs +++ b/crates/language/src/buffer.rs @@ -231,8 +231,15 @@ struct SyntaxTree { #[derive(Clone, Debug)] pub enum AutoindentMode { - Block { start_columns: Vec }, - Independent, + /// Indent each line of inserted text. + EachLine, + /// Apply the same indentation adjustment to all of the lines + /// in a given insertion. + Block { + /// The original indentation level of the first line of each + /// insertion, if it has been copied. + original_indent_columns: Vec, + }, } #[derive(Clone)] @@ -252,7 +259,7 @@ struct AutoindentRequestEntry { /// only be adjusted if the suggested indentation level has *changed* /// since the edit was made. first_line_is_new: bool, - start_column: Option, + original_indent_column: Option, } #[derive(Debug)] @@ -827,7 +834,7 @@ impl Buffer { let old_row = position.to_point(&request.before_edit).row; old_to_new_rows.insert(old_row, new_row); } - row_ranges.push((new_row..new_end_row, entry.start_column)); + row_ranges.push((new_row..new_end_row, entry.original_indent_column)); } // Build a map containing the suggested indentation for each of the edited lines @@ -905,20 +912,22 @@ impl Buffer { // For each block of inserted text, adjust the indentation of the remaining // lines of the block by the same amount as the first line was adjusted. if request.is_block_mode { - for (row_range, start_column) in - row_ranges.into_iter().filter_map(|(range, start_column)| { - if range.len() > 1 { - Some((range, start_column?)) - } else { - None - } - }) + for (row_range, original_indent_column) in + row_ranges + .into_iter() + .filter_map(|(range, original_indent_column)| { + if range.len() > 1 { + Some((range, original_indent_column?)) + } else { + None + } + }) { let new_indent = indent_sizes .get(&row_range.start) .copied() .unwrap_or_else(|| snapshot.indent_size_for_line(row_range.start)); - let delta = new_indent.len as i64 - start_column as i64; + let delta = new_indent.len as i64 - original_indent_column as i64; if delta != 0 { for row in row_range.skip(1) { indent_sizes.entry(row).or_insert_with(|| { @@ -1224,8 +1233,10 @@ impl Buffer { IndentSize::spaces(settings.tab_size(language_name.as_deref()).get()) }; let (start_columns, is_block_mode) = match mode { - AutoindentMode::Block { start_columns } => (start_columns, true), - AutoindentMode::Independent => (Default::default(), false), + AutoindentMode::Block { + original_indent_columns: start_columns, + } => (start_columns, true), + AutoindentMode::EachLine => (Default::default(), false), }; let mut delta = 0isize; @@ -1260,9 +1271,7 @@ impl Buffer { // Avoid auto-indenting before the insertion. if is_block_mode { - start_column = start_columns - .get(ix) - .map(|start| start + indent_size_for_text(new_text.chars()).len); + start_column = start_columns.get(ix).copied(); if new_text[range_of_insertion_to_indent.clone()].ends_with('\n') { range_of_insertion_to_indent.end -= 1; } @@ -1270,7 +1279,7 @@ impl Buffer { AutoindentRequestEntry { first_line_is_new, - start_column, + original_indent_column: start_column, range: self.anchor_before(new_start + range_of_insertion_to_indent.start) ..self.anchor_after(new_start + range_of_insertion_to_indent.end), } diff --git a/crates/language/src/tests.rs b/crates/language/src/tests.rs index bee320a932..937ff06930 100644 --- a/crates/language/src/tests.rs +++ b/crates/language/src/tests.rs @@ -35,7 +35,7 @@ fn test_line_endings(cx: &mut gpui::MutableAppContext) { buffer.check_invariants(); buffer.edit( [(buffer.len()..buffer.len(), "\r\nfour")], - Some(AutoindentMode::Independent), + Some(AutoindentMode::EachLine), cx, ); buffer.edit([(0..0, "zero\r\n")], None, cx); @@ -630,12 +630,12 @@ fn test_autoindent_with_soft_tabs(cx: &mut MutableAppContext) { let text = "fn a() {}"; let mut buffer = Buffer::new(0, text, cx).with_language(Arc::new(rust_lang()), cx); - buffer.edit([(8..8, "\n\n")], Some(AutoindentMode::Independent), cx); + buffer.edit([(8..8, "\n\n")], Some(AutoindentMode::EachLine), cx); assert_eq!(buffer.text(), "fn a() {\n \n}"); buffer.edit( [(Point::new(1, 4)..Point::new(1, 4), "b()\n")], - Some(AutoindentMode::Independent), + Some(AutoindentMode::EachLine), cx, ); assert_eq!(buffer.text(), "fn a() {\n b()\n \n}"); @@ -644,7 +644,7 @@ fn test_autoindent_with_soft_tabs(cx: &mut MutableAppContext) { // to be indented. buffer.edit( [(Point::new(2, 4)..Point::new(2, 4), ".c")], - Some(AutoindentMode::Independent), + Some(AutoindentMode::EachLine), cx, ); assert_eq!(buffer.text(), "fn a() {\n b()\n .c\n}"); @@ -653,7 +653,7 @@ fn test_autoindent_with_soft_tabs(cx: &mut MutableAppContext) { // causing the line to be outdented. buffer.edit( [(Point::new(2, 8)..Point::new(2, 9), "")], - Some(AutoindentMode::Independent), + Some(AutoindentMode::EachLine), cx, ); assert_eq!(buffer.text(), "fn a() {\n b()\n c\n}"); @@ -672,12 +672,12 @@ fn test_autoindent_with_hard_tabs(cx: &mut MutableAppContext) { let text = "fn a() {}"; let mut buffer = Buffer::new(0, text, cx).with_language(Arc::new(rust_lang()), cx); - buffer.edit([(8..8, "\n\n")], Some(AutoindentMode::Independent), cx); + buffer.edit([(8..8, "\n\n")], Some(AutoindentMode::EachLine), cx); assert_eq!(buffer.text(), "fn a() {\n\t\n}"); buffer.edit( [(Point::new(1, 1)..Point::new(1, 1), "b()\n")], - Some(AutoindentMode::Independent), + Some(AutoindentMode::EachLine), cx, ); assert_eq!(buffer.text(), "fn a() {\n\tb()\n\t\n}"); @@ -686,7 +686,7 @@ fn test_autoindent_with_hard_tabs(cx: &mut MutableAppContext) { // to be indented. buffer.edit( [(Point::new(2, 1)..Point::new(2, 1), ".c")], - Some(AutoindentMode::Independent), + Some(AutoindentMode::EachLine), cx, ); assert_eq!(buffer.text(), "fn a() {\n\tb()\n\t\t.c\n}"); @@ -695,7 +695,7 @@ fn test_autoindent_with_hard_tabs(cx: &mut MutableAppContext) { // causing the line to be outdented. buffer.edit( [(Point::new(2, 2)..Point::new(2, 3), "")], - Some(AutoindentMode::Independent), + Some(AutoindentMode::EachLine), cx, ); assert_eq!(buffer.text(), "fn a() {\n\tb()\n\tc\n}"); @@ -727,7 +727,7 @@ fn test_autoindent_does_not_adjust_lines_with_unchanged_suggestion(cx: &mut Muta (empty(Point::new(1, 1)), "()"), (empty(Point::new(2, 1)), "()"), ], - Some(AutoindentMode::Independent), + Some(AutoindentMode::EachLine), cx, ); assert_eq!( @@ -748,7 +748,7 @@ fn test_autoindent_does_not_adjust_lines_with_unchanged_suggestion(cx: &mut Muta (empty(Point::new(1, 1)), "\n.f\n.g"), (empty(Point::new(2, 1)), "\n.f\n.g"), ], - Some(AutoindentMode::Independent), + Some(AutoindentMode::EachLine), cx, ); assert_eq!( @@ -783,7 +783,7 @@ fn test_autoindent_does_not_adjust_lines_with_unchanged_suggestion(cx: &mut Muta // Delete a closing curly brace changes the suggested indent for the line. buffer.edit( [(Point::new(3, 4)..Point::new(3, 5), "")], - Some(AutoindentMode::Independent), + Some(AutoindentMode::EachLine), cx, ); assert_eq!( @@ -803,7 +803,7 @@ fn test_autoindent_does_not_adjust_lines_with_unchanged_suggestion(cx: &mut Muta // Manually editing the leading whitespace buffer.edit( [(Point::new(3, 0)..Point::new(3, 12), "")], - Some(AutoindentMode::Independent), + Some(AutoindentMode::EachLine), cx, ); assert_eq!( @@ -833,7 +833,7 @@ fn test_autoindent_adjusts_lines_when_only_text_changes(cx: &mut MutableAppConte let mut buffer = Buffer::new(0, text, cx).with_language(Arc::new(rust_lang()), cx); - buffer.edit([(5..5, "\nb")], Some(AutoindentMode::Independent), cx); + buffer.edit([(5..5, "\nb")], Some(AutoindentMode::EachLine), cx); assert_eq!( buffer.text(), " @@ -847,7 +847,7 @@ fn test_autoindent_adjusts_lines_when_only_text_changes(cx: &mut MutableAppConte // is now at the beginning of the line. buffer.edit( [(Point::new(1, 4)..Point::new(1, 5), "")], - Some(AutoindentMode::Independent), + Some(AutoindentMode::EachLine), cx, ); assert_eq!( @@ -871,7 +871,7 @@ fn test_autoindent_with_edit_at_end_of_buffer(cx: &mut MutableAppContext) { let mut buffer = Buffer::new(0, text, cx).with_language(Arc::new(rust_lang()), cx); buffer.edit( [(0..1, "\n"), (2..3, "\n")], - Some(AutoindentMode::Independent), + Some(AutoindentMode::EachLine), cx, ); assert_eq!(buffer.text(), "\n\n\n"); @@ -896,7 +896,7 @@ fn test_autoindent_multi_line_insertion(cx: &mut MutableAppContext) { let mut buffer = Buffer::new(0, text, cx).with_language(Arc::new(rust_lang()), cx); buffer.edit( [(Point::new(3, 0)..Point::new(3, 0), "e(\n f()\n);\n")], - Some(AutoindentMode::Independent), + Some(AutoindentMode::EachLine), cx, ); assert_eq!( @@ -945,7 +945,7 @@ fn test_autoindent_block_mode(cx: &mut MutableAppContext) { buffer.edit( [(Point::new(2, 0)..Point::new(2, 0), inserted_text.clone())], Some(AutoindentMode::Block { - start_columns: vec![0], + original_indent_columns: vec![0], }), cx, ); @@ -970,7 +970,7 @@ fn test_autoindent_block_mode(cx: &mut MutableAppContext) { buffer.edit( [(Point::new(2, 8)..Point::new(2, 8), inserted_text.clone())], Some(AutoindentMode::Block { - start_columns: vec![0], + original_indent_columns: vec![0], }), cx, ); @@ -1017,7 +1017,7 @@ fn test_autoindent_language_without_indents_query(cx: &mut MutableAppContext) { ); buffer.edit( [(Point::new(3, 0)..Point::new(3, 0), "\n")], - Some(AutoindentMode::Independent), + Some(AutoindentMode::EachLine), cx, ); assert_eq!( diff --git a/crates/vim/src/normal.rs b/crates/vim/src/normal.rs index 13055a9df7..39663e0db4 100644 --- a/crates/vim/src/normal.rs +++ b/crates/vim/src/normal.rs @@ -278,7 +278,7 @@ fn paste(_: &mut Workspace, _: &Paste, cx: &mut ViewContext) { } } drop(snapshot); - buffer.edit(edits, Some(AutoindentMode::Independent), cx); + buffer.edit(edits, Some(AutoindentMode::EachLine), cx); }); editor.change_selections(Some(Autoscroll::Fit), cx, |s| { diff --git a/crates/vim/src/utils.rs b/crates/vim/src/utils.rs index 75bb7445ff..6f682f6146 100644 --- a/crates/vim/src/utils.rs +++ b/crates/vim/src/utils.rs @@ -1,6 +1,5 @@ use editor::{ClipboardSelection, Editor}; use gpui::{ClipboardItem, MutableAppContext}; -use std::cmp; pub fn copy_selections_content(editor: &mut Editor, linewise: bool, cx: &mut MutableAppContext) { let selections = editor.selections.all_adjusted(cx); @@ -18,10 +17,7 @@ pub fn copy_selections_content(editor: &mut Editor, linewise: bool, cx: &mut Mut clipboard_selections.push(ClipboardSelection { len: text.len() - initial_len, is_entire_line: linewise, - first_line_indent: cmp::min( - start.column, - buffer.indent_size_for_line(start.row).len, - ), + first_line_indent: buffer.indent_size_for_line(start.row).len, }); } } diff --git a/crates/vim/src/visual.rs b/crates/vim/src/visual.rs index d9772c031b..76fea2e205 100644 --- a/crates/vim/src/visual.rs +++ b/crates/vim/src/visual.rs @@ -254,7 +254,7 @@ pub fn paste(_: &mut Workspace, _: &VisualPaste, cx: &mut ViewContext } } drop(snapshot); - buffer.edit(edits, Some(AutoindentMode::Independent), cx); + buffer.edit(edits, Some(AutoindentMode::EachLine), cx); }); editor.change_selections(Some(Autoscroll::Fit), cx, |s| { diff --git a/crates/zed/src/languages/c.rs b/crates/zed/src/languages/c.rs index 074f51bc8f..a4db4b9a75 100644 --- a/crates/zed/src/languages/c.rs +++ b/crates/zed/src/languages/c.rs @@ -269,21 +269,17 @@ mod tests { // indent inside braces let ix = buffer.len() - 1; - buffer.edit([(ix..ix, "\n\n")], Some(AutoindentMode::Independent), cx); + buffer.edit([(ix..ix, "\n\n")], Some(AutoindentMode::EachLine), cx); assert_eq!(buffer.text(), "int main() {\n \n}"); // indent body of single-statement if statement let ix = buffer.len() - 2; - buffer.edit( - [(ix..ix, "if (a)\nb;")], - Some(AutoindentMode::Independent), - cx, - ); + buffer.edit([(ix..ix, "if (a)\nb;")], Some(AutoindentMode::EachLine), cx); assert_eq!(buffer.text(), "int main() {\n if (a)\n b;\n}"); // indent inside field expression let ix = buffer.len() - 3; - buffer.edit([(ix..ix, "\n.c")], Some(AutoindentMode::Independent), cx); + buffer.edit([(ix..ix, "\n.c")], Some(AutoindentMode::EachLine), cx); assert_eq!(buffer.text(), "int main() {\n if (a)\n b\n .c;\n}"); buffer diff --git a/crates/zed/src/languages/python.rs b/crates/zed/src/languages/python.rs index f76b4e0fba..801c7c96f9 100644 --- a/crates/zed/src/languages/python.rs +++ b/crates/zed/src/languages/python.rs @@ -163,7 +163,7 @@ mod tests { let mut buffer = Buffer::new(0, "", cx).with_language(Arc::new(language), cx); let append = |buffer: &mut Buffer, text: &str, cx: &mut ModelContext| { let ix = buffer.len(); - buffer.edit([(ix..ix, text)], Some(AutoindentMode::Independent), cx); + buffer.edit([(ix..ix, text)], Some(AutoindentMode::EachLine), cx); }; // indent after "def():" @@ -209,7 +209,7 @@ mod tests { let argument_ix = buffer.text().find("1").unwrap(); buffer.edit( [(argument_ix..argument_ix + 1, "")], - Some(AutoindentMode::Independent), + Some(AutoindentMode::EachLine), cx, ); assert_eq!( @@ -228,7 +228,7 @@ mod tests { let end_whitespace_ix = buffer.len() - 4; buffer.edit( [(end_whitespace_ix..buffer.len(), "")], - Some(AutoindentMode::Independent), + Some(AutoindentMode::EachLine), cx, ); assert_eq!( diff --git a/crates/zed/src/languages/rust.rs b/crates/zed/src/languages/rust.rs index 8e804f0c7f..adbe431279 100644 --- a/crates/zed/src/languages/rust.rs +++ b/crates/zed/src/languages/rust.rs @@ -444,29 +444,29 @@ mod tests { // indent between braces buffer.set_text("fn a() {}", cx); let ix = buffer.len() - 1; - buffer.edit([(ix..ix, "\n\n")], Some(AutoindentMode::Independent), cx); + buffer.edit([(ix..ix, "\n\n")], Some(AutoindentMode::EachLine), cx); assert_eq!(buffer.text(), "fn a() {\n \n}"); // indent between braces, even after empty lines buffer.set_text("fn a() {\n\n\n}", cx); let ix = buffer.len() - 2; - buffer.edit([(ix..ix, "\n")], Some(AutoindentMode::Independent), cx); + buffer.edit([(ix..ix, "\n")], Some(AutoindentMode::EachLine), cx); assert_eq!(buffer.text(), "fn a() {\n\n\n \n}"); // indent a line that continues a field expression buffer.set_text("fn a() {\n \n}", cx); let ix = buffer.len() - 2; - buffer.edit([(ix..ix, "b\n.c")], Some(AutoindentMode::Independent), cx); + buffer.edit([(ix..ix, "b\n.c")], Some(AutoindentMode::EachLine), cx); assert_eq!(buffer.text(), "fn a() {\n b\n .c\n}"); // indent further lines that continue the field expression, even after empty lines let ix = buffer.len() - 2; - buffer.edit([(ix..ix, "\n\n.d")], Some(AutoindentMode::Independent), cx); + buffer.edit([(ix..ix, "\n\n.d")], Some(AutoindentMode::EachLine), cx); assert_eq!(buffer.text(), "fn a() {\n b\n .c\n \n .d\n}"); // dedent the line after the field expression let ix = buffer.len() - 2; - buffer.edit([(ix..ix, ";\ne")], Some(AutoindentMode::Independent), cx); + buffer.edit([(ix..ix, ";\ne")], Some(AutoindentMode::EachLine), cx); assert_eq!( buffer.text(), "fn a() {\n b\n .c\n \n .d;\n e\n}" @@ -475,21 +475,17 @@ mod tests { // indent inside a struct within a call buffer.set_text("const a: B = c(D {});", cx); let ix = buffer.len() - 3; - buffer.edit([(ix..ix, "\n\n")], Some(AutoindentMode::Independent), cx); + buffer.edit([(ix..ix, "\n\n")], Some(AutoindentMode::EachLine), cx); assert_eq!(buffer.text(), "const a: B = c(D {\n \n});"); // indent further inside a nested call let ix = buffer.len() - 4; - buffer.edit( - [(ix..ix, "e: f(\n\n)")], - Some(AutoindentMode::Independent), - cx, - ); + buffer.edit([(ix..ix, "e: f(\n\n)")], Some(AutoindentMode::EachLine), cx); assert_eq!(buffer.text(), "const a: B = c(D {\n e: f(\n \n )\n});"); // keep that indent after an empty line let ix = buffer.len() - 8; - buffer.edit([(ix..ix, "\n")], Some(AutoindentMode::Independent), cx); + buffer.edit([(ix..ix, "\n")], Some(AutoindentMode::EachLine), cx); assert_eq!( buffer.text(), "const a: B = c(D {\n e: f(\n \n \n )\n});" From cfe3ebd2b30a9d1523c4e317d2d5d596c24f6a7f Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Fri, 29 Jul 2022 09:12:36 +0200 Subject: [PATCH 32/42] Use XDG-compliant directories for config and cache files --- crates/project/src/db.rs | 10 ++++----- crates/zed/src/main.rs | 48 +++++++++++++--------------------------- crates/zed/src/zed.rs | 26 +++++++++++++++------- 3 files changed, 38 insertions(+), 46 deletions(-) diff --git a/crates/project/src/db.rs b/crates/project/src/db.rs index bc125e4303..0b630bf256 100644 --- a/crates/project/src/db.rs +++ b/crates/project/src/db.rs @@ -1,5 +1,5 @@ use anyhow::Result; -use std::path::PathBuf; +use std::path::Path; use std::sync::Arc; pub struct Db(DbStore); @@ -16,8 +16,8 @@ enum DbStore { impl Db { /// Open or create a database at the given file path. - pub fn open(path: PathBuf) -> Result> { - let db = rocksdb::DB::open_default(&path)?; + pub fn open(path: &Path) -> Result> { + let db = rocksdb::DB::open_default(path)?; Ok(Arc::new(Self(DbStore::Real(db)))) } @@ -125,7 +125,7 @@ mod tests { fn test_db() { let dir = TempDir::new("db-test").unwrap(); let fake_db = Db::open_fake(); - let real_db = Db::open(dir.path().join("test.db")).unwrap(); + let real_db = Db::open(&dir.path().join("test.db")).unwrap(); for db in [&real_db, &fake_db] { assert_eq!( @@ -152,7 +152,7 @@ mod tests { drop(real_db); - let real_db = Db::open(dir.path().join("test.db")).unwrap(); + let real_db = Db::open(&dir.path().join("test.db")).unwrap(); assert_eq!( real_db.read(["key-1", "key-2", "key-3"]).unwrap(), &[Some("one".as_bytes().to_vec()), None, None,] diff --git a/crates/zed/src/main.rs b/crates/zed/src/main.rs index 2ca1403b6c..d800901b49 100644 --- a/crates/zed/src/main.rs +++ b/crates/zed/src/main.rs @@ -28,15 +28,7 @@ use project::{Fs, ProjectStore}; use serde_json::json; use settings::{self, KeymapFileContent, Settings, SettingsFileContent}; use smol::process::Command; -use std::{ - env, - ffi::OsStr, - fs, panic, - path::{Path, PathBuf}, - sync::Arc, - thread, - time::Duration, -}; +use std::{env, ffi::OsStr, fs, panic, path::PathBuf, sync::Arc, thread, time::Duration}; use terminal; use theme::ThemeRegistry; use util::{ResultExt, TryFutureExt}; @@ -50,21 +42,19 @@ use zed::{ fn main() { let http = http::client(); - let home_dir = dirs::home_dir().expect("could not find home dir"); - let db_dir_path = home_dir.join("Library/Application Support/Zed"); - let logs_dir_path = home_dir.join("Library/Logs/Zed"); - fs::create_dir_all(&db_dir_path).expect("could not create database path"); - fs::create_dir_all(&logs_dir_path).expect("could not create logs path"); - init_logger(&logs_dir_path); + fs::create_dir_all(&*zed::LANGUAGES_DIR_PATH).expect("could not create languages path"); + fs::create_dir_all(&*zed::DB_DIR_PATH).expect("could not create database path"); + fs::create_dir_all(&*zed::LOGS_DIR_PATH).expect("could not create logs path"); + init_logger(); log::info!("========== starting zed =========="); let mut app = gpui::App::new(Assets).unwrap(); let app_version = ZED_APP_VERSION .or_else(|| app.platform().app_version().ok()) .map_or("dev".to_string(), |v| v.to_string()); - init_panic_hook(logs_dir_path, app_version, http.clone(), app.background()); + init_panic_hook(app_version, http.clone(), app.background()); let db = app.background().spawn(async move { - project::Db::open(db_dir_path.join("zed.db")) + project::Db::open(&*zed::DB_PATH) .log_err() .unwrap_or(project::Db::null()) }); @@ -100,7 +90,7 @@ fn main() { app.run(move |cx| { let client = client::Client::new(http.clone()); let mut languages = LanguageRegistry::new(login_shell_env_loaded); - languages.set_language_server_download_dir(zed::ROOT_PATH.clone()); + languages.set_language_server_download_dir(zed::LANGUAGES_DIR_PATH.clone()); let languages = Arc::new(languages); let init_languages = cx .background() @@ -205,42 +195,34 @@ fn main() { }); } -fn init_logger(logs_dir_path: &Path) { +fn init_logger() { if stdout_is_a_pty() { env_logger::init(); } else { let level = LevelFilter::Info; - let log_file_path = logs_dir_path.join("Zed.log"); // Prevent log file from becoming too large. const MAX_LOG_BYTES: u64 = 1 * 1024 * 1024; - if fs::metadata(&log_file_path).map_or(false, |metadata| metadata.len() > MAX_LOG_BYTES) { - let _ = fs::rename(&log_file_path, logs_dir_path.join("Zed.log.old")); + if fs::metadata(&*zed::LOG_PATH).map_or(false, |metadata| metadata.len() > MAX_LOG_BYTES) { + let _ = fs::rename(&*zed::LOG_PATH, &*zed::OLD_LOG_PATH); } let log_file = OpenOptions::new() .create(true) .append(true) - .open(log_file_path) + .open(&*zed::LOG_PATH) .expect("could not open logfile"); simplelog::WriteLogger::init(level, simplelog::Config::default(), log_file) .expect("could not initialize logger"); } } -fn init_panic_hook( - logs_dir_path: PathBuf, - app_version: String, - http: Arc, - background: Arc, -) { +fn init_panic_hook(app_version: String, http: Arc, background: Arc) { background .spawn({ - let logs_dir_path = logs_dir_path.clone(); - async move { let panic_report_url = format!("{}/api/panic", &*client::ZED_SERVER_URL); - let mut children = smol::fs::read_dir(&logs_dir_path).await?; + let mut children = smol::fs::read_dir(&*zed::LOGS_DIR_PATH).await?; while let Some(child) = children.next().await { let child = child?; let child_path = child.path(); @@ -330,7 +312,7 @@ fn init_panic_hook( let panic_filename = chrono::Utc::now().format("%Y_%m_%d %H_%M_%S").to_string(); fs::write( - logs_dir_path.join(format!("zed-{}-{}.panic", app_version, panic_filename)), + zed::LOGS_DIR_PATH.join(format!("zed-{}-{}.panic", app_version, panic_filename)), &message, ) .context("error writing panic to disk") diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index 6ed9b40a18..fb8496293e 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -31,6 +31,7 @@ use serde::Deserialize; use serde_json::to_string_pretty; use settings::{keymap_file_json_schema, settings_file_json_schema, Settings}; use std::{ + env, path::{Path, PathBuf}, str, sync::Arc, @@ -67,13 +68,22 @@ actions!( const MIN_FONT_SIZE: f32 = 6.0; lazy_static! { - pub static ref HOME_PATH: PathBuf = - dirs::home_dir().expect("failed to determine home directory"); - pub static ref LOG_PATH: PathBuf = HOME_PATH.join("Library/Logs/Zed/Zed.log"); - pub static ref OLD_LOG_PATH: PathBuf = HOME_PATH.join("Library/Logs/Zed/Zed.log.old"); - pub static ref ROOT_PATH: PathBuf = HOME_PATH.join(".zed"); - pub static ref SETTINGS_PATH: PathBuf = ROOT_PATH.join("settings.json"); - pub static ref KEYMAP_PATH: PathBuf = ROOT_PATH.join("keymap.json"); + static ref HOME_PATH: PathBuf = dirs::home_dir().expect("failed to determine home directory"); + static ref CACHE_DIR_PATH: PathBuf = dirs::cache_dir() + .expect("failed to determine cache directory") + .join("Zed"); + static ref CONFIG_DIR_PATH: PathBuf = env::var_os("XDG_CONFIG_HOME") + .map(|home| home.into()) + .unwrap_or_else(|| HOME_PATH.join(".config")) + .join("zed"); + pub static ref LOGS_DIR_PATH: PathBuf = HOME_PATH.join("Library/Logs/Zed"); + pub static ref LANGUAGES_DIR_PATH: PathBuf = CACHE_DIR_PATH.join("languages"); + pub static ref DB_DIR_PATH: PathBuf = CACHE_DIR_PATH.join("db"); + pub static ref DB_PATH: PathBuf = DB_DIR_PATH.join("zed.db"); + pub static ref SETTINGS_PATH: PathBuf = CONFIG_DIR_PATH.join("settings.json"); + pub static ref KEYMAP_PATH: PathBuf = CONFIG_DIR_PATH.join("keymap.json"); + pub static ref LOG_PATH: PathBuf = LOGS_DIR_PATH.join("Zed.log"); + pub static ref OLD_LOG_PATH: PathBuf = LOGS_DIR_PATH.join("Zed.log.old"); } pub fn init(app_state: &Arc, cx: &mut gpui::MutableAppContext) { @@ -399,7 +409,7 @@ fn open_config_file( cx.spawn(|workspace, mut cx| async move { let fs = &app_state.fs; if !fs.is_file(path).await { - fs.create_dir(&ROOT_PATH).await?; + fs.create_dir(&CONFIG_DIR_PATH).await?; fs.create_file(path, Default::default()).await?; fs.save(path, &default_content(), Default::default()) .await?; From 5f6e4c7d91d6cdab2e70087f9c64bb42317f566a Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Fri, 29 Jul 2022 09:27:21 +0200 Subject: [PATCH 33/42] Extract all zed config/cache paths into a `paths` module --- crates/zed/src/main.rs | 25 +++++++++++++------------ crates/zed/src/paths.rs | 22 ++++++++++++++++++++++ crates/zed/src/zed.rs | 38 +++++++------------------------------- 3 files changed, 42 insertions(+), 43 deletions(-) create mode 100644 crates/zed/src/paths.rs diff --git a/crates/zed/src/main.rs b/crates/zed/src/main.rs index d800901b49..7d24eaa88f 100644 --- a/crates/zed/src/main.rs +++ b/crates/zed/src/main.rs @@ -42,9 +42,9 @@ use zed::{ fn main() { let http = http::client(); - fs::create_dir_all(&*zed::LANGUAGES_DIR_PATH).expect("could not create languages path"); - fs::create_dir_all(&*zed::DB_DIR_PATH).expect("could not create database path"); - fs::create_dir_all(&*zed::LOGS_DIR_PATH).expect("could not create logs path"); + fs::create_dir_all(&*zed::paths::LANGUAGES_DIR).expect("could not create languages path"); + fs::create_dir_all(&*zed::paths::DB_DIR).expect("could not create database path"); + fs::create_dir_all(&*zed::paths::LOGS_DIR).expect("could not create logs path"); init_logger(); log::info!("========== starting zed =========="); @@ -54,7 +54,7 @@ fn main() { .map_or("dev".to_string(), |v| v.to_string()); init_panic_hook(app_version, http.clone(), app.background()); let db = app.background().spawn(async move { - project::Db::open(&*zed::DB_PATH) + project::Db::open(&*zed::paths::DB) .log_err() .unwrap_or(project::Db::null()) }); @@ -90,7 +90,7 @@ fn main() { app.run(move |cx| { let client = client::Client::new(http.clone()); let mut languages = LanguageRegistry::new(login_shell_env_loaded); - languages.set_language_server_download_dir(zed::LANGUAGES_DIR_PATH.clone()); + languages.set_language_server_download_dir(zed::paths::LANGUAGES_DIR.clone()); let languages = Arc::new(languages); let init_languages = cx .background() @@ -203,14 +203,15 @@ fn init_logger() { // Prevent log file from becoming too large. const MAX_LOG_BYTES: u64 = 1 * 1024 * 1024; - if fs::metadata(&*zed::LOG_PATH).map_or(false, |metadata| metadata.len() > MAX_LOG_BYTES) { - let _ = fs::rename(&*zed::LOG_PATH, &*zed::OLD_LOG_PATH); + if fs::metadata(&*zed::paths::LOG).map_or(false, |metadata| metadata.len() > MAX_LOG_BYTES) + { + let _ = fs::rename(&*zed::paths::LOG, &*zed::paths::OLD_LOG); } let log_file = OpenOptions::new() .create(true) .append(true) - .open(&*zed::LOG_PATH) + .open(&*zed::paths::LOG) .expect("could not open logfile"); simplelog::WriteLogger::init(level, simplelog::Config::default(), log_file) .expect("could not initialize logger"); @@ -222,7 +223,7 @@ fn init_panic_hook(app_version: String, http: Arc, background: A .spawn({ async move { let panic_report_url = format!("{}/api/panic", &*client::ZED_SERVER_URL); - let mut children = smol::fs::read_dir(&*zed::LOGS_DIR_PATH).await?; + let mut children = smol::fs::read_dir(&*zed::paths::LOGS_DIR).await?; while let Some(child) = children.next().await { let child = child?; let child_path = child.path(); @@ -312,7 +313,7 @@ fn init_panic_hook(app_version: String, http: Arc, background: A let panic_filename = chrono::Utc::now().format("%Y_%m_%d %H_%M_%S").to_string(); fs::write( - zed::LOGS_DIR_PATH.join(format!("zed-{}-{}.panic", app_version, panic_filename)), + zed::paths::LOGS_DIR.join(format!("zed-{}-{}.panic", app_version, panic_filename)), &message, ) .context("error writing panic to disk") @@ -446,8 +447,8 @@ fn load_config_files( .clone() .spawn(async move { let settings_file = - WatchedJsonFile::new(fs.clone(), &executor, zed::SETTINGS_PATH.clone()).await; - let keymap_file = WatchedJsonFile::new(fs, &executor, zed::KEYMAP_PATH.clone()).await; + WatchedJsonFile::new(fs.clone(), &executor, zed::paths::SETTINGS.clone()).await; + let keymap_file = WatchedJsonFile::new(fs, &executor, zed::paths::KEYMAP.clone()).await; tx.send((settings_file, keymap_file)).ok() }) .detach(); diff --git a/crates/zed/src/paths.rs b/crates/zed/src/paths.rs new file mode 100644 index 0000000000..763c4d1070 --- /dev/null +++ b/crates/zed/src/paths.rs @@ -0,0 +1,22 @@ +use std::{env, path::PathBuf}; + +use lazy_static::lazy_static; + +lazy_static! { + static ref HOME: PathBuf = dirs::home_dir().expect("failed to determine home directory"); + static ref CACHE_DIR: PathBuf = dirs::cache_dir() + .expect("failed to determine cache directory") + .join("Zed"); + pub static ref CONFIG_DIR: PathBuf = env::var_os("XDG_CONFIG_HOME") + .map(|home| home.into()) + .unwrap_or_else(|| HOME.join(".config")) + .join("zed"); + pub static ref LOGS_DIR: PathBuf = HOME.join("Library/Logs/Zed"); + pub static ref LANGUAGES_DIR: PathBuf = CACHE_DIR.join("languages"); + pub static ref DB_DIR: PathBuf = CACHE_DIR.join("db"); + pub static ref DB: PathBuf = DB_DIR.join("zed.db"); + pub static ref SETTINGS: PathBuf = CONFIG_DIR.join("settings.json"); + pub static ref KEYMAP: PathBuf = CONFIG_DIR.join("keymap.json"); + pub static ref LOG: PathBuf = LOGS_DIR.join("Zed.log"); + pub static ref OLD_LOG: PathBuf = LOGS_DIR.join("Zed.log.old"); +} diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index fb8496293e..20ecf68e8d 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -1,6 +1,7 @@ mod feedback; pub mod languages; pub mod menus; +pub mod paths; pub mod settings_file; #[cfg(any(test, feature = "test-support"))] pub mod test; @@ -22,7 +23,6 @@ use gpui::{ AssetSource, AsyncAppContext, ViewContext, }; use language::Rope; -use lazy_static::lazy_static; pub use lsp; pub use project::{self, fs}; use project_panel::ProjectPanel; @@ -30,12 +30,7 @@ use search::{BufferSearchBar, ProjectSearchBar}; use serde::Deserialize; use serde_json::to_string_pretty; use settings::{keymap_file_json_schema, settings_file_json_schema, Settings}; -use std::{ - env, - path::{Path, PathBuf}, - str, - sync::Arc, -}; +use std::{env, path::Path, str, sync::Arc}; use util::ResultExt; pub use workspace; use workspace::{sidebar::Side, AppState, Workspace}; @@ -67,25 +62,6 @@ actions!( const MIN_FONT_SIZE: f32 = 6.0; -lazy_static! { - static ref HOME_PATH: PathBuf = dirs::home_dir().expect("failed to determine home directory"); - static ref CACHE_DIR_PATH: PathBuf = dirs::cache_dir() - .expect("failed to determine cache directory") - .join("Zed"); - static ref CONFIG_DIR_PATH: PathBuf = env::var_os("XDG_CONFIG_HOME") - .map(|home| home.into()) - .unwrap_or_else(|| HOME_PATH.join(".config")) - .join("zed"); - pub static ref LOGS_DIR_PATH: PathBuf = HOME_PATH.join("Library/Logs/Zed"); - pub static ref LANGUAGES_DIR_PATH: PathBuf = CACHE_DIR_PATH.join("languages"); - pub static ref DB_DIR_PATH: PathBuf = CACHE_DIR_PATH.join("db"); - pub static ref DB_PATH: PathBuf = DB_DIR_PATH.join("zed.db"); - pub static ref SETTINGS_PATH: PathBuf = CONFIG_DIR_PATH.join("settings.json"); - pub static ref KEYMAP_PATH: PathBuf = CONFIG_DIR_PATH.join("keymap.json"); - pub static ref LOG_PATH: PathBuf = LOGS_DIR_PATH.join("Zed.log"); - pub static ref OLD_LOG_PATH: PathBuf = LOGS_DIR_PATH.join("Zed.log.old"); -} - pub fn init(app_state: &Arc, cx: &mut gpui::MutableAppContext) { cx.add_action(about); cx.add_global_action(quit); @@ -122,7 +98,7 @@ pub fn init(app_state: &Arc, cx: &mut gpui::MutableAppContext) { cx.add_action({ let app_state = app_state.clone(); move |_: &mut Workspace, _: &OpenSettings, cx: &mut ViewContext| { - open_config_file(&SETTINGS_PATH, app_state.clone(), cx, || { + open_config_file(&paths::SETTINGS, app_state.clone(), cx, || { str::from_utf8( Assets .load("settings/initial_user_settings.json") @@ -143,7 +119,7 @@ pub fn init(app_state: &Arc, cx: &mut gpui::MutableAppContext) { cx.add_action({ let app_state = app_state.clone(); move |_: &mut Workspace, _: &OpenKeymap, cx: &mut ViewContext| { - open_config_file(&KEYMAP_PATH, app_state.clone(), cx, || Default::default()); + open_config_file(&paths::KEYMAP, app_state.clone(), cx, || Default::default()); } }); cx.add_action({ @@ -409,7 +385,7 @@ fn open_config_file( cx.spawn(|workspace, mut cx| async move { let fs = &app_state.fs; if !fs.is_file(path).await { - fs.create_dir(&CONFIG_DIR_PATH).await?; + fs.create_dir(&paths::CONFIG_DIR).await?; fs.create_file(path, Default::default()).await?; fs.save(path, &default_content(), Default::default()) .await?; @@ -437,8 +413,8 @@ fn open_log_file( workspace.with_local_workspace(cx, app_state.clone(), |_, cx| { cx.spawn_weak(|workspace, mut cx| async move { let (old_log, new_log) = futures::join!( - app_state.fs.load(&OLD_LOG_PATH), - app_state.fs.load(&LOG_PATH) + app_state.fs.load(&paths::OLD_LOG), + app_state.fs.load(&paths::LOG) ); if let Some(workspace) = workspace.upgrade(&cx) { From f2d92d640d08414bbe14800cb9f2655bbf0ee0cc Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Fri, 29 Jul 2022 09:57:38 +0200 Subject: [PATCH 34/42] Copy setting and keymap files from legacy config locations --- crates/zed/src/main.rs | 26 +++++++++++++++++++++++--- crates/zed/src/paths.rs | 28 +++++++++++++++------------- crates/zed/src/zed.rs | 10 +++++++--- 3 files changed, 45 insertions(+), 19 deletions(-) diff --git a/crates/zed/src/main.rs b/crates/zed/src/main.rs index 7d24eaa88f..ed718272d8 100644 --- a/crates/zed/src/main.rs +++ b/crates/zed/src/main.rs @@ -42,9 +42,7 @@ use zed::{ fn main() { let http = http::client(); - fs::create_dir_all(&*zed::paths::LANGUAGES_DIR).expect("could not create languages path"); - fs::create_dir_all(&*zed::paths::DB_DIR).expect("could not create database path"); - fs::create_dir_all(&*zed::paths::LOGS_DIR).expect("could not create logs path"); + init_paths(); init_logger(); log::info!("========== starting zed =========="); @@ -195,6 +193,28 @@ fn main() { }); } +fn init_paths() { + fs::create_dir_all(&*zed::paths::CONFIG_DIR).expect("could not create config path"); + fs::create_dir_all(&*zed::paths::LANGUAGES_DIR).expect("could not create languages path"); + fs::create_dir_all(&*zed::paths::DB_DIR).expect("could not create database path"); + fs::create_dir_all(&*zed::paths::LOGS_DIR).expect("could not create logs path"); + + // Copy setting files from legacy locations. TODO: remove this after a few releases. + thread::spawn(|| { + if fs::metadata(&*zed::paths::legacy::SETTINGS).is_ok() + && fs::metadata(&*zed::paths::SETTINGS).is_err() + { + fs::copy(&*zed::paths::legacy::SETTINGS, &*zed::paths::SETTINGS).log_err(); + } + + if fs::metadata(&*zed::paths::legacy::KEYMAP).is_ok() + && fs::metadata(&*zed::paths::KEYMAP).is_err() + { + fs::copy(&*zed::paths::legacy::KEYMAP, &*zed::paths::KEYMAP).log_err(); + } + }); +} + fn init_logger() { if stdout_is_a_pty() { env_logger::init(); diff --git a/crates/zed/src/paths.rs b/crates/zed/src/paths.rs index 763c4d1070..6643cc0fe6 100644 --- a/crates/zed/src/paths.rs +++ b/crates/zed/src/paths.rs @@ -1,22 +1,24 @@ -use std::{env, path::PathBuf}; +use std::path::PathBuf; -use lazy_static::lazy_static; - -lazy_static! { +lazy_static::lazy_static! { static ref HOME: PathBuf = dirs::home_dir().expect("failed to determine home directory"); - static ref CACHE_DIR: PathBuf = dirs::cache_dir() - .expect("failed to determine cache directory") - .join("Zed"); - pub static ref CONFIG_DIR: PathBuf = env::var_os("XDG_CONFIG_HOME") - .map(|home| home.into()) - .unwrap_or_else(|| HOME.join(".config")) - .join("zed"); + pub static ref CONFIG_DIR: PathBuf = HOME.join(".config").join("zed"); pub static ref LOGS_DIR: PathBuf = HOME.join("Library/Logs/Zed"); - pub static ref LANGUAGES_DIR: PathBuf = CACHE_DIR.join("languages"); - pub static ref DB_DIR: PathBuf = CACHE_DIR.join("db"); + pub static ref LANGUAGES_DIR: PathBuf = HOME.join("Library/Application Support/Zed/languages"); + pub static ref DB_DIR: PathBuf = HOME.join("Library/Application Support/Zed/db"); pub static ref DB: PathBuf = DB_DIR.join("zed.db"); pub static ref SETTINGS: PathBuf = CONFIG_DIR.join("settings.json"); pub static ref KEYMAP: PathBuf = CONFIG_DIR.join("keymap.json"); pub static ref LOG: PathBuf = LOGS_DIR.join("Zed.log"); pub static ref OLD_LOG: PathBuf = LOGS_DIR.join("Zed.log.old"); } + +pub mod legacy { + use std::path::PathBuf; + + lazy_static::lazy_static! { + static ref CONFIG_DIR: PathBuf = super::HOME.join(".zed"); + pub static ref SETTINGS: PathBuf = CONFIG_DIR.join("settings.json"); + pub static ref KEYMAP: PathBuf = CONFIG_DIR.join("keymap.json"); + } +} diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index 20ecf68e8d..2f683d8e8e 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -223,11 +223,11 @@ pub fn initialize_workspace( }, "schemas": [ { - "fileMatch": [".zed/settings.json"], + "fileMatch": [schema_file_match(&*paths::SETTINGS)], "schema": settings_file_json_schema(theme_names, language_names), }, { - "fileMatch": [".zed/keymap.json"], + "fileMatch": [schema_file_match(&*paths::KEYMAP)], "schema": keymap_file_json_schema(&action_names), } ] @@ -385,7 +385,6 @@ fn open_config_file( cx.spawn(|workspace, mut cx| async move { let fs = &app_state.fs; if !fs.is_file(path).await { - fs.create_dir(&paths::CONFIG_DIR).await?; fs.create_file(path, Default::default()).await?; fs.save(path, &default_content(), Default::default()) .await?; @@ -481,6 +480,11 @@ fn open_bundled_config_file( }); } +fn schema_file_match(path: &Path) -> &Path { + path.strip_prefix(path.parent().unwrap().parent().unwrap()) + .unwrap() +} + #[cfg(test)] mod tests { use super::*; From 5149c153290bc2eceeb4d4e45a82aa6f4e6b175f Mon Sep 17 00:00:00 2001 From: ForLoveOfCats Date: Fri, 29 Jul 2022 11:41:08 -0400 Subject: [PATCH 35/42] Add "go to type definition" action --- assets/keymaps/default.json | 1 + crates/editor/src/editor.rs | 29 ++- crates/editor/src/mouse_context_menu.rs | 5 +- crates/lsp/src/lsp.rs | 1 + crates/project/src/lsp_command.rs | 241 ++++++++++++++++++++++++ crates/project/src/project.rs | 10 + crates/rpc/proto/zed.proto | 155 ++++++++------- crates/rpc/src/proto.rs | 4 + crates/rpc/src/rpc.rs | 2 +- crates/zed/src/menus.rs | 4 + 10 files changed, 377 insertions(+), 75 deletions(-) diff --git a/assets/keymaps/default.json b/assets/keymaps/default.json index 72252edd71..28eec35def 100644 --- a/assets/keymaps/default.json +++ b/assets/keymaps/default.json @@ -192,6 +192,7 @@ "shift-f8": "editor::GoToPrevDiagnostic", "f2": "editor::Rename", "f12": "editor::GoToDefinition", + "cmd-f12": "editor::GoToTypeDefinition", "alt-shift-f12": "editor::FindAllReferences", "ctrl-m": "editor::MoveToEnclosingBracket", "alt-cmd-[": "editor::Fold", diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 819aa5ccd5..558a6bfd98 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -187,6 +187,7 @@ actions!( SelectLargerSyntaxNode, SelectSmallerSyntaxNode, GoToDefinition, + GoToTypeDefinition, MoveToEnclosingBracket, UndoSelection, RedoSelection, @@ -297,6 +298,7 @@ pub fn init(cx: &mut MutableAppContext) { cx.add_action(Editor::go_to_diagnostic); cx.add_action(Editor::go_to_prev_diagnostic); cx.add_action(Editor::go_to_definition); + cx.add_action(Editor::go_to_type_definition); cx.add_action(Editor::page_up); cx.add_action(Editor::page_down); cx.add_action(Editor::fold); @@ -895,6 +897,11 @@ pub struct NavigationData { pub struct EditorCreated(pub ViewHandle); +enum GotoDefinitionKind { + Symbol, + Type, +} + impl Editor { pub fn single_line( field_editor_style: Option, @@ -4693,6 +4700,22 @@ impl Editor { workspace: &mut Workspace, _: &GoToDefinition, cx: &mut ViewContext, + ) { + Self::go_to_definition_of_kind(GotoDefinitionKind::Symbol, workspace, cx); + } + + pub fn go_to_type_definition( + workspace: &mut Workspace, + _: &GoToTypeDefinition, + cx: &mut ViewContext, + ) { + Self::go_to_definition_of_kind(GotoDefinitionKind::Type, workspace, cx); + } + + fn go_to_definition_of_kind( + kind: GotoDefinitionKind, + workspace: &mut Workspace, + cx: &mut ViewContext, ) { let active_item = workspace.active_item(cx); let editor_handle = if let Some(editor) = active_item @@ -4714,7 +4737,11 @@ impl Editor { }; let project = workspace.project().clone(); - let definitions = project.update(cx, |project, cx| project.definition(&buffer, head, cx)); + let definitions = project.update(cx, |project, cx| match kind { + GotoDefinitionKind::Symbol => project.definition(&buffer, head, cx), + GotoDefinitionKind::Type => project.type_definition(&buffer, head, cx), + }); + cx.spawn(|workspace, mut cx| async move { let definitions = definitions.await?; workspace.update(&mut cx, |workspace, cx| { diff --git a/crates/editor/src/mouse_context_menu.rs b/crates/editor/src/mouse_context_menu.rs index ce2faf8fa6..513a9ed99c 100644 --- a/crates/editor/src/mouse_context_menu.rs +++ b/crates/editor/src/mouse_context_menu.rs @@ -2,8 +2,8 @@ use context_menu::ContextMenuItem; use gpui::{geometry::vector::Vector2F, impl_internal_actions, MutableAppContext, ViewContext}; use crate::{ - DisplayPoint, Editor, EditorMode, Event, FindAllReferences, GoToDefinition, Rename, SelectMode, - ToggleCodeActions, + DisplayPoint, Editor, EditorMode, Event, FindAllReferences, GoToDefinition, GoToTypeDefinition, + Rename, SelectMode, ToggleCodeActions, }; #[derive(Clone, PartialEq)] @@ -50,6 +50,7 @@ pub fn deploy_context_menu( vec![ ContextMenuItem::item("Rename Symbol", Rename), ContextMenuItem::item("Go To Definition", GoToDefinition), + ContextMenuItem::item("Go To Type Definition", GoToTypeDefinition), ContextMenuItem::item("Find All References", FindAllReferences), ContextMenuItem::item( "Code Actions", diff --git a/crates/lsp/src/lsp.rs b/crates/lsp/src/lsp.rs index 8c689808ae..5c8916e34e 100644 --- a/crates/lsp/src/lsp.rs +++ b/crates/lsp/src/lsp.rs @@ -1,3 +1,4 @@ +pub use lsp_types::request::*; pub use lsp_types::*; use anyhow::{anyhow, Context, Result}; diff --git a/crates/project/src/lsp_command.rs b/crates/project/src/lsp_command.rs index 0c30ee2924..6ec52451a1 100644 --- a/crates/project/src/lsp_command.rs +++ b/crates/project/src/lsp_command.rs @@ -75,6 +75,10 @@ pub(crate) struct GetDefinition { pub position: PointUtf16, } +pub(crate) struct GetTypeDefinition { + pub position: PointUtf16, +} + pub(crate) struct GetReferences { pub position: PointUtf16, } @@ -565,6 +569,243 @@ impl LspCommand for GetDefinition { } } +#[async_trait(?Send)] +impl LspCommand for GetTypeDefinition { + type Response = Vec; + type LspRequest = lsp::request::GotoTypeDefinition; + type ProtoRequest = proto::GetTypeDefinition; + + fn to_lsp(&self, path: &Path, _: &AppContext) -> lsp::GotoTypeDefinitionParams { + lsp::GotoTypeDefinitionParams { + text_document_position_params: lsp::TextDocumentPositionParams { + text_document: lsp::TextDocumentIdentifier { + uri: lsp::Url::from_file_path(path).unwrap(), + }, + position: point_to_lsp(self.position), + }, + work_done_progress_params: Default::default(), + partial_result_params: Default::default(), + } + } + + async fn response_from_lsp( + self, + message: Option, + project: ModelHandle, + buffer: ModelHandle, + mut cx: AsyncAppContext, + ) -> Result> { + let mut definitions = Vec::new(); + let (lsp_adapter, language_server) = project + .read_with(&cx, |project, cx| { + project + .language_server_for_buffer(buffer.read(cx), cx) + .map(|(adapter, server)| (adapter.clone(), server.clone())) + }) + .ok_or_else(|| anyhow!("no language server found for buffer"))?; + + if let Some(message) = message { + let mut unresolved_links = Vec::new(); + match message { + lsp::GotoTypeDefinitionResponse::Scalar(loc) => { + unresolved_links.push((None, loc.uri, loc.range)); + } + lsp::GotoTypeDefinitionResponse::Array(locs) => { + unresolved_links.extend(locs.into_iter().map(|l| (None, l.uri, l.range))); + } + lsp::GotoTypeDefinitionResponse::Link(links) => { + unresolved_links.extend(links.into_iter().map(|l| { + ( + l.origin_selection_range, + l.target_uri, + l.target_selection_range, + ) + })); + } + } + + for (origin_range, target_uri, target_range) in unresolved_links { + let target_buffer_handle = project + .update(&mut cx, |this, cx| { + this.open_local_buffer_via_lsp( + target_uri, + language_server.server_id(), + lsp_adapter.name.clone(), + cx, + ) + }) + .await?; + + cx.read(|cx| { + let origin_location = origin_range.map(|origin_range| { + let origin_buffer = buffer.read(cx); + let origin_start = origin_buffer + .clip_point_utf16(point_from_lsp(origin_range.start), Bias::Left); + let origin_end = origin_buffer + .clip_point_utf16(point_from_lsp(origin_range.end), Bias::Left); + Location { + buffer: buffer.clone(), + range: origin_buffer.anchor_after(origin_start) + ..origin_buffer.anchor_before(origin_end), + } + }); + + let target_buffer = target_buffer_handle.read(cx); + let target_start = target_buffer + .clip_point_utf16(point_from_lsp(target_range.start), Bias::Left); + let target_end = target_buffer + .clip_point_utf16(point_from_lsp(target_range.end), Bias::Left); + let target_location = Location { + buffer: target_buffer_handle, + range: target_buffer.anchor_after(target_start) + ..target_buffer.anchor_before(target_end), + }; + + definitions.push(LocationLink { + origin: origin_location, + target: target_location, + }) + }); + } + } + + Ok(definitions) + } + + fn to_proto(&self, project_id: u64, buffer: &Buffer) -> proto::GetTypeDefinition { + proto::GetTypeDefinition { + project_id, + buffer_id: buffer.remote_id(), + position: Some(language::proto::serialize_anchor( + &buffer.anchor_before(self.position), + )), + version: serialize_version(&buffer.version()), + } + } + + async fn from_proto( + message: proto::GetTypeDefinition, + _: ModelHandle, + buffer: ModelHandle, + mut cx: AsyncAppContext, + ) -> Result { + let position = message + .position + .and_then(deserialize_anchor) + .ok_or_else(|| anyhow!("invalid position"))?; + buffer + .update(&mut cx, |buffer, _| { + buffer.wait_for_version(deserialize_version(message.version)) + }) + .await; + Ok(Self { + position: buffer.read_with(&cx, |buffer, _| position.to_point_utf16(buffer)), + }) + } + + fn response_to_proto( + response: Vec, + project: &mut Project, + peer_id: PeerId, + _: &clock::Global, + cx: &AppContext, + ) -> proto::GetTypeDefinitionResponse { + let links = response + .into_iter() + .map(|definition| { + let origin = definition.origin.map(|origin| { + let buffer = project.serialize_buffer_for_peer(&origin.buffer, peer_id, cx); + proto::Location { + start: Some(serialize_anchor(&origin.range.start)), + end: Some(serialize_anchor(&origin.range.end)), + buffer: Some(buffer), + } + }); + + let buffer = + project.serialize_buffer_for_peer(&definition.target.buffer, peer_id, cx); + let target = proto::Location { + start: Some(serialize_anchor(&definition.target.range.start)), + end: Some(serialize_anchor(&definition.target.range.end)), + buffer: Some(buffer), + }; + + proto::LocationLink { + origin, + target: Some(target), + } + }) + .collect(); + proto::GetTypeDefinitionResponse { links } + } + + async fn response_from_proto( + self, + message: proto::GetTypeDefinitionResponse, + project: ModelHandle, + _: ModelHandle, + mut cx: AsyncAppContext, + ) -> Result> { + let mut links = Vec::new(); + for link in message.links { + let origin = match link.origin { + Some(origin) => { + let buffer = origin + .buffer + .ok_or_else(|| anyhow!("missing origin buffer"))?; + let buffer = project + .update(&mut cx, |this, cx| this.deserialize_buffer(buffer, cx)) + .await?; + let start = origin + .start + .and_then(deserialize_anchor) + .ok_or_else(|| anyhow!("missing origin start"))?; + let end = origin + .end + .and_then(deserialize_anchor) + .ok_or_else(|| anyhow!("missing origin end"))?; + buffer + .update(&mut cx, |buffer, _| buffer.wait_for_anchors([&start, &end])) + .await; + Some(Location { + buffer, + range: start..end, + }) + } + None => None, + }; + + let target = link.target.ok_or_else(|| anyhow!("missing target"))?; + let buffer = target.buffer.ok_or_else(|| anyhow!("missing buffer"))?; + let buffer = project + .update(&mut cx, |this, cx| this.deserialize_buffer(buffer, cx)) + .await?; + let start = target + .start + .and_then(deserialize_anchor) + .ok_or_else(|| anyhow!("missing target start"))?; + let end = target + .end + .and_then(deserialize_anchor) + .ok_or_else(|| anyhow!("missing target end"))?; + buffer + .update(&mut cx, |buffer, _| buffer.wait_for_anchors([&start, &end])) + .await; + let target = Location { + buffer, + range: start..end, + }; + + links.push(LocationLink { origin, target }) + } + Ok(links) + } + + fn buffer_id_from_proto(message: &proto::GetTypeDefinition) -> u64 { + message.buffer_id + } +} + #[async_trait(?Send)] impl LspCommand for GetReferences { type Response = Vec; diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 898dbb5a2f..8adc10ba55 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -3250,6 +3250,16 @@ impl Project { self.request_lsp(buffer.clone(), GetDefinition { position }, cx) } + pub fn type_definition( + &self, + buffer: &ModelHandle, + position: T, + cx: &mut ModelContext, + ) -> Task>> { + let position = position.to_point_utf16(buffer.read(cx)); + self.request_lsp(buffer.clone(), GetTypeDefinition { position }, cx) + } + pub fn references( &self, buffer: &ModelHandle, diff --git a/crates/rpc/proto/zed.proto b/crates/rpc/proto/zed.proto index 35f3049edb..f52815a8be 100644 --- a/crates/rpc/proto/zed.proto +++ b/crates/rpc/proto/zed.proto @@ -26,85 +26,87 @@ message Envelope { GetDefinition get_definition = 20; GetDefinitionResponse get_definition_response = 21; - GetReferences get_references = 22; - GetReferencesResponse get_references_response = 23; - GetDocumentHighlights get_document_highlights = 24; - GetDocumentHighlightsResponse get_document_highlights_response = 25; - GetProjectSymbols get_project_symbols = 26; - GetProjectSymbolsResponse get_project_symbols_response = 27; - OpenBufferForSymbol open_buffer_for_symbol = 28; - OpenBufferForSymbolResponse open_buffer_for_symbol_response = 29; + GetTypeDefinition get_type_definition = 22; + GetTypeDefinitionResponse get_type_definition_response = 23; + GetReferences get_references = 24; + GetReferencesResponse get_references_response = 25; + GetDocumentHighlights get_document_highlights = 26; + GetDocumentHighlightsResponse get_document_highlights_response = 27; + GetProjectSymbols get_project_symbols = 28; + GetProjectSymbolsResponse get_project_symbols_response = 29; + OpenBufferForSymbol open_buffer_for_symbol = 30; + OpenBufferForSymbolResponse open_buffer_for_symbol_response = 31; - UpdateProject update_project = 30; - RegisterProjectActivity register_project_activity = 31; - UpdateWorktree update_worktree = 32; - UpdateWorktreeExtensions update_worktree_extensions = 33; + UpdateProject update_project = 32; + RegisterProjectActivity register_project_activity = 33; + UpdateWorktree update_worktree = 34; + UpdateWorktreeExtensions update_worktree_extensions = 35; - CreateProjectEntry create_project_entry = 34; - RenameProjectEntry rename_project_entry = 35; - CopyProjectEntry copy_project_entry = 36; - DeleteProjectEntry delete_project_entry = 37; - ProjectEntryResponse project_entry_response = 38; + CreateProjectEntry create_project_entry = 36; + RenameProjectEntry rename_project_entry = 37; + CopyProjectEntry copy_project_entry = 38; + DeleteProjectEntry delete_project_entry = 39; + ProjectEntryResponse project_entry_response = 40; - UpdateDiagnosticSummary update_diagnostic_summary = 39; - StartLanguageServer start_language_server = 40; - UpdateLanguageServer update_language_server = 41; + UpdateDiagnosticSummary update_diagnostic_summary = 41; + StartLanguageServer start_language_server = 42; + UpdateLanguageServer update_language_server = 43; - OpenBufferById open_buffer_by_id = 42; - OpenBufferByPath open_buffer_by_path = 43; - OpenBufferResponse open_buffer_response = 44; - UpdateBuffer update_buffer = 45; - UpdateBufferFile update_buffer_file = 46; - SaveBuffer save_buffer = 47; - BufferSaved buffer_saved = 48; - BufferReloaded buffer_reloaded = 49; - ReloadBuffers reload_buffers = 50; - ReloadBuffersResponse reload_buffers_response = 51; - FormatBuffers format_buffers = 52; - FormatBuffersResponse format_buffers_response = 53; - GetCompletions get_completions = 54; - GetCompletionsResponse get_completions_response = 55; - ApplyCompletionAdditionalEdits apply_completion_additional_edits = 56; - ApplyCompletionAdditionalEditsResponse apply_completion_additional_edits_response = 57; - GetCodeActions get_code_actions = 58; - GetCodeActionsResponse get_code_actions_response = 59; - GetHover get_hover = 60; - GetHoverResponse get_hover_response = 61; - ApplyCodeAction apply_code_action = 62; - ApplyCodeActionResponse apply_code_action_response = 63; - PrepareRename prepare_rename = 64; - PrepareRenameResponse prepare_rename_response = 65; - PerformRename perform_rename = 66; - PerformRenameResponse perform_rename_response = 67; - SearchProject search_project = 68; - SearchProjectResponse search_project_response = 69; + OpenBufferById open_buffer_by_id = 44; + OpenBufferByPath open_buffer_by_path = 45; + OpenBufferResponse open_buffer_response = 46; + UpdateBuffer update_buffer = 47; + UpdateBufferFile update_buffer_file = 48; + SaveBuffer save_buffer = 49; + BufferSaved buffer_saved = 50; + BufferReloaded buffer_reloaded = 51; + ReloadBuffers reload_buffers = 52; + ReloadBuffersResponse reload_buffers_response = 53; + FormatBuffers format_buffers = 54; + FormatBuffersResponse format_buffers_response = 55; + GetCompletions get_completions = 56; + GetCompletionsResponse get_completions_response = 57; + ApplyCompletionAdditionalEdits apply_completion_additional_edits = 58; + ApplyCompletionAdditionalEditsResponse apply_completion_additional_edits_response = 59; + GetCodeActions get_code_actions = 60; + GetCodeActionsResponse get_code_actions_response = 61; + GetHover get_hover = 62; + GetHoverResponse get_hover_response = 63; + ApplyCodeAction apply_code_action = 64; + ApplyCodeActionResponse apply_code_action_response = 65; + PrepareRename prepare_rename = 66; + PrepareRenameResponse prepare_rename_response = 67; + PerformRename perform_rename = 68; + PerformRenameResponse perform_rename_response = 69; + SearchProject search_project = 70; + SearchProjectResponse search_project_response = 71; - GetChannels get_channels = 70; - GetChannelsResponse get_channels_response = 71; - JoinChannel join_channel = 72; - JoinChannelResponse join_channel_response = 73; - LeaveChannel leave_channel = 74; - SendChannelMessage send_channel_message = 75; - SendChannelMessageResponse send_channel_message_response = 76; - ChannelMessageSent channel_message_sent = 77; - GetChannelMessages get_channel_messages = 78; - GetChannelMessagesResponse get_channel_messages_response = 79; + GetChannels get_channels = 72; + GetChannelsResponse get_channels_response = 73; + JoinChannel join_channel = 74; + JoinChannelResponse join_channel_response = 75; + LeaveChannel leave_channel = 76; + SendChannelMessage send_channel_message = 77; + SendChannelMessageResponse send_channel_message_response = 78; + ChannelMessageSent channel_message_sent = 79; + GetChannelMessages get_channel_messages = 80; + GetChannelMessagesResponse get_channel_messages_response = 81; - UpdateContacts update_contacts = 80; - UpdateInviteInfo update_invite_info = 81; - ShowContacts show_contacts = 82; + UpdateContacts update_contacts = 82; + UpdateInviteInfo update_invite_info = 83; + ShowContacts show_contacts = 84; - GetUsers get_users = 83; - FuzzySearchUsers fuzzy_search_users = 84; - UsersResponse users_response = 85; - RequestContact request_contact = 86; - RespondToContactRequest respond_to_contact_request = 87; - RemoveContact remove_contact = 88; + GetUsers get_users = 85; + FuzzySearchUsers fuzzy_search_users = 86; + UsersResponse users_response = 87; + RequestContact request_contact = 88; + RespondToContactRequest respond_to_contact_request = 89; + RemoveContact remove_contact = 90; - Follow follow = 89; - FollowResponse follow_response = 90; - UpdateFollowers update_followers = 91; - Unfollow unfollow = 92; + Follow follow = 91; + FollowResponse follow_response = 92; + UpdateFollowers update_followers = 93; + Unfollow unfollow = 94; } } @@ -263,6 +265,17 @@ message GetDefinitionResponse { repeated LocationLink links = 1; } +message GetTypeDefinition { + uint64 project_id = 1; + uint64 buffer_id = 2; + Anchor position = 3; + repeated VectorClockEntry version = 4; + } + +message GetTypeDefinitionResponse { + repeated LocationLink links = 1; +} + message GetReferences { uint64 project_id = 1; uint64 buffer_id = 2; diff --git a/crates/rpc/src/proto.rs b/crates/rpc/src/proto.rs index e3844a8692..8cd5ca36fb 100644 --- a/crates/rpc/src/proto.rs +++ b/crates/rpc/src/proto.rs @@ -106,6 +106,8 @@ messages!( (GetCompletionsResponse, Background), (GetDefinition, Background), (GetDefinitionResponse, Background), + (GetTypeDefinition, Background), + (GetTypeDefinitionResponse, Background), (GetDocumentHighlights, Background), (GetDocumentHighlightsResponse, Background), (GetReferences, Background), @@ -183,6 +185,7 @@ request_messages!( (GetHover, GetHoverResponse), (GetCompletions, GetCompletionsResponse), (GetDefinition, GetDefinitionResponse), + (GetTypeDefinition, GetTypeDefinitionResponse), (GetDocumentHighlights, GetDocumentHighlightsResponse), (GetReferences, GetReferencesResponse), (GetProjectSymbols, GetProjectSymbolsResponse), @@ -226,6 +229,7 @@ entity_messages!( GetCodeActions, GetCompletions, GetDefinition, + GetTypeDefinition, GetDocumentHighlights, GetHover, GetReferences, diff --git a/crates/rpc/src/rpc.rs b/crates/rpc/src/rpc.rs index 015ac10707..c4017015f9 100644 --- a/crates/rpc/src/rpc.rs +++ b/crates/rpc/src/rpc.rs @@ -6,4 +6,4 @@ pub use conn::Connection; pub use peer::*; mod macros; -pub const PROTOCOL_VERSION: u32 = 28; +pub const PROTOCOL_VERSION: u32 = 29; diff --git a/crates/zed/src/menus.rs b/crates/zed/src/menus.rs index e8297a1727..73817ca2e3 100644 --- a/crates/zed/src/menus.rs +++ b/crates/zed/src/menus.rs @@ -274,6 +274,10 @@ pub fn menus() -> Vec> { name: "Go to Definition", action: Box::new(editor::GoToDefinition), }, + MenuItem::Action { + name: "Go to Type Definition", + action: Box::new(editor::GoToTypeDefinition), + }, MenuItem::Action { name: "Go to References", action: Box::new(editor::FindAllReferences), From 2c70583ef0eeaab3a3855b047c67b1758578d041 Mon Sep 17 00:00:00 2001 From: ForLoveOfCats Date: Fri, 29 Jul 2022 00:11:55 -0400 Subject: [PATCH 36/42] Add `cmd+shift+click` action for triggering go to type definition --- crates/editor/src/element.rs | 41 ++++-- crates/editor/src/link_go_to_definition.rs | 155 +++++++++++++++++---- crates/gpui/src/platform/event.rs | 4 +- 3 files changed, 159 insertions(+), 41 deletions(-) diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index b8bee49d8d..fa4c126671 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -6,7 +6,9 @@ use super::{ use crate::{ display_map::{BlockStyle, DisplaySnapshot, TransformBlock}, hover_popover::HoverAt, - link_go_to_definition::{CmdChanged, GoToFetchedDefinition, UpdateGoToDefinitionLink}, + link_go_to_definition::{ + CmdShiftChanged, GoToFetchedDefinition, GoToFetchedTypeDefinition, UpdateGoToDefinitionLink, + }, mouse_context_menu::DeployMouseContextMenu, EditorStyle, }; @@ -122,7 +124,12 @@ impl EditorElement { if cmd && paint.text_bounds.contains_point(position) { let (point, overshoot) = paint.point_for_position(&self.snapshot(cx), layout, position); if overshoot.is_zero() { - cx.dispatch_action(GoToFetchedDefinition { point }); + if shift { + cx.dispatch_action(GoToFetchedTypeDefinition { point }); + } else { + cx.dispatch_action(GoToFetchedDefinition { point }); + } + return true; } } @@ -238,8 +245,12 @@ impl EditorElement { fn mouse_moved( &self, - position: Vector2F, - cmd: bool, + MouseMovedEvent { + cmd, + shift, + position, + .. + }: MouseMovedEvent, layout: &LayoutState, paint: &PaintState, cx: &mut EventContext, @@ -260,6 +271,7 @@ impl EditorElement { cx.dispatch_action(UpdateGoToDefinitionLink { point, cmd_held: cmd, + shift_held: shift, }); if paint @@ -283,8 +295,11 @@ impl EditorElement { true } - fn modifiers_changed(&self, cmd: bool, cx: &mut EventContext) -> bool { - cx.dispatch_action(CmdChanged { cmd_down: cmd }); + fn modifiers_changed(&self, event: ModifiersChangedEvent, cx: &mut EventContext) -> bool { + cx.dispatch_action(CmdShiftChanged { + cmd_down: event.cmd, + shift_down: event.shift, + }); false } @@ -1534,32 +1549,34 @@ impl Element for EditorElement { paint, cx, ), + Event::MouseDown(MouseButtonEvent { button: MouseButton::Right, position, .. }) => self.mouse_right_down(*position, layout, paint, cx), + Event::MouseUp(MouseButtonEvent { button: MouseButton::Left, position, .. }) => self.mouse_up(*position, cx), + Event::MouseMoved(MouseMovedEvent { pressed_button: Some(MouseButton::Left), position, .. }) => self.mouse_dragged(*position, layout, paint, cx), + Event::ScrollWheel(ScrollWheelEvent { position, delta, precise, }) => self.scroll(*position, *delta, *precise, layout, paint, cx), - Event::ModifiersChanged(ModifiersChangedEvent { cmd, .. }) => { - self.modifiers_changed(*cmd, cx) - } - Event::MouseMoved(MouseMovedEvent { position, cmd, .. }) => { - self.mouse_moved(*position, *cmd, layout, paint, cx) - } + + &Event::ModifiersChanged(event) => self.modifiers_changed(event, cx), + + &Event::MouseMoved(event) => self.mouse_moved(event, layout, paint, cx), _ => false, } diff --git a/crates/editor/src/link_go_to_definition.rs b/crates/editor/src/link_go_to_definition.rs index f0dc359b4b..3f8b34b6e3 100644 --- a/crates/editor/src/link_go_to_definition.rs +++ b/crates/editor/src/link_go_to_definition.rs @@ -8,18 +8,21 @@ use util::TryFutureExt; use workspace::Workspace; use crate::{ - Anchor, DisplayPoint, Editor, EditorSnapshot, Event, GoToDefinition, Select, SelectPhase, + Anchor, DisplayPoint, Editor, EditorSnapshot, Event, GoToDefinition, GoToTypeDefinition, + Select, SelectPhase, }; #[derive(Clone, PartialEq)] pub struct UpdateGoToDefinitionLink { pub point: Option, pub cmd_held: bool, + pub shift_held: bool, } #[derive(Clone, PartialEq)] -pub struct CmdChanged { +pub struct CmdShiftChanged { pub cmd_down: bool, + pub shift_down: bool, } #[derive(Clone, PartialEq)] @@ -27,28 +30,44 @@ pub struct GoToFetchedDefinition { pub point: DisplayPoint, } +#[derive(Clone, PartialEq)] +pub struct GoToFetchedTypeDefinition { + pub point: DisplayPoint, +} + impl_internal_actions!( editor, - [UpdateGoToDefinitionLink, CmdChanged, GoToFetchedDefinition] + [ + UpdateGoToDefinitionLink, + CmdShiftChanged, + GoToFetchedDefinition, + GoToFetchedTypeDefinition + ] ); pub fn init(cx: &mut MutableAppContext) { cx.add_action(update_go_to_definition_link); - cx.add_action(cmd_changed); + cx.add_action(cmd_shift_changed); cx.add_action(go_to_fetched_definition); + cx.add_action(go_to_fetched_type_definition); } #[derive(Default)] pub struct LinkGoToDefinitionState { pub last_mouse_location: Option, pub symbol_range: Option>, + pub kind: Option, pub definitions: Vec, pub task: Option>>, } pub fn update_go_to_definition_link( editor: &mut Editor, - &UpdateGoToDefinitionLink { point, cmd_held }: &UpdateGoToDefinitionLink, + &UpdateGoToDefinitionLink { + point, + cmd_held, + shift_held, + }: &UpdateGoToDefinitionLink, cx: &mut ViewContext, ) { // Store new mouse point as an anchor @@ -72,7 +91,13 @@ pub fn update_go_to_definition_link( editor.link_go_to_definition_state.last_mouse_location = point.clone(); if cmd_held { if let Some(point) = point { - show_link_definition(editor, point, snapshot, cx); + let kind = if shift_held { + LinkDefinitionKind::Type + } else { + LinkDefinitionKind::Symbol + }; + + show_link_definition(kind, editor, point, snapshot, cx); return; } } @@ -80,9 +105,12 @@ pub fn update_go_to_definition_link( hide_link_definition(editor, cx); } -pub fn cmd_changed( +pub fn cmd_shift_changed( editor: &mut Editor, - &CmdChanged { cmd_down }: &CmdChanged, + &CmdShiftChanged { + cmd_down, + shift_down, + }: &CmdShiftChanged, cx: &mut ViewContext, ) { if let Some(point) = editor @@ -92,19 +120,37 @@ pub fn cmd_changed( { if cmd_down { let snapshot = editor.snapshot(cx); - show_link_definition(editor, point.clone(), snapshot, cx); + let kind = if shift_down { + LinkDefinitionKind::Type + } else { + LinkDefinitionKind::Symbol + }; + + show_link_definition(kind, editor, point.clone(), snapshot, cx); } else { hide_link_definition(editor, cx) } } } +#[derive(Debug, Clone, Copy, PartialEq)] +pub enum LinkDefinitionKind { + Symbol, + Type, +} + pub fn show_link_definition( + definition_kind: LinkDefinitionKind, editor: &mut Editor, trigger_point: Anchor, snapshot: EditorSnapshot, cx: &mut ViewContext, ) { + let same_kind = editor.link_go_to_definition_state.kind == Some(definition_kind); + if !same_kind { + hide_link_definition(editor, cx); + } + if editor.pending_rename.is_some() { return; } @@ -135,17 +181,22 @@ pub fn show_link_definition( return; }; - // Don't request again if the location is within the symbol region of a previous request + // Don't request again if the location is within the symbol region of a previous request with the same kind if let Some(symbol_range) = &editor.link_go_to_definition_state.symbol_range { - if symbol_range + let point_after_start = symbol_range .start .cmp(&trigger_point, &snapshot.buffer_snapshot) - .is_le() - && symbol_range - .end - .cmp(&trigger_point, &snapshot.buffer_snapshot) - .is_ge() - { + .is_le(); + + let point_before_end = symbol_range + .end + .cmp(&trigger_point, &snapshot.buffer_snapshot) + .is_ge(); + + let point_within_range = point_after_start && point_before_end; + let same_kind = editor.link_go_to_definition_state.kind == Some(definition_kind); + + if point_within_range && same_kind { return; } } @@ -154,8 +205,14 @@ pub fn show_link_definition( async move { // query the LSP for definition info let definition_request = cx.update(|cx| { - project.update(cx, |project, cx| { - project.definition(&buffer, buffer_position.clone(), cx) + project.update(cx, |project, cx| match definition_kind { + LinkDefinitionKind::Symbol => { + project.definition(&buffer, buffer_position.clone(), cx) + } + + LinkDefinitionKind::Type => { + project.type_definition(&buffer, buffer_position.clone(), cx) + } }) }); @@ -181,6 +238,7 @@ pub fn show_link_definition( this.update(&mut cx, |this, cx| { // Clear any existing highlights this.clear_text_highlights::(cx); + this.link_go_to_definition_state.kind = Some(definition_kind); this.link_go_to_definition_state.symbol_range = result .as_ref() .and_then(|(symbol_range, _)| symbol_range.clone()); @@ -258,7 +316,24 @@ pub fn hide_link_definition(editor: &mut Editor, cx: &mut ViewContext) { pub fn go_to_fetched_definition( workspace: &mut Workspace, - GoToFetchedDefinition { point }: &GoToFetchedDefinition, + &GoToFetchedDefinition { point }: &GoToFetchedDefinition, + cx: &mut ViewContext, +) { + go_to_fetched_definition_of_kind(LinkDefinitionKind::Symbol, workspace, point, cx); +} + +pub fn go_to_fetched_type_definition( + workspace: &mut Workspace, + &GoToFetchedTypeDefinition { point }: &GoToFetchedTypeDefinition, + cx: &mut ViewContext, +) { + go_to_fetched_definition_of_kind(LinkDefinitionKind::Type, workspace, point, cx); +} + +fn go_to_fetched_definition_of_kind( + kind: LinkDefinitionKind, + workspace: &mut Workspace, + point: DisplayPoint, cx: &mut ViewContext, ) { let active_item = workspace.active_item(cx); @@ -271,13 +346,14 @@ pub fn go_to_fetched_definition( return; }; - let definitions = editor_handle.update(cx, |editor, cx| { + let (cached_definitions, cached_definitions_kind) = editor_handle.update(cx, |editor, cx| { let definitions = editor.link_go_to_definition_state.definitions.clone(); hide_link_definition(editor, cx); - definitions + (definitions, editor.link_go_to_definition_state.kind) }); - if !definitions.is_empty() { + let is_correct_kind = cached_definitions_kind == Some(kind); + if !cached_definitions.is_empty() && is_correct_kind { editor_handle.update(cx, |editor, cx| { if !editor.focused { cx.focus_self(); @@ -285,7 +361,7 @@ pub fn go_to_fetched_definition( } }); - Editor::navigate_to_definitions(workspace, editor_handle, definitions, cx); + Editor::navigate_to_definitions(workspace, editor_handle, cached_definitions, cx); } else { editor_handle.update(cx, |editor, cx| { editor.select( @@ -298,7 +374,13 @@ pub fn go_to_fetched_definition( ); }); - Editor::go_to_definition(workspace, &GoToDefinition, cx); + match kind { + LinkDefinitionKind::Symbol => Editor::go_to_definition(workspace, &GoToDefinition, cx), + + LinkDefinitionKind::Type => { + Editor::go_to_type_definition(workspace, &GoToTypeDefinition, cx) + } + } } } @@ -367,6 +449,7 @@ mod tests { &UpdateGoToDefinitionLink { point: Some(hover_point), cmd_held: true, + shift_held: false, }, cx, ); @@ -382,7 +465,14 @@ mod tests { // Unpress cmd causes highlight to go away cx.update_editor(|editor, cx| { - cmd_changed(editor, &CmdChanged { cmd_down: false }, cx); + cmd_shift_changed( + editor, + &CmdShiftChanged { + cmd_down: false, + shift_down: false, + }, + cx, + ); }); // Assert no link highlights cx.assert_editor_text_highlights::(indoc! {" @@ -412,6 +502,7 @@ mod tests { &UpdateGoToDefinitionLink { point: Some(hover_point), cmd_held: true, + shift_held: false, }, cx, ); @@ -445,6 +536,7 @@ mod tests { &UpdateGoToDefinitionLink { point: Some(hover_point), cmd_held: true, + shift_held: false, }, cx, ); @@ -473,6 +565,7 @@ mod tests { &UpdateGoToDefinitionLink { point: Some(hover_point), cmd_held: false, + shift_held: false, }, cx, ); @@ -512,7 +605,14 @@ mod tests { ]))) }); cx.update_editor(|editor, cx| { - cmd_changed(editor, &CmdChanged { cmd_down: true }, cx); + cmd_shift_changed( + editor, + &CmdShiftChanged { + cmd_down: true, + shift_down: false, + }, + cx, + ); }); requests.next().await; cx.foreground().run_until_parked(); @@ -537,6 +637,7 @@ mod tests { &UpdateGoToDefinitionLink { point: Some(hover_point), cmd_held: true, + shift_held: false, }, cx, ); diff --git a/crates/gpui/src/platform/event.rs b/crates/gpui/src/platform/event.rs index e833bd8eb5..6ac75926be 100644 --- a/crates/gpui/src/platform/event.rs +++ b/crates/gpui/src/platform/event.rs @@ -11,7 +11,7 @@ pub struct KeyUpEvent { pub keystroke: Keystroke, } -#[derive(Clone, Debug)] +#[derive(Clone, Copy, Debug)] pub struct ModifiersChangedEvent { pub ctrl: bool, pub alt: bool, @@ -19,7 +19,7 @@ pub struct ModifiersChangedEvent { pub cmd: bool, } -#[derive(Clone, Debug, Default)] +#[derive(Clone, Copy, Debug, Default)] pub struct ScrollWheelEvent { pub position: Vector2F, pub delta: Vector2F, From 479fbee57441b7635bf3b109d179f0d62e531e80 Mon Sep 17 00:00:00 2001 From: ForLoveOfCats Date: Fri, 29 Jul 2022 16:59:54 -0400 Subject: [PATCH 37/42] Add test cases for `cmd+shift+click" behavior --- crates/editor/src/link_go_to_definition.rs | 284 +++++++++++++++------ 1 file changed, 208 insertions(+), 76 deletions(-) diff --git a/crates/editor/src/link_go_to_definition.rs b/crates/editor/src/link_go_to_definition.rs index 3f8b34b6e3..b57179c07d 100644 --- a/crates/editor/src/link_go_to_definition.rs +++ b/crates/editor/src/link_go_to_definition.rs @@ -194,8 +194,6 @@ pub fn show_link_definition( .is_ge(); let point_within_range = point_after_start && point_before_end; - let same_kind = editor.link_go_to_definition_state.kind == Some(definition_kind); - if point_within_range && same_kind { return; } @@ -388,11 +386,128 @@ fn go_to_fetched_definition_of_kind( mod tests { use futures::StreamExt; use indoc::indoc; + use lsp::request::{GotoDefinition, GotoTypeDefinition}; use crate::test::EditorLspTestContext; use super::*; + #[gpui::test] + async fn test_link_go_to_type_definition(cx: &mut gpui::TestAppContext) { + let mut cx = EditorLspTestContext::new_rust( + lsp::ServerCapabilities { + hover_provider: Some(lsp::HoverProviderCapability::Simple(true)), + ..Default::default() + }, + cx, + ) + .await; + + cx.set_state(indoc! {" + struct A; + let v|ariable = A; + "}); + + // Basic hold cmd+shift, expect highlight in region if response contains type definition + let hover_point = cx.display_point(indoc! {" + struct A; + let v|ariable = A; + "}); + let symbol_range = cx.lsp_range(indoc! {" + struct A; + let [variable] = A; + "}); + let target_range = cx.lsp_range(indoc! {" + struct [A]; + let variable = A; + "}); + + let mut requests = + cx.handle_request::(move |url, _, _| async move { + Ok(Some(lsp::GotoTypeDefinitionResponse::Link(vec![ + lsp::LocationLink { + origin_selection_range: Some(symbol_range), + target_uri: url.clone(), + target_range, + target_selection_range: target_range, + }, + ]))) + }); + + // Press cmd+shift to trigger highlight + cx.update_editor(|editor, cx| { + update_go_to_definition_link( + editor, + &UpdateGoToDefinitionLink { + point: Some(hover_point), + cmd_held: true, + shift_held: true, + }, + cx, + ); + }); + requests.next().await; + cx.foreground().run_until_parked(); + cx.assert_editor_text_highlights::(indoc! {" + struct A; + let [variable] = A; + "}); + + // Unpress shift causes highlight to go away (normal goto-definition is not valid here) + cx.update_editor(|editor, cx| { + cmd_shift_changed( + editor, + &CmdShiftChanged { + cmd_down: true, + shift_down: false, + }, + cx, + ); + }); + // Assert no link highlights + cx.assert_editor_text_highlights::(indoc! {" + struct A; + let variable = A; + "}); + + // Cmd+shift click without existing definition requests and jumps + let hover_point = cx.display_point(indoc! {" + struct A; + let v|ariable = A; + "}); + let target_range = cx.lsp_range(indoc! {" + struct [A]; + let variable = A; + "}); + + let mut requests = + cx.handle_request::(move |url, _, _| async move { + Ok(Some(lsp::GotoTypeDefinitionResponse::Link(vec![ + lsp::LocationLink { + origin_selection_range: None, + target_uri: url, + target_range, + target_selection_range: target_range, + }, + ]))) + }); + + cx.update_workspace(|workspace, cx| { + go_to_fetched_type_definition( + workspace, + &GoToFetchedTypeDefinition { point: hover_point }, + cx, + ); + }); + requests.next().await; + cx.foreground().run_until_parked(); + + cx.assert_editor_state(indoc! {" + struct [A}; + let variable = A; + "}); + } + #[gpui::test] async fn test_link_go_to_definition(cx: &mut gpui::TestAppContext) { let mut cx = EditorLspTestContext::new_rust( @@ -409,7 +524,8 @@ mod tests { do_work(); fn do_work() - test();"}); + test(); + "}); // Basic hold cmd, expect highlight in region if response contains definition let hover_point = cx.display_point(indoc! {" @@ -417,32 +533,34 @@ mod tests { do_w|ork(); fn do_work() - test();"}); - + test(); + "}); let symbol_range = cx.lsp_range(indoc! {" fn test() [do_work](); fn do_work() - test();"}); + test(); + "}); let target_range = cx.lsp_range(indoc! {" fn test() do_work(); fn [do_work]() - test();"}); + test(); + "}); + + let mut requests = cx.handle_request::(move |url, _, _| async move { + Ok(Some(lsp::GotoDefinitionResponse::Link(vec![ + lsp::LocationLink { + origin_selection_range: Some(symbol_range), + target_uri: url.clone(), + target_range, + target_selection_range: target_range, + }, + ]))) + }); - let mut requests = - cx.handle_request::(move |url, _, _| async move { - Ok(Some(lsp::GotoDefinitionResponse::Link(vec![ - lsp::LocationLink { - origin_selection_range: Some(symbol_range), - target_uri: url.clone(), - target_range, - target_selection_range: target_range, - }, - ]))) - }); cx.update_editor(|editor, cx| { update_go_to_definition_link( editor, @@ -461,7 +579,8 @@ mod tests { [do_work](); fn do_work() - test();"}); + test(); + "}); // Unpress cmd causes highlight to go away cx.update_editor(|editor, cx| { @@ -480,22 +599,22 @@ mod tests { do_work(); fn do_work() - test();"}); + test(); + "}); // Response without source range still highlights word cx.update_editor(|editor, _| editor.link_go_to_definition_state.last_mouse_location = None); - let mut requests = - cx.handle_request::(move |url, _, _| async move { - Ok(Some(lsp::GotoDefinitionResponse::Link(vec![ - lsp::LocationLink { - // No origin range - origin_selection_range: None, - target_uri: url.clone(), - target_range, - target_selection_range: target_range, - }, - ]))) - }); + let mut requests = cx.handle_request::(move |url, _, _| async move { + Ok(Some(lsp::GotoDefinitionResponse::Link(vec![ + lsp::LocationLink { + // No origin range + origin_selection_range: None, + target_uri: url.clone(), + target_range, + target_selection_range: target_range, + }, + ]))) + }); cx.update_editor(|editor, cx| { update_go_to_definition_link( editor, @@ -515,7 +634,8 @@ mod tests { [do_work](); fn do_work() - test();"}); + test(); + "}); // Moving mouse to location with no response dismisses highlight let hover_point = cx.display_point(indoc! {" @@ -523,13 +643,14 @@ mod tests { do_work(); fn do_work() - test();"}); - let mut requests = - cx.lsp - .handle_request::(move |_, _| async move { - // No definitions returned - Ok(Some(lsp::GotoDefinitionResponse::Link(vec![]))) - }); + test(); + "}); + let mut requests = cx + .lsp + .handle_request::(move |_, _| async move { + // No definitions returned + Ok(Some(lsp::GotoDefinitionResponse::Link(vec![]))) + }); cx.update_editor(|editor, cx| { update_go_to_definition_link( editor, @@ -550,7 +671,8 @@ mod tests { do_work(); fn do_work() - test();"}); + test(); + "}); // Move mouse without cmd and then pressing cmd triggers highlight let hover_point = cx.display_point(indoc! {" @@ -558,7 +680,8 @@ mod tests { do_work(); fn do_work() - te|st();"}); + te|st(); + "}); cx.update_editor(|editor, cx| { update_go_to_definition_link( editor, @@ -578,32 +701,34 @@ mod tests { do_work(); fn do_work() - test();"}); + test(); + "}); let symbol_range = cx.lsp_range(indoc! {" fn test() do_work(); fn do_work() - [test]();"}); + [test](); + "}); let target_range = cx.lsp_range(indoc! {" fn [test]() do_work(); fn do_work() - test();"}); + test(); + "}); - let mut requests = - cx.handle_request::(move |url, _, _| async move { - Ok(Some(lsp::GotoDefinitionResponse::Link(vec![ - lsp::LocationLink { - origin_selection_range: Some(symbol_range), - target_uri: url, - target_range, - target_selection_range: target_range, - }, - ]))) - }); + let mut requests = cx.handle_request::(move |url, _, _| async move { + Ok(Some(lsp::GotoDefinitionResponse::Link(vec![ + lsp::LocationLink { + origin_selection_range: Some(symbol_range), + target_uri: url, + target_range, + target_selection_range: target_range, + }, + ]))) + }); cx.update_editor(|editor, cx| { cmd_shift_changed( editor, @@ -622,7 +747,8 @@ mod tests { do_work(); fn do_work() - [test]();"}); + [test](); + "}); // Moving within symbol range doesn't re-request let hover_point = cx.display_point(indoc! {" @@ -630,7 +756,8 @@ mod tests { do_work(); fn do_work() - tes|t();"}); + tes|t(); + "}); cx.update_editor(|editor, cx| { update_go_to_definition_link( editor, @@ -648,7 +775,8 @@ mod tests { do_work(); fn do_work() - [test]();"}); + [test](); + "}); // Cmd click with existing definition doesn't re-request and dismisses highlight cx.update_workspace(|workspace, cx| { @@ -656,7 +784,7 @@ mod tests { }); // Assert selection moved to to definition cx.lsp - .handle_request::(move |_, _| async move { + .handle_request::(move |_, _| async move { // Empty definition response to make sure we aren't hitting the lsp and using // the cached location instead Ok(Some(lsp::GotoDefinitionResponse::Link(vec![]))) @@ -666,14 +794,16 @@ mod tests { do_work(); fn do_work() - test();"}); + test(); + "}); // Assert no link highlights after jump cx.assert_editor_text_highlights::(indoc! {" fn test() do_work(); fn do_work() - test();"}); + test(); + "}); // Cmd click without existing definition requests and jumps let hover_point = cx.display_point(indoc! {" @@ -681,25 +811,26 @@ mod tests { do_w|ork(); fn do_work() - test();"}); + test(); + "}); let target_range = cx.lsp_range(indoc! {" fn test() do_work(); fn [do_work]() - test();"}); + test(); + "}); - let mut requests = - cx.handle_request::(move |url, _, _| async move { - Ok(Some(lsp::GotoDefinitionResponse::Link(vec![ - lsp::LocationLink { - origin_selection_range: None, - target_uri: url, - target_range, - target_selection_range: target_range, - }, - ]))) - }); + let mut requests = cx.handle_request::(move |url, _, _| async move { + Ok(Some(lsp::GotoDefinitionResponse::Link(vec![ + lsp::LocationLink { + origin_selection_range: None, + target_uri: url, + target_range, + target_selection_range: target_range, + }, + ]))) + }); cx.update_workspace(|workspace, cx| { go_to_fetched_definition(workspace, &GoToFetchedDefinition { point: hover_point }, cx); }); @@ -711,6 +842,7 @@ mod tests { do_work(); fn [do_work}() - test();"}); + test(); + "}); } } From d474e1e1f43fe393772350181ab00cf35cf2357c Mon Sep 17 00:00:00 2001 From: ForLoveOfCats Date: Fri, 29 Jul 2022 18:04:14 -0400 Subject: [PATCH 38/42] Deduplicate some lsp_command code --- crates/project/src/lsp_command.rs | 534 ++++++++++++------------------ 1 file changed, 205 insertions(+), 329 deletions(-) diff --git a/crates/project/src/lsp_command.rs b/crates/project/src/lsp_command.rs index 6ec52451a1..4d679ec9cb 100644 --- a/crates/project/src/lsp_command.rs +++ b/crates/project/src/lsp_command.rs @@ -8,11 +8,11 @@ use gpui::{AppContext, AsyncAppContext, ModelHandle}; use language::{ point_from_lsp, point_to_lsp, proto::{deserialize_anchor, deserialize_version, serialize_anchor, serialize_version}, - range_from_lsp, Anchor, Bias, Buffer, PointUtf16, ToPointUtf16, + range_from_lsp, Anchor, Bias, Buffer, CachedLspAdapter, PointUtf16, ToPointUtf16, }; -use lsp::{DocumentHighlightKind, ServerCapabilities}; +use lsp::{DocumentHighlightKind, LanguageServer, ServerCapabilities}; use pulldown_cmark::{CodeBlockKind, Event, Options, Parser, Tag}; -use std::{cmp::Reverse, ops::Range, path::Path}; +use std::{cmp::Reverse, ops::Range, path::Path, sync::Arc}; #[async_trait(?Send)] pub(crate) trait LspCommand: 'static + Sized { @@ -242,13 +242,7 @@ impl LspCommand for PerformRename { mut cx: AsyncAppContext, ) -> Result { if let Some(edit) = message { - let (lsp_adapter, lsp_server) = project - .read_with(&cx, |project, cx| { - project - .language_server_for_buffer(buffer.read(cx), cx) - .map(|(adapter, server)| (adapter.clone(), server.clone())) - }) - .ok_or_else(|| anyhow!("no language server found for buffer"))?; + let (lsp_adapter, lsp_server) = language_server_for_buffer(&project, &buffer, &mut cx)?; Project::deserialize_workspace_edit( project, edit, @@ -356,83 +350,9 @@ impl LspCommand for GetDefinition { message: Option, project: ModelHandle, buffer: ModelHandle, - mut cx: AsyncAppContext, + cx: AsyncAppContext, ) -> Result> { - let mut definitions = Vec::new(); - let (lsp_adapter, language_server) = project - .read_with(&cx, |project, cx| { - project - .language_server_for_buffer(buffer.read(cx), cx) - .map(|(adapter, server)| (adapter.clone(), server.clone())) - }) - .ok_or_else(|| anyhow!("no language server found for buffer"))?; - - if let Some(message) = message { - let mut unresolved_links = Vec::new(); - match message { - lsp::GotoDefinitionResponse::Scalar(loc) => { - unresolved_links.push((None, loc.uri, loc.range)); - } - lsp::GotoDefinitionResponse::Array(locs) => { - unresolved_links.extend(locs.into_iter().map(|l| (None, l.uri, l.range))); - } - lsp::GotoDefinitionResponse::Link(links) => { - unresolved_links.extend(links.into_iter().map(|l| { - ( - l.origin_selection_range, - l.target_uri, - l.target_selection_range, - ) - })); - } - } - - for (origin_range, target_uri, target_range) in unresolved_links { - let target_buffer_handle = project - .update(&mut cx, |this, cx| { - this.open_local_buffer_via_lsp( - target_uri, - language_server.server_id(), - lsp_adapter.name.clone(), - cx, - ) - }) - .await?; - - cx.read(|cx| { - let origin_location = origin_range.map(|origin_range| { - let origin_buffer = buffer.read(cx); - let origin_start = origin_buffer - .clip_point_utf16(point_from_lsp(origin_range.start), Bias::Left); - let origin_end = origin_buffer - .clip_point_utf16(point_from_lsp(origin_range.end), Bias::Left); - Location { - buffer: buffer.clone(), - range: origin_buffer.anchor_after(origin_start) - ..origin_buffer.anchor_before(origin_end), - } - }); - - let target_buffer = target_buffer_handle.read(cx); - let target_start = target_buffer - .clip_point_utf16(point_from_lsp(target_range.start), Bias::Left); - let target_end = target_buffer - .clip_point_utf16(point_from_lsp(target_range.end), Bias::Left); - let target_location = Location { - buffer: target_buffer_handle, - range: target_buffer.anchor_after(target_start) - ..target_buffer.anchor_before(target_end), - }; - - definitions.push(LocationLink { - origin: origin_location, - target: target_location, - }) - }); - } - } - - Ok(definitions) + location_links_from_lsp(message, project, buffer, cx).await } fn to_proto(&self, project_id: u64, buffer: &Buffer) -> proto::GetDefinition { @@ -473,32 +393,7 @@ impl LspCommand for GetDefinition { _: &clock::Global, cx: &AppContext, ) -> proto::GetDefinitionResponse { - let links = response - .into_iter() - .map(|definition| { - let origin = definition.origin.map(|origin| { - let buffer = project.serialize_buffer_for_peer(&origin.buffer, peer_id, cx); - proto::Location { - start: Some(serialize_anchor(&origin.range.start)), - end: Some(serialize_anchor(&origin.range.end)), - buffer: Some(buffer), - } - }); - - let buffer = - project.serialize_buffer_for_peer(&definition.target.buffer, peer_id, cx); - let target = proto::Location { - start: Some(serialize_anchor(&definition.target.range.start)), - end: Some(serialize_anchor(&definition.target.range.end)), - buffer: Some(buffer), - }; - - proto::LocationLink { - origin, - target: Some(target), - } - }) - .collect(); + let links = location_links_to_proto(response, project, peer_id, cx); proto::GetDefinitionResponse { links } } @@ -507,61 +402,9 @@ impl LspCommand for GetDefinition { message: proto::GetDefinitionResponse, project: ModelHandle, _: ModelHandle, - mut cx: AsyncAppContext, + cx: AsyncAppContext, ) -> Result> { - let mut links = Vec::new(); - for link in message.links { - let origin = match link.origin { - Some(origin) => { - let buffer = origin - .buffer - .ok_or_else(|| anyhow!("missing origin buffer"))?; - let buffer = project - .update(&mut cx, |this, cx| this.deserialize_buffer(buffer, cx)) - .await?; - let start = origin - .start - .and_then(deserialize_anchor) - .ok_or_else(|| anyhow!("missing origin start"))?; - let end = origin - .end - .and_then(deserialize_anchor) - .ok_or_else(|| anyhow!("missing origin end"))?; - buffer - .update(&mut cx, |buffer, _| buffer.wait_for_anchors([&start, &end])) - .await; - Some(Location { - buffer, - range: start..end, - }) - } - None => None, - }; - - let target = link.target.ok_or_else(|| anyhow!("missing target"))?; - let buffer = target.buffer.ok_or_else(|| anyhow!("missing buffer"))?; - let buffer = project - .update(&mut cx, |this, cx| this.deserialize_buffer(buffer, cx)) - .await?; - let start = target - .start - .and_then(deserialize_anchor) - .ok_or_else(|| anyhow!("missing target start"))?; - let end = target - .end - .and_then(deserialize_anchor) - .ok_or_else(|| anyhow!("missing target end"))?; - buffer - .update(&mut cx, |buffer, _| buffer.wait_for_anchors([&start, &end])) - .await; - let target = Location { - buffer, - range: start..end, - }; - - links.push(LocationLink { origin, target }) - } - Ok(links) + location_links_from_proto(message.links, project, cx).await } fn buffer_id_from_proto(message: &proto::GetDefinition) -> u64 { @@ -593,83 +436,9 @@ impl LspCommand for GetTypeDefinition { message: Option, project: ModelHandle, buffer: ModelHandle, - mut cx: AsyncAppContext, + cx: AsyncAppContext, ) -> Result> { - let mut definitions = Vec::new(); - let (lsp_adapter, language_server) = project - .read_with(&cx, |project, cx| { - project - .language_server_for_buffer(buffer.read(cx), cx) - .map(|(adapter, server)| (adapter.clone(), server.clone())) - }) - .ok_or_else(|| anyhow!("no language server found for buffer"))?; - - if let Some(message) = message { - let mut unresolved_links = Vec::new(); - match message { - lsp::GotoTypeDefinitionResponse::Scalar(loc) => { - unresolved_links.push((None, loc.uri, loc.range)); - } - lsp::GotoTypeDefinitionResponse::Array(locs) => { - unresolved_links.extend(locs.into_iter().map(|l| (None, l.uri, l.range))); - } - lsp::GotoTypeDefinitionResponse::Link(links) => { - unresolved_links.extend(links.into_iter().map(|l| { - ( - l.origin_selection_range, - l.target_uri, - l.target_selection_range, - ) - })); - } - } - - for (origin_range, target_uri, target_range) in unresolved_links { - let target_buffer_handle = project - .update(&mut cx, |this, cx| { - this.open_local_buffer_via_lsp( - target_uri, - language_server.server_id(), - lsp_adapter.name.clone(), - cx, - ) - }) - .await?; - - cx.read(|cx| { - let origin_location = origin_range.map(|origin_range| { - let origin_buffer = buffer.read(cx); - let origin_start = origin_buffer - .clip_point_utf16(point_from_lsp(origin_range.start), Bias::Left); - let origin_end = origin_buffer - .clip_point_utf16(point_from_lsp(origin_range.end), Bias::Left); - Location { - buffer: buffer.clone(), - range: origin_buffer.anchor_after(origin_start) - ..origin_buffer.anchor_before(origin_end), - } - }); - - let target_buffer = target_buffer_handle.read(cx); - let target_start = target_buffer - .clip_point_utf16(point_from_lsp(target_range.start), Bias::Left); - let target_end = target_buffer - .clip_point_utf16(point_from_lsp(target_range.end), Bias::Left); - let target_location = Location { - buffer: target_buffer_handle, - range: target_buffer.anchor_after(target_start) - ..target_buffer.anchor_before(target_end), - }; - - definitions.push(LocationLink { - origin: origin_location, - target: target_location, - }) - }); - } - } - - Ok(definitions) + location_links_from_lsp(message, project, buffer, cx).await } fn to_proto(&self, project_id: u64, buffer: &Buffer) -> proto::GetTypeDefinition { @@ -710,32 +479,7 @@ impl LspCommand for GetTypeDefinition { _: &clock::Global, cx: &AppContext, ) -> proto::GetTypeDefinitionResponse { - let links = response - .into_iter() - .map(|definition| { - let origin = definition.origin.map(|origin| { - let buffer = project.serialize_buffer_for_peer(&origin.buffer, peer_id, cx); - proto::Location { - start: Some(serialize_anchor(&origin.range.start)), - end: Some(serialize_anchor(&origin.range.end)), - buffer: Some(buffer), - } - }); - - let buffer = - project.serialize_buffer_for_peer(&definition.target.buffer, peer_id, cx); - let target = proto::Location { - start: Some(serialize_anchor(&definition.target.range.start)), - end: Some(serialize_anchor(&definition.target.range.end)), - buffer: Some(buffer), - }; - - proto::LocationLink { - origin, - target: Some(target), - } - }) - .collect(); + let links = location_links_to_proto(response, project, peer_id, cx); proto::GetTypeDefinitionResponse { links } } @@ -744,61 +488,9 @@ impl LspCommand for GetTypeDefinition { message: proto::GetTypeDefinitionResponse, project: ModelHandle, _: ModelHandle, - mut cx: AsyncAppContext, + cx: AsyncAppContext, ) -> Result> { - let mut links = Vec::new(); - for link in message.links { - let origin = match link.origin { - Some(origin) => { - let buffer = origin - .buffer - .ok_or_else(|| anyhow!("missing origin buffer"))?; - let buffer = project - .update(&mut cx, |this, cx| this.deserialize_buffer(buffer, cx)) - .await?; - let start = origin - .start - .and_then(deserialize_anchor) - .ok_or_else(|| anyhow!("missing origin start"))?; - let end = origin - .end - .and_then(deserialize_anchor) - .ok_or_else(|| anyhow!("missing origin end"))?; - buffer - .update(&mut cx, |buffer, _| buffer.wait_for_anchors([&start, &end])) - .await; - Some(Location { - buffer, - range: start..end, - }) - } - None => None, - }; - - let target = link.target.ok_or_else(|| anyhow!("missing target"))?; - let buffer = target.buffer.ok_or_else(|| anyhow!("missing buffer"))?; - let buffer = project - .update(&mut cx, |this, cx| this.deserialize_buffer(buffer, cx)) - .await?; - let start = target - .start - .and_then(deserialize_anchor) - .ok_or_else(|| anyhow!("missing target start"))?; - let end = target - .end - .and_then(deserialize_anchor) - .ok_or_else(|| anyhow!("missing target end"))?; - buffer - .update(&mut cx, |buffer, _| buffer.wait_for_anchors([&start, &end])) - .await; - let target = Location { - buffer, - range: start..end, - }; - - links.push(LocationLink { origin, target }) - } - Ok(links) + location_links_from_proto(message.links, project, cx).await } fn buffer_id_from_proto(message: &proto::GetTypeDefinition) -> u64 { @@ -806,6 +498,195 @@ impl LspCommand for GetTypeDefinition { } } +fn language_server_for_buffer( + project: &ModelHandle, + buffer: &ModelHandle, + cx: &mut AsyncAppContext, +) -> Result<(Arc, Arc)> { + project + .read_with(cx, |project, cx| { + project + .language_server_for_buffer(buffer.read(cx), cx) + .map(|(adapter, server)| (adapter.clone(), server.clone())) + }) + .ok_or_else(|| anyhow!("no language server found for buffer")) +} + +async fn location_links_from_proto( + proto_links: Vec, + project: ModelHandle, + mut cx: AsyncAppContext, +) -> Result> { + let mut links = Vec::new(); + + for link in proto_links { + let origin = match link.origin { + Some(origin) => { + let buffer = origin + .buffer + .ok_or_else(|| anyhow!("missing origin buffer"))?; + let buffer = project + .update(&mut cx, |this, cx| this.deserialize_buffer(buffer, cx)) + .await?; + let start = origin + .start + .and_then(deserialize_anchor) + .ok_or_else(|| anyhow!("missing origin start"))?; + let end = origin + .end + .and_then(deserialize_anchor) + .ok_or_else(|| anyhow!("missing origin end"))?; + buffer + .update(&mut cx, |buffer, _| buffer.wait_for_anchors([&start, &end])) + .await; + Some(Location { + buffer, + range: start..end, + }) + } + None => None, + }; + + let target = link.target.ok_or_else(|| anyhow!("missing target"))?; + let buffer = target.buffer.ok_or_else(|| anyhow!("missing buffer"))?; + let buffer = project + .update(&mut cx, |this, cx| this.deserialize_buffer(buffer, cx)) + .await?; + let start = target + .start + .and_then(deserialize_anchor) + .ok_or_else(|| anyhow!("missing target start"))?; + let end = target + .end + .and_then(deserialize_anchor) + .ok_or_else(|| anyhow!("missing target end"))?; + buffer + .update(&mut cx, |buffer, _| buffer.wait_for_anchors([&start, &end])) + .await; + let target = Location { + buffer, + range: start..end, + }; + + links.push(LocationLink { origin, target }) + } + + Ok(links) +} + +async fn location_links_from_lsp( + message: Option, + project: ModelHandle, + buffer: ModelHandle, + mut cx: AsyncAppContext, +) -> Result> { + let message = match message { + Some(message) => message, + None => return Ok(Vec::new()), + }; + + let mut unresolved_links = Vec::new(); + match message { + lsp::GotoDefinitionResponse::Scalar(loc) => { + unresolved_links.push((None, loc.uri, loc.range)); + } + + lsp::GotoDefinitionResponse::Array(locs) => { + unresolved_links.extend(locs.into_iter().map(|l| (None, l.uri, l.range))); + } + + lsp::GotoDefinitionResponse::Link(links) => { + unresolved_links.extend(links.into_iter().map(|l| { + ( + l.origin_selection_range, + l.target_uri, + l.target_selection_range, + ) + })); + } + } + + let (lsp_adapter, language_server) = language_server_for_buffer(&project, &buffer, &mut cx)?; + let mut definitions = Vec::new(); + for (origin_range, target_uri, target_range) in unresolved_links { + let target_buffer_handle = project + .update(&mut cx, |this, cx| { + this.open_local_buffer_via_lsp( + target_uri, + language_server.server_id(), + lsp_adapter.name.clone(), + cx, + ) + }) + .await?; + + cx.read(|cx| { + let origin_location = origin_range.map(|origin_range| { + let origin_buffer = buffer.read(cx); + let origin_start = + origin_buffer.clip_point_utf16(point_from_lsp(origin_range.start), Bias::Left); + let origin_end = + origin_buffer.clip_point_utf16(point_from_lsp(origin_range.end), Bias::Left); + Location { + buffer: buffer.clone(), + range: origin_buffer.anchor_after(origin_start) + ..origin_buffer.anchor_before(origin_end), + } + }); + + let target_buffer = target_buffer_handle.read(cx); + let target_start = + target_buffer.clip_point_utf16(point_from_lsp(target_range.start), Bias::Left); + let target_end = + target_buffer.clip_point_utf16(point_from_lsp(target_range.end), Bias::Left); + let target_location = Location { + buffer: target_buffer_handle, + range: target_buffer.anchor_after(target_start) + ..target_buffer.anchor_before(target_end), + }; + + definitions.push(LocationLink { + origin: origin_location, + target: target_location, + }) + }); + } + Ok(definitions) +} + +fn location_links_to_proto( + links: Vec, + project: &mut Project, + peer_id: PeerId, + cx: &AppContext, +) -> Vec { + links + .into_iter() + .map(|definition| { + let origin = definition.origin.map(|origin| { + let buffer = project.serialize_buffer_for_peer(&origin.buffer, peer_id, cx); + proto::Location { + start: Some(serialize_anchor(&origin.range.start)), + end: Some(serialize_anchor(&origin.range.end)), + buffer: Some(buffer), + } + }); + + let buffer = project.serialize_buffer_for_peer(&definition.target.buffer, peer_id, cx); + let target = proto::Location { + start: Some(serialize_anchor(&definition.target.range.start)), + end: Some(serialize_anchor(&definition.target.range.end)), + buffer: Some(buffer), + }; + + proto::LocationLink { + origin, + target: Some(target), + } + }) + .collect() +} + #[async_trait(?Send)] impl LspCommand for GetReferences { type Response = Vec; @@ -836,13 +717,8 @@ impl LspCommand for GetReferences { mut cx: AsyncAppContext, ) -> Result> { let mut references = Vec::new(); - let (lsp_adapter, language_server) = project - .read_with(&cx, |project, cx| { - project - .language_server_for_buffer(buffer.read(cx), cx) - .map(|(adapter, server)| (adapter.clone(), server.clone())) - }) - .ok_or_else(|| anyhow!("no language server found for buffer"))?; + let (lsp_adapter, language_server) = + language_server_for_buffer(&project, &buffer, &mut cx)?; if let Some(locations) = locations { for lsp_location in locations { From bc8bec826142d1cffff891e6b60960fd1da10bbe Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Mon, 1 Aug 2022 12:23:23 +0200 Subject: [PATCH 39/42] Assign 5 invites to users who redeem an invite code --- crates/collab/src/db.rs | 29 ++++++++++++++++++++++++----- 1 file changed, 24 insertions(+), 5 deletions(-) diff --git a/crates/collab/src/db.rs b/crates/collab/src/db.rs index f45a544728..1c1893dc79 100644 --- a/crates/collab/src/db.rs +++ b/crates/collab/src/db.rs @@ -6,7 +6,6 @@ use async_trait::async_trait; use axum::http::StatusCode; use collections::HashMap; use futures::StreamExt; -use nanoid::nanoid; use serde::{Deserialize, Serialize}; pub use sqlx::postgres::PgPoolOptions as DbOptions; use sqlx::{types::Uuid, FromRow, QueryBuilder, Row}; @@ -218,7 +217,7 @@ impl Db for PostgresDb { .push_bind(github_login) .push_bind(email_address) .push_bind(false) - .push_bind(nanoid!(16)) + .push_bind(random_invite_code()) .push_bind(invite_count as i32); }, ); @@ -346,7 +345,7 @@ impl Db for PostgresDb { WHERE id = $2 AND invite_code IS NULL ", ) - .bind(nanoid!(16)) + .bind(random_invite_code()) .bind(id) .execute(&mut tx) .await?; @@ -451,15 +450,17 @@ impl Db for PostgresDb { let invitee_id = sqlx::query_scalar( " INSERT INTO users - (github_login, email_address, admin, inviter_id) + (github_login, email_address, admin, inviter_id, invite_code, invite_count) VALUES - ($1, $2, 'f', $3) + ($1, $2, 'f', $3, $4, $5) RETURNING id ", ) .bind(login) .bind(email_address) .bind(inviter_id) + .bind(random_invite_code()) + .bind(5) .fetch_one(&mut tx) .await .map(UserId)?; @@ -1458,6 +1459,10 @@ fn fuzzy_like_string(string: &str) -> String { result } +fn random_invite_code() -> String { + nanoid::nanoid!(16) +} + #[cfg(test)] pub mod tests { use super::*; @@ -2381,6 +2386,20 @@ pub mod tests { .unwrap_err(); let (_, invite_count) = db.get_invite_code_for_user(user1).await.unwrap().unwrap(); assert_eq!(invite_count, 1); + + // Ensure invited users get invite codes too. + assert_eq!( + db.get_invite_code_for_user(user2).await.unwrap().unwrap().1, + 5 + ); + assert_eq!( + db.get_invite_code_for_user(user3).await.unwrap().unwrap().1, + 5 + ); + assert_eq!( + db.get_invite_code_for_user(user4).await.unwrap().unwrap().1, + 5 + ); } pub struct TestDb { From aa09bc527f396fa98c673c2cd91ea6307fdb66e3 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Tue, 2 Aug 2022 13:57:55 +0200 Subject: [PATCH 40/42] Allow signing in again if authentication is pending or was unsuccessful The local server that we spin up to receive OAuth callbacks isn't called when an error occurs and it is non-trivial to do so with next-auth. Besides, there could be cases where the user explicitly closes the browser window before the callback can be invoked. With this commit, the user can sign in even while an authentication is still in progress. As opposed to waiting for at most 10 minutes before killing the local HTTP server if we haven't received the callback, we will repeatedly check for a response every second for 100 seconds. This gives us a chance to determine whether a new authentication has started in the meantime and, if so, abort the current authentication flow. --- crates/client/src/client.rs | 99 +++++++++++++++++-------------- crates/workspace/src/workspace.rs | 2 +- 2 files changed, 56 insertions(+), 45 deletions(-) diff --git a/crates/client/src/client.rs b/crates/client/src/client.rs index 0e9ec4076a..dae4ba1fec 100644 --- a/crates/client/src/client.rs +++ b/crates/client/src/client.rs @@ -569,14 +569,14 @@ impl Client { ) -> anyhow::Result<()> { let was_disconnected = match *self.status().borrow() { Status::SignedOut => true, - Status::ConnectionError | Status::ConnectionLost | Status::ReconnectionError { .. } => { - false + Status::ConnectionError + | Status::ConnectionLost + | Status::Authenticating { .. } + | Status::Reauthenticating { .. } + | Status::ReconnectionError { .. } => false, + Status::Connected { .. } | Status::Connecting { .. } | Status::Reconnecting { .. } => { + return Ok(()) } - Status::Connected { .. } - | Status::Connecting { .. } - | Status::Reconnecting { .. } - | Status::Authenticating - | Status::Reauthenticating => return Ok(()), Status::UpgradeRequired => return Err(EstablishConnectionError::UpgradeRequired)?, }; @@ -593,13 +593,22 @@ impl Client { read_from_keychain = credentials.is_some(); } if credentials.is_none() { - credentials = Some(match self.authenticate(&cx).await { - Ok(credentials) => credentials, - Err(err) => { - self.set_status(Status::ConnectionError, cx); - return Err(err); + let mut status_rx = self.status(); + let _ = status_rx.next().await; + futures::select_biased! { + authenticate = self.authenticate(&cx).fuse() => { + match authenticate { + Ok(creds) => credentials = Some(creds), + Err(err) => { + self.set_status(Status::ConnectionError, cx); + return Err(err); + } + } } - }); + _ = status_rx.next().fuse() => { + return Err(anyhow!("authentication canceled")); + } + } } let credentials = credentials.unwrap(); @@ -899,40 +908,42 @@ impl Client { // custom URL scheme instead of this local HTTP server. let (user_id, access_token) = executor .spawn(async move { - if let Some(req) = server.recv_timeout(Duration::from_secs(10 * 60))? { - let path = req.url(); - let mut user_id = None; - let mut access_token = None; - let url = Url::parse(&format!("http://example.com{}", path)) - .context("failed to parse login notification url")?; - for (key, value) in url.query_pairs() { - if key == "access_token" { - access_token = Some(value.to_string()); - } else if key == "user_id" { - user_id = Some(value.to_string()); + for _ in 0..100 { + if let Some(req) = server.recv_timeout(Duration::from_secs(1))? { + let path = req.url(); + let mut user_id = None; + let mut access_token = None; + let url = Url::parse(&format!("http://example.com{}", path)) + .context("failed to parse login notification url")?; + for (key, value) in url.query_pairs() { + if key == "access_token" { + access_token = Some(value.to_string()); + } else if key == "user_id" { + user_id = Some(value.to_string()); + } } - } - let post_auth_url = - format!("{}/native_app_signin_succeeded", *ZED_SERVER_URL); - req.respond( - tiny_http::Response::empty(302).with_header( - tiny_http::Header::from_bytes( - &b"Location"[..], - post_auth_url.as_bytes(), - ) - .unwrap(), - ), - ) - .context("failed to respond to login http request")?; - Ok(( - user_id.ok_or_else(|| anyhow!("missing user_id parameter"))?, - access_token - .ok_or_else(|| anyhow!("missing access_token parameter"))?, - )) - } else { - Err(anyhow!("didn't receive login redirect")) + let post_auth_url = + format!("{}/native_app_signin_succeeded", *ZED_SERVER_URL); + req.respond( + tiny_http::Response::empty(302).with_header( + tiny_http::Header::from_bytes( + &b"Location"[..], + post_auth_url.as_bytes(), + ) + .unwrap(), + ), + ) + .context("failed to respond to login http request")?; + return Ok(( + user_id.ok_or_else(|| anyhow!("missing user_id parameter"))?, + access_token + .ok_or_else(|| anyhow!("missing access_token parameter"))?, + )); + } } + + Err(anyhow!("didn't receive login redirect")) }) .await?; diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index ac3d1c3696..c060f57072 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -1811,7 +1811,7 @@ impl Workspace { match &*self.client.status().borrow() { client::Status::ConnectionError | client::Status::ConnectionLost - | client::Status::Reauthenticating + | client::Status::Reauthenticating { .. } | client::Status::Reconnecting { .. } | client::Status::ReconnectionError { .. } => Some( Container::new( From a1d0d2ccc4bccb2866cc6c7f8b83f8cf95ebc6d8 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Tue, 2 Aug 2022 14:28:47 +0200 Subject: [PATCH 41/42] Add tests for client authentication while a previous sign in is pending --- crates/client/src/client.rs | 46 ++++++++++++++++++++++++++++++++++++- 1 file changed, 45 insertions(+), 1 deletion(-) diff --git a/crates/client/src/client.rs b/crates/client/src/client.rs index dae4ba1fec..af084dc88d 100644 --- a/crates/client/src/client.rs +++ b/crates/client/src/client.rs @@ -1072,7 +1072,9 @@ pub fn decode_worktree_url(url: &str) -> Option<(u64, String)> { mod tests { use super::*; use crate::test::{FakeHttpClient, FakeServer}; - use gpui::TestAppContext; + use gpui::{executor::Deterministic, TestAppContext}; + use parking_lot::Mutex; + use std::future; #[gpui::test(iterations = 10)] async fn test_reconnection(cx: &mut TestAppContext) { @@ -1109,6 +1111,48 @@ mod tests { assert_eq!(server.auth_count(), 2); // Client re-authenticated due to an invalid token } + #[gpui::test(iterations = 10)] + async fn test_authenticating_more_than_once( + cx: &mut TestAppContext, + deterministic: Arc, + ) { + cx.foreground().forbid_parking(); + + let auth_count = Arc::new(Mutex::new(0)); + let dropped_auth_count = Arc::new(Mutex::new(0)); + let client = Client::new(FakeHttpClient::with_404_response()); + client.override_authenticate({ + let auth_count = auth_count.clone(); + let dropped_auth_count = dropped_auth_count.clone(); + move |cx| { + let auth_count = auth_count.clone(); + let dropped_auth_count = dropped_auth_count.clone(); + cx.foreground().spawn(async move { + *auth_count.lock() += 1; + let _drop = util::defer(move || *dropped_auth_count.lock() += 1); + future::pending::<()>().await; + unreachable!() + }) + } + }); + + let _authenticate = cx.spawn(|cx| { + let client = client.clone(); + async move { client.authenticate_and_connect(false, &cx).await } + }); + deterministic.run_until_parked(); + assert_eq!(*auth_count.lock(), 1); + assert_eq!(*dropped_auth_count.lock(), 0); + + let _authenticate = cx.spawn(|cx| { + let client = client.clone(); + async move { client.authenticate_and_connect(false, &cx).await } + }); + deterministic.run_until_parked(); + assert_eq!(*auth_count.lock(), 2); + assert_eq!(*dropped_auth_count.lock(), 1); + } + #[test] fn test_encode_and_decode_worktree_url() { let url = encode_worktree_url(5, "deadbeef"); From 25d47daad6c815e47a03ca604eb215706a211920 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Tue, 2 Aug 2022 14:40:46 +0200 Subject: [PATCH 42/42] Fix tests assuming a certain execution order --- crates/contacts_panel/src/contacts_panel.rs | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/crates/contacts_panel/src/contacts_panel.rs b/crates/contacts_panel/src/contacts_panel.rs index 532d545b91..aef9037879 100644 --- a/crates/contacts_panel/src/contacts_panel.rs +++ b/crates/contacts_panel/src/contacts_panel.rs @@ -1270,13 +1270,6 @@ mod tests { .detach(); }); - let request = server.receive::().await.unwrap(); - server - .respond( - request.receipt(), - proto::RegisterProjectResponse { project_id: 200 }, - ) - .await; let get_users_request = server.receive::().await.unwrap(); server .respond( @@ -1307,6 +1300,14 @@ mod tests { ) .await; + let request = server.receive::().await.unwrap(); + server + .respond( + request.receipt(), + proto::RegisterProjectResponse { project_id: 200 }, + ) + .await; + server.send(proto::UpdateContacts { incoming_requests: vec![proto::IncomingContactRequest { requester_id: 1,