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 792d970fcd26af1a19369bc381ea971f5aa287f6..0a32a8427bf489111065ca80b76c9086557e2ab8 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 540c5bfd6c80999104475e43b18f9d504eb6bc65..23dfa4b58ba20f3a7fd77c2defe9229ff0a56ff4 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 0000000000000000000000000000000000000000..d35af34ac9bc43fdd833217025622fea699d6037 --- /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 c86dce72262112c5f5b49301d2ed481c01036604..4937edfdee1537f0133a54587a0c29f6c5dd1f98 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 a73ac7f0cf4d6a256cf7bb0fcbdcfaf1f7cd5cea..55219b168782e02dcdb5ba2976852767ec1eaeb5 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 759eb901942ae91f9064d96c4f2eb5fd901c6d68..e116761eb9cc198c0a857bd0b62981c599207b04 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 1080daff1d221bb3de9cd3998a981121634bb4e4..e36bd2c75a6f066885ed340122ed505799fcf662 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 d92bd04251b34fae0a739afa56aad04101e3079e..f570fb2fb672234a80a17ca807080f48395444dc 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 a3dcd5fbce3942355da8cfacacb875b6bca0fd41..9471967eeecba37a5fa156321198ee4fda9a4aaa 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 f6abb22ddc4312f22f8d68013dab4082c7cea0c9..401ab33d7f79a37101e6da93e7baa966dd3ffc09 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 e116761eb9cc198c0a857bd0b62981c599207b04..29106b5889170a4a5b4631bbe9cde51b8a5bec71 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 10425f63a83068a7b48194f7ac5060564982f28d..8f2305eaff7ec17d78cc58852a4f299dc7720e76 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 0321e770cb795eb860a41bab9604ae90047d7c02..e8ff1932d4e2d9f9956a45e0072f08423b679c85 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 63cbe5099717078431edcfe6b5a434bb90fe636a..5d9a8ee5e374884b64ddd82c2f808adb6681e444 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 2e8a42a9eaf6258bc8e089bc3db8ac3527e4426b..65114ebd604f2b76796676b2eab0abb81e8b82f1 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 18ac436d2a212c1412839223c433165f70783c7c..53d3c2944e49c690b22eff59bc901133228d0e3f 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 d7245fc106a0edad8f756dea857a2773132f8f24..fc7cabc9c67fd35f340d744a9bd6f2fa3d6c1922 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 a313cf002e1987c7daa328fac63ffd42c1db38e6..07a2db5348d0157c15996bde26cde380b8d0db11 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 7851d9f2191d299e8d5100741d273d263ecf4dc3..a6a7079dd54c2f3e2ddc79ba95a97706300fed66 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 a38f55dcaf1873a30b9c6fbf577ec56921364a67..00dc3918fcb350e0459d20488fad7e9ccef3c90c 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 790a142162b2056d5f1b66f2b4d71f479bd1542d..b41e8fc810b7d927e3b298e3321028206253e887 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 3115e0e6690a3eaba2ce61f235182d031a6e9ce5..ef28fc81e54fcd28ae09ba6540c664680bbeb61e 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 84bcc4ebd8c8fce0d8699da3a8dbfbe343c5fa8e..ae4cf928993fdd79df3739aa4226fd5261975097 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 63ea16b1d814d6c8539c038ff2e353022e781049..0d8add4ed7c96ed30aae8d39eaf2e66e9a03019d 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 24a040abbb63391f7920bbc00a286a5b5b9dccb2..9d3f76a894b1d8461200252f609ae6c9427b77e6 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 0000000000000000000000000000000000000000..68bad3da268a98b3d1a44f52dd9687ea6865ef2b --- /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 0000000000000000000000000000000000000000..965470690e2db31d1dd6b4fdd10185d7825b2594 --- /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 0000000000000000000000000000000000000000..60972007b6c4c0a40ddc449d4c8f6a439a22e9e1 --- /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 c3859443b21c9d899bf1b7b64d97f19cee2c441b..aca476ef508173e60f84da60f1ba299f2bdb7009 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 e4255646bb849886532a895c527a1768e2982518..3e15dd7d1fd4504f4e87e3c8f14881c3ea4c6c72 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 0ebf3fe75eb9d2b06b16dc2a854bcc5a37b56776..7e959b59242742de30144d1eb4859b7fdfc5b43b 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 7513909ce12bf95c08c9fb0d79ef670f8a71a140..268f8bb498fb623b6554dc3db1d6a4aa89343f26 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 3a2aafbcd678ef67d21d127242d75652932dd847..270a33db70b2e2e412ef1351d16e2964f164e512 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 7349274681fc89d09715b98a86770284598932aa..64910c54e7c159420cce8e74f3d93bb2f7640781 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 37d0ee904c77fb16c6f497e24acc5014a3f85b62..adb60d2c2791699f32f0b1bc124a2f2092432d16 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 524012d31a7cc0d08edb666f4460f9755a6aa159..9b7f0011b2a9fb9eb1fedb36f45658d63bbf725b 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 8386cecbebad214f57f6315eef55cd57752cf8cb..bcf66dfdf71679c0c206e1af1322d2236381ce0a 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 629ba0c62069b2dc2aa912475cfcf3a5a5d6fd59..fb8b021d6b79289ba1ffa4f70eef41f6ebef8e8d 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 425bfa6c436466973354d069f079fc44d581d621..a264a3257306e656b373dad7acab1412ac023c2f 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 b45c30172e139a2804c51276e41eda665b029ee5..0ee5e24b248bda4178ae5e48142b93904311a3e4 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 00eb332440071284ca7f35000b6a245c940bb10e..958614203cfedde2296036d58c4c274aed3c07f5 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 927982deee1a099ec1f8666872863a49454db6f6..95da7ff4e1e433625938b152417ee0ddc550f330 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 4bbf9c03015947093c0261b4c60b31402edc54c6..7c884eec39e3b36cb1b8a5a719a3e5ecf6d22469 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 5eae1d55b7e1406d0956c67cf6b9dba9949faefc..13ed0b12e4a1f64b30d56f631d4bea0cbd8c7345 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 4f72d4b6b4e617c1619055dee7c246cc7f2e02f6..aa99277646653c899ee049547e5574b76b25b840 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 39058e51d42ef9707f890ece172cb6d98f5d8d32..9cf29f6e15f03b2d570303bd2d6f3b5696edb610 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 213eb8f13b0e9ff1793e5946fc3be0243e42232b..3fd6e13554dc9f93b3c76cbd0cf5ccced38df8ce 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 23dfa4b58ba20f3a7fd77c2defe9229ff0a56ff4..457f649a6b4ca64b8d033f1ea0689f97cf7ad989 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 c384748c3caeb227f3abe034b213de8ce07f3b12..b06da7fe33768600365ebf2d42cb642a2197e1d5 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 5499f1852cb4330467268dee6436b53589a90e9b..582f1c84968a5c1a25ddac5fd3c21ba907353c6d 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 a7c69d7660c695c5d2c439fdcd94c9a327b49d4f..0244641ec77ec62d21160ef719ac14b7d3b0c895 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 507eb74d8d39543f8aaa8859cf43c5eb9056f0e4..e3c1844974fa1bc56c7bb7c402250cf81fece7e7 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 84a72aabcc4b91977247b8d1cc299ab22338004c..514deb4c339efee074a01ee4ee425e9243a06087 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 7007a5da2d5d7d525ed879ea7ab9be1fdda0051a..26f88c6784111bcf1a0b2f33aa44a4d07baaf71a 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 d5d63fdadf6befdf6120d29ae6bc6dffd4235479..2a4c6d7f70736aa0aa39c3f1596d10beb25aef2b 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 8d1bb8078dd32b3bdfbfcf8b6b8d0d33bc82d463..7545ad762ef0f8bf9564864e0b8f08317ee53ba9 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 4937edfdee1537f0133a54587a0c29f6c5dd1f98..875b2bb2d58c966bb7b63d5d05ee6baadec59c99 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 d35af34ac9bc43fdd833217025622fea699d6037..66da26d7ed7c3134a5a9b9a089ff78136e9d52de 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 8e9e976efd7edc431ea897a574c79885f2bf5f92..ed860b9ecf9bcf5a8de2c82efe0080c6945caa2d 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 0fe39b4096d61ae0cc42b868c18a1a81616800f5..d56305cee985188a51f66498d471252190c8512d 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 4e5829559eb78abca44a5ed6a4bf41d1ecc17e4b..1dc4b0ddbf8d8b41204d43c02dad48493c5a55d4 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 6c34d6be70ebd3193469814e628f6c136335a9d8..5c05e8eaba1b72f29d76bba9863cb90dfe01badc 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 ec5280befc7a767777010da8937397b0cff5eb51..9ff05073824a8cf35d61178256937c7c2c66847d 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 1dc4b0ddbf8d8b41204d43c02dad48493c5a55d4..27339d2d00b414dff65a3fc5595d64cbac336880 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 bff9438124313f53d961c4e1bf0cee5f3c9cee35..e2685ba36e7fc4a25fbd76a4a4e4e56611296e80 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 ac3759c257b21f7deafcd8ef011805208fc9699a..3f1f0f1b8ce92741d99a312835fada5bfda17f09 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 e2685ba36e7fc4a25fbd76a4a4e4e56611296e80..c349ed50f27951b217d871173afa95b551f50bb2 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 | 176 ++++++++++++++++++---------------- crates/language/src/tests.rs | 47 +++++++++ 2 files changed, 139 insertions(+), 84 deletions(-) diff --git a/crates/language/src/buffer.rs b/crates/language/src/buffer.rs index c349ed50f27951b217d871173afa95b551f50bb2..ef9b2663152e1f4690d1ac33875cf7b0e4bb2038 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); - - 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; - } + .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 first_line_is_new = false; + + // 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; + } - // 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 3f1f0f1b8ce92741d99a312835fada5bfda17f09..3f721256cf94a5268b5a2c2f6df45f125c183f79 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 5dc10c7b57185bd9dc070054c1732ffd18319102..88957575398b8affa6df2d1322df9c462773c6d9 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 2da8c263426c45a440cd9ec9aed3ccd44ace5cb9..5262daab5fde4f3bb705780c801ea7a8d07ef94e 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 de175812ba31485c24ebe359e12f30a993951c4e..86af181765e6a9f2dccf6a351a1bc350bfc26c90 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 | 18 ++++++++++++------ crates/gpui/src/platform/mac/window.rs | 22 ++++++++++++++-------- crates/terminal/src/mappings/keys.rs | 1 + 4 files changed, 34 insertions(+), 14 deletions(-) diff --git a/crates/gpui/src/keymap.rs b/crates/gpui/src/keymap.rs index 22cb3ba5dea4d357405f2e0d99b1e2efe6242d6a..d32344b0dbc8c2096d4527616316eef9a92e73bc 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 e0e178aa8c0833471d5cfb7be37a2a516138bfa6..5aedc63b2e03caecf92d03dfc912263f4b6ca9ca 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 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 mut chars_ignoring_modifiers = - CStr::from_ptr(native_event.charactersIgnoringModifiers().UTF8String() as *mut c_char) - .to_str() - .unwrap(); + 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 86af181765e6a9f2dccf6a351a1bc350bfc26c90..05cc542b98d6fa8f66fe7fb00b3a1acc66c05508 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 f88bfa927ac7f650251bd04c5762a71365562626..3ed46de459f2ed23ddc0cc5c8d9fa1a05fa105cf 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 65909702ba5f9d37cf68988411e646fa0c018c3b..9ece9605b779df7a1131c470d7cfec4064cd4023 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 55445c930e960c98f1cec0aef56900e87900d979..03f213b5845961d6db47a351b99df0ab83053111 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 c5c823c79e349effb0a641d485cd2c6d7b8cab89..ca2c27cf7028cff6a8f1fe44f818adc4a985133c 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 fedb999cad2718b7877ff72af40c041a7e30420d..02fcdabd0994a5acb147a42732e6cb28adf09a63 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 ed860b9ecf9bcf5a8de2c82efe0080c6945caa2d..e4d454c0de5905607a006033a7bc9ddf6f8947aa 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 d56305cee985188a51f66498d471252190c8512d..be20be2fd7fb0e9df29956cf6344b10c7325d1be 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 187b0182493295c84309a379e5e206d4f970cadd..7693b39d81d50338c59329d7b2880e8f768c3914 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 27339d2d00b414dff65a3fc5595d64cbac336880..028a8f23b03fb0c50ad2851a7bc14067f6396a71 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 867913fc7ba914ed18461cc212b4fed91e72d44c..709dc31f64a3a498ecfc8280d852826b85010504 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 337e3afc2dc03d04d4753377c31f584067fc7871..2ca1403b6c868c8683f16c13f1e0970467525b4c 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 e8ff1932d4e2d9f9956a45e0072f08423b679c85..ac3d1c36960d8914d40506a2e30cf7bdbec8f302 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 709dc31f64a3a498ecfc8280d852826b85010504..f7ccefec2abd79298968a056272816c1771d63ea 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 ef9b2663152e1f4690d1ac33875cf7b0e4bb2038..8eef0a527424d7a8638a80a79cabbfca6d85cdd6 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 ed860b9ecf9bcf5a8de2c82efe0080c6945caa2d..6a6ba400c7b2bb23a9f25f4239e1e3bb2c649d30 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 00ef7b11a0943d30deefc0c9a21f2e666da25c45..fa48cabf05cd379219e81199070577245796ef00 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 c70ad6b7311576cacf7c509f8dc6d10f15aa2f49..0dc8165bfbb91bb0ed53593b5d089d46a4a3b974 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 8eef0a527424d7a8638a80a79cabbfca6d85cdd6..4b0d2566c365320f90842c5677921176ae4112fd 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 3f721256cf94a5268b5a2c2f6df45f125c183f79..0a6fb2ce025893b7ed0a84be8e5a280f771f726b 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 54554beaf6c52a47d98b9e16aa8be3dac766a5b8..71fde5bd5e2cfbc97da21f2e310df7d26e9ceb52 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 ca0b24bda78f024701ecdec2582cb0225a9dc9da..644d9ece40b6835c78fa40c19d7bd88cc4171182 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 18d49f78d478840876caadec32a53e5e9b5ee544..856986b1ebb924772af2142db2e9578a3b91b843 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 3730b5d21afc2a37e22843320b2b4deaf41cbef3..02ad100df81e8ba13e5d52ad805017cab6a498b3 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 9471967eeecba37a5fa156321198ee4fda9a4aaa..1f3ccef0be0d6ecd3946e2971850d3be0b7b3412 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 6f50da6a7d28a51b67c0711b6dfe35ea1ab0adf7..9b12df60d9d1d3c6a67c6f302ab97847ed6c5ae6 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 ed0df25d69b20d13e519a1baef6568269660a6dc..52379011709c2b3fc81183065fcbf22e643e5114 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 95c3abb25238adbf76dccf496ad2e719f815b881..a6e5536d15ac6b1ddc4dac29029f11d402351a7c 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 f570fb2fb672234a80a17ca807080f48395444dc..4543aa3b27b28874e1f45d5d5fc942f2fefa2b05 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 fa48cabf05cd379219e81199070577245796ef00..a12285dbbe4a96693ab05e1f0d21abe83b132192 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 0dc8165bfbb91bb0ed53593b5d089d46a4a3b974..6e9f368e77be909c1a8fb2d149be679b3aa0b66b 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 4b0d2566c365320f90842c5677921176ae4112fd..8f0f7d40ec269edbebd7e85abf88e478515550b9 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 0a6fb2ce025893b7ed0a84be8e5a280f771f726b..acfaf5980d9df806cdcf3830364539383296275c 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 29106b5889170a4a5b4631bbe9cde51b8a5bec71..898dbb5a2f6c3a23457b240b16618d9552f1a7f5 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 e36bd2c75a6f066885ed340122ed505799fcf662..4c5e9ef8e1b95994dbe9550b745ef6fb091d79b4 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 529da6f7b6ea3b70d5e6e85593c0a721a44a202d..52631e71b4b71827e6ad03ede931034e1f021b68 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 65909702ba5f9d37cf68988411e646fa0c018c3b..8ec0fea9a9daadf0b7b6d681a3036b5dc9c0157f 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 fedb999cad2718b7877ff72af40c041a7e30420d..a81ac1c455f7d9678035e8eed6ab780388a0ff02 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 71fde5bd5e2cfbc97da21f2e310df7d26e9ceb52..074f51bc8fa9b084963a716b327abd60a318101a 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 644d9ece40b6835c78fa40c19d7bd88cc4171182..f76b4e0fbaad03a97cd0bce946a9d77ae4260aaf 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 856986b1ebb924772af2142db2e9578a3b91b843..8e804f0c7f89cff2856bbafd435dcea8185668e9 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 8f0f7d40ec269edbebd7e85abf88e478515550b9..ddacefec06e4d6f83f1e2d77dc5159f47c12da82 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 acfaf5980d9df806cdcf3830364539383296275c..0657e256042fb4ebd304512fba380e86e5410b7d 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), 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, 0)..Point::new(2, 0), pasted_text.clone())], + [(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 4543aa3b27b28874e1f45d5d5fc942f2fefa2b05..1f65451a834e67ba6a4ff9fbd56650d06fc6257b 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 a12285dbbe4a96693ab05e1f0d21abe83b132192..be9972c3d5d26e7430bfef3832e351c26de2c035 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 ddacefec06e4d6f83f1e2d77dc5159f47c12da82..b2d45717f093a5d13d1b792f82bde517e7b55bd3 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 0657e256042fb4ebd304512fba380e86e5410b7d..bee320a932ebf1fd36c44c480431dc4c8e6ee80c 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 cb6a736c6344d0c91cfdb7b5b22458ac0e9fed2e..75bb7445ffb42e8c62ff0b15a2e517eb6b0c5ea3 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 1f65451a834e67ba6a4ff9fbd56650d06fc6257b..819aa5ccd5ebdf139117183a1d9568e47b6d0a2a 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 be9972c3d5d26e7430bfef3832e351c26de2c035..1fc7cf0560c262106aad9f63a6504d4ea57f40b0 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 b2d45717f093a5d13d1b792f82bde517e7b55bd3..e6b0d48820b0db06de9cf96c4c887dbaff561607 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 bee320a932ebf1fd36c44c480431dc4c8e6ee80c..937ff069305cabe280d4d5de5949ea5423181054 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 13055a9df7dad1fe8094d8198a410d9abea857bc..39663e0db42ebf28974dc075a6f3f3cea06c2059 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 75bb7445ffb42e8c62ff0b15a2e517eb6b0c5ea3..6f682f61462ab731f79d961d3ad031df8c20c202 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 d9772c031b7af99a4b44e33090e2efe01e89bb6d..76fea2e2051ae80899ae9d66d2f787dc8f9ecd5f 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 074f51bc8fa9b084963a716b327abd60a318101a..a4db4b9a755d030de8d98039beea31fa5312e109 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 f76b4e0fbaad03a97cd0bce946a9d77ae4260aaf..801c7c96f9539af14d8013f9292291e6ee42b54f 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 8e804f0c7f89cff2856bbafd435dcea8185668e9..adbe4312796ed5407942a5ca084eabd17a3c4deb 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 bc125e4303a6e394c4538633daf4878b805fa52f..0b630bf256147270577703d4d0df3b993d90b234 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 2ca1403b6c868c8683f16c13f1e0970467525b4c..d800901b4982fd861163a1b277c80e0075b748da 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 6ed9b40a18ae07dddf76d216fac1de6958ea5cc1..fb8496293e3131cd76f0988a2dd02eb5c3be76d3 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 d800901b4982fd861163a1b277c80e0075b748da..7d24eaa88f3bd9e81dad46f58af0bbb2a1f24afc 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 0000000000000000000000000000000000000000..763c4d10706fa88c823480f20a1e4889aa5f3225 --- /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 fb8496293e3131cd76f0988a2dd02eb5c3be76d3..20ecf68e8d421370a6483162c93d27e92703056d 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 7d24eaa88f3bd9e81dad46f58af0bbb2a1f24afc..ed718272d8fb50be0d4e96da9434ed9e4c854836 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 763c4d10706fa88c823480f20a1e4889aa5f3225..6643cc0fe6d40c7d91038e0634eab8b8f27de0cf 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 20ecf68e8d421370a6483162c93d27e92703056d..2f683d8e8e4093a728117d6a8f637f317957e33c 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 | 171 +++++++++-------- crates/rpc/src/proto.rs | 4 + crates/rpc/src/rpc.rs | 2 +- crates/zed/src/menus.rs | 4 + 10 files changed, 385 insertions(+), 83 deletions(-) diff --git a/assets/keymaps/default.json b/assets/keymaps/default.json index 72252edd71ebfb49a534caa85d289e6ddd1b3d8f..28eec35defc36c52970be443eba0bb0ac16c328e 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 819aa5ccd5ebdf139117183a1d9568e47b6d0a2a..558a6bfd9852490cd79946ba49375bc88db7d953 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 ce2faf8fa63c40c445086048408f3b5be47b8895..513a9ed99ce06e0ac25d2f70d5f4d829155f1426 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 8c689808aefac3f4c98e1a8826d1d9e957eedc57..5c8916e34eede7de7f6ddd109ba38841980baa14 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 0c30ee29248cb19074c272fc2c92b3225df79111..6ec52451a1d4dd9f68ae822a516751af93ccdf34 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 898dbb5a2f6c3a23457b240b16618d9552f1a7f5..8adc10ba5564ac57d29cb5107b9fd73d3817d53d 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 35f3049edbad4f694cee2fe70811b53d18e75adb..f52815a8be60d306e5d328f4910cae30828c29a8 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; - - UpdateProject update_project = 30; - RegisterProjectActivity register_project_activity = 31; - UpdateWorktree update_worktree = 32; - UpdateWorktreeExtensions update_worktree_extensions = 33; - - 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; - - UpdateDiagnosticSummary update_diagnostic_summary = 39; - StartLanguageServer start_language_server = 40; - UpdateLanguageServer update_language_server = 41; - - 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; - - 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; - - UpdateContacts update_contacts = 80; - UpdateInviteInfo update_invite_info = 81; - ShowContacts show_contacts = 82; - - 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; - - Follow follow = 89; - FollowResponse follow_response = 90; - UpdateFollowers update_followers = 91; - Unfollow unfollow = 92; + 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 = 32; + RegisterProjectActivity register_project_activity = 33; + UpdateWorktree update_worktree = 34; + UpdateWorktreeExtensions update_worktree_extensions = 35; + + 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 = 41; + StartLanguageServer start_language_server = 42; + UpdateLanguageServer update_language_server = 43; + + 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 = 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 = 82; + UpdateInviteInfo update_invite_info = 83; + ShowContacts show_contacts = 84; + + 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 = 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 e3844a869276a2fb3ed1a6737c775b909d459c29..8cd5ca36fbf523e8a25cf6ee125ba022957273ac 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 015ac10707c7cd7960a30f3c13cf350f433e0ce6..c4017015f9067280d76de94f6bcc242cdf6a1cca 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 e8297a172708ab6f3fee75282c49d4fd0bc9d038..73817ca2e39a8aa6d758bbbd1156850d27efcfdc 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 b8bee49d8d8feb79120bfa461b2bf7727f8a0b11..fa4c12667142c6ee86fbff1dd80a87660862c8a0 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 f0dc359b4b58843b51882279beb093ed074537cb..3f8b34b6e35185e6a78bbec7a6ae429259ca3d63 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 e833bd8eb5f78ec639222bde1b9b6338e4e2d15a..6ac75926be426e2a7512d35ea9c06cd5c7f64047 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 | 288 +++++++++++++++------ 1 file changed, 210 insertions(+), 78 deletions(-) diff --git a/crates/editor/src/link_go_to_definition.rs b/crates/editor/src/link_go_to_definition.rs index 3f8b34b6e35185e6a78bbec7a6ae429259ca3d63..b57179c07dca5cf3362b44353fd6d9997014ddc6 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();"}); - - 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, - }, - ]))) - }); + 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, + }, + ]))) + }); 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();"}); - - 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, - }, - ]))) - }); + 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, + }, + ]))) + }); 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 | 532 ++++++++++++------------------ 1 file changed, 204 insertions(+), 328 deletions(-) diff --git a/crates/project/src/lsp_command.rs b/crates/project/src/lsp_command.rs index 6ec52451a1d4dd9f68ae822a516751af93ccdf34..4d679ec9cb1ac96af7d90e083a3ebb01d01d501a 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,66 +488,203 @@ 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, - }; + location_links_from_proto(message.links, project, cx).await + } - 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, - }; + fn buffer_id_from_proto(message: &proto::GetTypeDefinition) -> u64 { + message.buffer_id + } +} - links.push(LocationLink { origin, target }) +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, + ) + })); } - Ok(links) } - fn buffer_id_from_proto(message: &proto::GetTypeDefinition) -> u64 { - message.buffer_id + 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)] @@ -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 f45a54472800ddb690b4e27a72de0fc71ad3a1d0..1c1893dc796c65eaedae1e792809a01db0daf427 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 0e9ec4076ad43754a53e11f833935c9b807f025c..dae4ba1fec42b19e8aef1eabeea01f0563eb1f4d 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 ac3d1c36960d8914d40506a2e30cf7bdbec8f302..c060f57072692767305e12e55fed7c2c80ce7481 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 dae4ba1fec42b19e8aef1eabeea01f0563eb1f4d..af084dc88d45eff6acbcf57c3de73499e34fbee6 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 532d545b9121ddcbdd809b23567fb768fb265cfa..aef90378791a9f49f6e08dddd647324ae0ee361c 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,