diff --git a/Cargo.lock b/Cargo.lock index 605ddf6c2605ba14bb65dac5d4ae1fa9b548b99d..ec9c58b1a9b50e5c95b8c43dcae99bd7a25b8dca 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1638,6 +1638,7 @@ dependencies = [ "futures", "fuzzy", "gpui", + "indoc", "itertools", "language", "lazy_static", diff --git a/crates/editor/Cargo.toml b/crates/editor/Cargo.toml index 4664d9c52e674fa0786bdd1de501d20cecf99ee9..076fecbfcbb58385e859b97b793b3bb0a41e8302 100644 --- a/crates/editor/Cargo.toml +++ b/crates/editor/Cargo.toml @@ -37,6 +37,7 @@ workspace = { path = "../workspace" } aho-corasick = "0.7" anyhow = "1.0" futures = "0.3" +indoc = "1.0.4" itertools = "0.10" lazy_static = "1.4" log = "0.4" diff --git a/crates/editor/src/display_map.rs b/crates/editor/src/display_map.rs index 6bf495c91facd29c6ba1c25e2d5dfda8dd5a2bef..d4e8dc6cf70d9ae7a0462bd55ced5a77f18f795f 100644 --- a/crates/editor/src/display_map.rs +++ b/crates/editor/src/display_map.rs @@ -204,7 +204,12 @@ impl DisplayMap { } fn tab_size(buffer: &ModelHandle, cx: &mut ModelContext) -> u32 { - let language_name = buffer.read(cx).language(cx).map(|language| language.name()); + let language_name = buffer + .read(cx) + .as_singleton() + .and_then(|buffer| buffer.read(cx).language()) + .map(|language| language.name()); + cx.global::().tab_size(language_name.as_deref()) } diff --git a/crates/editor/src/display_map/block_map.rs b/crates/editor/src/display_map/block_map.rs index d749b607eb5730fbb17b2def2696aaac6b6817f8..b7f1836cdc2cb7b6808c6eb969f63a624f6f5921 100644 --- a/crates/editor/src/display_map/block_map.rs +++ b/crates/editor/src/display_map/block_map.rs @@ -969,6 +969,7 @@ mod tests { use crate::multi_buffer::MultiBuffer; use gpui::{elements::Empty, Element}; use rand::prelude::*; + use settings::Settings; use std::env; use text::RandomCharIter; @@ -988,6 +989,8 @@ mod tests { #[gpui::test] fn test_basic_blocks(cx: &mut gpui::MutableAppContext) { + cx.set_global(Settings::test(cx)); + let family_id = cx.font_cache().load_family(&["Helvetica"]).unwrap(); let font_id = cx .font_cache() @@ -1167,6 +1170,8 @@ mod tests { #[gpui::test] fn test_blocks_on_wrapped_lines(cx: &mut gpui::MutableAppContext) { + cx.set_global(Settings::test(cx)); + let family_id = cx.font_cache().load_family(&["Helvetica"]).unwrap(); let font_id = cx .font_cache() @@ -1209,6 +1214,8 @@ mod tests { #[gpui::test(iterations = 100)] fn test_random_blocks(cx: &mut gpui::MutableAppContext, mut rng: StdRng) { + cx.set_global(Settings::test(cx)); + let operations = env::var("OPERATIONS") .map(|i| i.parse().expect("invalid `OPERATIONS` variable")) .unwrap_or(10); diff --git a/crates/editor/src/display_map/fold_map.rs b/crates/editor/src/display_map/fold_map.rs index 2c09244a7df38492b10ad4837f5443e3e8f7c457..3c020dceb78e0bdfb3600aaee63720c89976ef8e 100644 --- a/crates/editor/src/display_map/fold_map.rs +++ b/crates/editor/src/display_map/fold_map.rs @@ -1210,6 +1210,7 @@ mod tests { use super::*; use crate::{MultiBuffer, ToPoint}; use rand::prelude::*; + use settings::Settings; use std::{cmp::Reverse, env, mem, sync::Arc}; use sum_tree::TreeMap; use text::RandomCharIter; @@ -1218,6 +1219,7 @@ mod tests { #[gpui::test] fn test_basic_folds(cx: &mut gpui::MutableAppContext) { + cx.set_global(Settings::test(cx)); let buffer = MultiBuffer::build_simple(&sample_text(5, 6, 'a'), cx); let subscription = buffer.update(cx, |buffer, _| buffer.subscribe()); let buffer_snapshot = buffer.read(cx).snapshot(cx); @@ -1291,6 +1293,7 @@ mod tests { #[gpui::test] fn test_adjacent_folds(cx: &mut gpui::MutableAppContext) { + cx.set_global(Settings::test(cx)); let buffer = MultiBuffer::build_simple("abcdefghijkl", cx); let subscription = buffer.update(cx, |buffer, _| buffer.subscribe()); let buffer_snapshot = buffer.read(cx).snapshot(cx); @@ -1354,6 +1357,7 @@ mod tests { #[gpui::test] fn test_merging_folds_via_edit(cx: &mut gpui::MutableAppContext) { + cx.set_global(Settings::test(cx)); let buffer = MultiBuffer::build_simple(&sample_text(5, 6, 'a'), cx); let subscription = buffer.update(cx, |buffer, _| buffer.subscribe()); let buffer_snapshot = buffer.read(cx).snapshot(cx); @@ -1404,6 +1408,7 @@ mod tests { #[gpui::test(iterations = 100)] fn test_random_folds(cx: &mut gpui::MutableAppContext, mut rng: StdRng) { + cx.set_global(Settings::test(cx)); let operations = env::var("OPERATIONS") .map(|i| i.parse().expect("invalid `OPERATIONS` variable")) .unwrap_or(10); diff --git a/crates/editor/src/display_map/wrap_map.rs b/crates/editor/src/display_map/wrap_map.rs index 790cc8017b064bce5403a3900249e69c5bf8be23..00e9b63b1ad66625942c62daf21c4b04c372bb9f 100644 --- a/crates/editor/src/display_map/wrap_map.rs +++ b/crates/editor/src/display_map/wrap_map.rs @@ -1014,12 +1014,14 @@ mod tests { use gpui::test::observe; use language::RandomCharIter; use rand::prelude::*; + use settings::Settings; use smol::stream::StreamExt; use std::{cmp, env}; use text::Rope; #[gpui::test(iterations = 100)] async fn test_random_wraps(cx: &mut gpui::TestAppContext, mut rng: StdRng) { + cx.update(|cx| cx.set_global(Settings::test(cx))); cx.foreground().set_block_on_ticks(0..=50); cx.foreground().forbid_parking(); let operations = env::var("OPERATIONS") diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index d52e456b959bfccae5eb6dd4f7e9dd15a1689681..7ceb3465f816e9356c2f70352c8938c71676b787 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -64,7 +64,6 @@ const CURSOR_BLINK_INTERVAL: Duration = Duration::from_millis(500); const MAX_LINE_LEN: usize = 1024; const MIN_NAVIGATION_HISTORY_ROW_DELTA: i64 = 10; const MAX_SELECTION_HISTORY_LEN: usize = 1024; -const INDENT_SIZE: u32 = 4; action!(Cancel); action!(Backspace); @@ -1131,8 +1130,12 @@ impl Editor { } } - pub fn language<'a>(&self, cx: &'a AppContext) -> Option<&'a Arc> { - self.buffer.read(cx).language(cx) + pub fn language_at<'a, T: ToOffset>( + &self, + point: T, + cx: &'a AppContext, + ) -> Option<&'a Arc> { + self.buffer.read(cx).language_at(point, cx) } fn style(&self, cx: &AppContext) -> EditorStyle { @@ -2946,8 +2949,9 @@ impl Editor { .buffer_line_for_row(old_head.row) { let indent_column = buffer.indent_column_for_line(line_buffer_range.start.row); + let language_name = buffer.language().map(|language| language.name()); + let indent = cx.global::().tab_size(language_name.as_deref()); if old_head.column <= indent_column && old_head.column > 0 { - let indent = INDENT_SIZE; new_head = cmp::min( new_head, Point::new(old_head.row, ((old_head.column - 1) / indent) * indent), @@ -2992,12 +2996,15 @@ impl Editor { return; } - let tab_size = cx.global::().tab_size; let mut selections = self.local_selections::(cx); if selections.iter().all(|s| s.is_empty()) { self.transact(cx, |this, cx| { this.buffer.update(cx, |buffer, cx| { for selection in &mut selections { + let language_name = + buffer.language_at(selection.start, cx).map(|l| l.name()); + let tab_size = + cx.global::().tab_size(language_name.as_deref()); let char_column = buffer .read(cx) .text_for_range( @@ -3026,12 +3033,14 @@ impl Editor { } pub fn indent(&mut self, _: &Indent, cx: &mut ViewContext) { - let tab_size = cx.global::().tab_size; let mut selections = self.local_selections::(cx); self.transact(cx, |this, cx| { let mut last_indent = None; this.buffer.update(cx, |buffer, cx| { + let snapshot = buffer.snapshot(cx); for selection in &mut selections { + let language_name = buffer.language_at(selection.start, cx).map(|l| l.name()); + let tab_size = cx.global::().tab_size(language_name.as_deref()); let mut start_row = selection.start.row; let mut end_row = selection.end.row + 1; @@ -3055,7 +3064,7 @@ impl Editor { } for row in start_row..end_row { - let indent_column = buffer.read(cx).indent_column_for_line(row); + let indent_column = snapshot.indent_column_for_line(row); let columns_to_next_tab_stop = tab_size - (indent_column % tab_size); let row_start = Point::new(row, 0); buffer.edit( @@ -3082,14 +3091,16 @@ impl Editor { } pub fn outdent(&mut self, _: &Outdent, cx: &mut ViewContext) { - let tab_size = cx.global::().tab_size; let selections = self.local_selections::(cx); let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); let mut deletion_ranges = Vec::new(); let mut last_outdent = None; { - let buffer = self.buffer.read(cx).read(cx); + let buffer = self.buffer.read(cx); + let snapshot = buffer.snapshot(cx); for selection in &selections { + let language_name = buffer.language_at(selection.start, cx).map(|l| l.name()); + let tab_size = cx.global::().tab_size(language_name.as_deref()); let mut rows = selection.spanned_rows(false, &display_map); // Avoid re-outdenting a row that has already been outdented by a @@ -3101,7 +3112,7 @@ impl Editor { } for row in rows { - let column = buffer.indent_column_for_line(row); + let column = snapshot.indent_column_for_line(row); if column > 0 { let mut deletion_len = column % tab_size; if deletion_len == 0 { @@ -4245,24 +4256,26 @@ impl Editor { } pub fn toggle_comments(&mut self, _: &ToggleComments, cx: &mut ViewContext) { - // Get the line comment prefix. Split its trailing whitespace into a separate string, - // as that portion won't be used for detecting if a line is a comment. - let full_comment_prefix = - if let Some(prefix) = self.language(cx).and_then(|l| l.line_comment_prefix()) { - prefix.to_string() - } else { - return; - }; - let comment_prefix = full_comment_prefix.trim_end_matches(' '); - let comment_prefix_whitespace = &full_comment_prefix[comment_prefix.len()..]; - self.transact(cx, |this, cx| { let mut selections = this.local_selections::(cx); let mut all_selection_lines_are_comments = true; let mut edit_ranges = Vec::new(); let mut last_toggled_row = None; this.buffer.update(cx, |buffer, cx| { + // TODO: Handle selections that cross excerpts for selection in &mut selections { + // Get the line comment prefix. Split its trailing whitespace into a separate string, + // as that portion won't be used for detecting if a line is a comment. + let full_comment_prefix = if let Some(prefix) = buffer + .language_at(selection.start, cx) + .and_then(|l| l.line_comment_prefix()) + { + prefix.to_string() + } else { + return; + }; + let comment_prefix = full_comment_prefix.trim_end_matches(' '); + let comment_prefix_whitespace = &full_comment_prefix[comment_prefix.len()..]; edit_ranges.clear(); let snapshot = buffer.snapshot(cx); @@ -5670,16 +5683,22 @@ impl Editor { } pub fn soft_wrap_mode(&self, cx: &AppContext) -> SoftWrap { - let language = self.language(cx).map(|language| language.name()); + let language_name = self + .buffer + .read(cx) + .as_singleton() + .and_then(|singleton_buffer| singleton_buffer.read(cx).language()) + .map(|l| l.name()); + let settings = cx.global::(); let mode = self .soft_wrap_mode_override - .unwrap_or_else(|| settings.soft_wrap(language.as_deref())); + .unwrap_or_else(|| settings.soft_wrap(language_name.as_deref())); match mode { settings::SoftWrap::None => SoftWrap::None, settings::SoftWrap::EditorWidth => SoftWrap::EditorWidth, settings::SoftWrap::PreferredLineLength => { - SoftWrap::Column(settings.preferred_line_length(language.as_deref())) + SoftWrap::Column(settings.preferred_line_length(language_name.as_deref())) } } } @@ -6463,14 +6482,18 @@ pub fn styled_runs_for_code_label<'a>( #[cfg(test)] mod tests { + use crate::test::{assert_text_with_selections, select_ranges}; + use super::*; use gpui::{ geometry::rect::RectF, platform::{WindowBounds, WindowOptions}, }; + use indoc::indoc; use language::{FakeLspAdapter, LanguageConfig}; use lsp::FakeLanguageServer; use project::FakeFs; + use settings::LanguageOverride; use smol::stream::StreamExt; use std::{cell::RefCell, rc::Rc, time::Instant}; use text::Point; @@ -6480,7 +6503,7 @@ mod tests { #[gpui::test] fn test_edit_events(cx: &mut MutableAppContext) { - populate_settings(cx); + cx.set_global(Settings::test(cx)); let buffer = cx.add_model(|cx| language::Buffer::new(0, "123456", cx)); let events = Rc::new(RefCell::new(Vec::new())); @@ -6588,7 +6611,7 @@ mod tests { #[gpui::test] fn test_undo_redo_with_selection_restoration(cx: &mut MutableAppContext) { - populate_settings(cx); + cx.set_global(Settings::test(cx)); let mut now = Instant::now(); let buffer = cx.add_model(|cx| language::Buffer::new(0, "123456", cx)); let group_interval = buffer.read(cx).transaction_group_interval(); @@ -6657,7 +6680,7 @@ mod tests { #[gpui::test] fn test_selection_with_mouse(cx: &mut gpui::MutableAppContext) { - populate_settings(cx); + cx.set_global(Settings::test(cx)); let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\nddddddd\n", cx); let (_, editor) = cx.add_window(Default::default(), |cx| build_editor(buffer, cx)); @@ -6722,7 +6745,7 @@ mod tests { #[gpui::test] fn test_canceling_pending_selection(cx: &mut gpui::MutableAppContext) { - populate_settings(cx); + cx.set_global(Settings::test(cx)); let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\ndddddd\n", cx); let (_, view) = cx.add_window(Default::default(), |cx| build_editor(buffer, cx)); @@ -6754,7 +6777,7 @@ mod tests { #[gpui::test] fn test_navigation_history(cx: &mut gpui::MutableAppContext) { - populate_settings(cx); + cx.set_global(Settings::test(cx)); use workspace::Item; let nav_history = Rc::new(RefCell::new(workspace::NavHistory::default())); let buffer = MultiBuffer::build_simple(&sample_text(30, 5, 'a'), cx); @@ -6814,7 +6837,7 @@ mod tests { #[gpui::test] fn test_cancel(cx: &mut gpui::MutableAppContext) { - populate_settings(cx); + cx.set_global(Settings::test(cx)); let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\ndddddd\n", cx); let (_, view) = cx.add_window(Default::default(), |cx| build_editor(buffer, cx)); @@ -6854,7 +6877,7 @@ mod tests { #[gpui::test] fn test_fold(cx: &mut gpui::MutableAppContext) { - populate_settings(cx); + cx.set_global(Settings::test(cx)); let buffer = MultiBuffer::build_simple( &" impl Foo { @@ -6939,7 +6962,7 @@ mod tests { #[gpui::test] fn test_move_cursor(cx: &mut gpui::MutableAppContext) { - populate_settings(cx); + cx.set_global(Settings::test(cx)); let buffer = MultiBuffer::build_simple(&sample_text(6, 6, 'a'), cx); let (_, view) = cx.add_window(Default::default(), |cx| build_editor(buffer.clone(), cx)); @@ -7013,7 +7036,7 @@ mod tests { #[gpui::test] fn test_move_cursor_multibyte(cx: &mut gpui::MutableAppContext) { - populate_settings(cx); + cx.set_global(Settings::test(cx)); let buffer = MultiBuffer::build_simple("ⓐⓑⓒⓓⓔ\nabcde\nαβγδε\n", cx); let (_, view) = cx.add_window(Default::default(), |cx| build_editor(buffer.clone(), cx)); @@ -7114,7 +7137,7 @@ mod tests { #[gpui::test] fn test_move_cursor_different_line_lengths(cx: &mut gpui::MutableAppContext) { - populate_settings(cx); + cx.set_global(Settings::test(cx)); let buffer = MultiBuffer::build_simple("ⓐⓑⓒⓓⓔ\nabcd\nαβγ\nabcd\nⓐⓑⓒⓓⓔ\n", cx); let (_, view) = cx.add_window(Default::default(), |cx| build_editor(buffer.clone(), cx)); view.update(cx, |view, cx| { @@ -7159,7 +7182,7 @@ mod tests { #[gpui::test] fn test_beginning_end_of_line(cx: &mut gpui::MutableAppContext) { - populate_settings(cx); + cx.set_global(Settings::test(cx)); let buffer = MultiBuffer::build_simple("abc\n def", cx); let (_, view) = cx.add_window(Default::default(), |cx| build_editor(buffer, cx)); view.update(cx, |view, cx| { @@ -7300,7 +7323,7 @@ mod tests { #[gpui::test] fn test_prev_next_word_boundary(cx: &mut gpui::MutableAppContext) { - populate_settings(cx); + cx.set_global(Settings::test(cx)); let buffer = MultiBuffer::build_simple("use std::str::{foo, bar}\n\n {baz.qux()}", cx); let (_, view) = cx.add_window(Default::default(), |cx| build_editor(buffer, cx)); view.update(cx, |view, cx| { @@ -7405,7 +7428,7 @@ mod tests { #[gpui::test] fn test_prev_next_word_bounds_with_soft_wrap(cx: &mut gpui::MutableAppContext) { - populate_settings(cx); + cx.set_global(Settings::test(cx)); let buffer = MultiBuffer::build_simple("use one::{\n two::three::four::five\n};", cx); let (_, view) = cx.add_window(Default::default(), |cx| build_editor(buffer, cx)); @@ -7458,7 +7481,7 @@ mod tests { #[gpui::test] fn test_delete_to_word_boundary(cx: &mut gpui::MutableAppContext) { - populate_settings(cx); + cx.set_global(Settings::test(cx)); let buffer = MultiBuffer::build_simple("one two three four", cx); let (_, view) = cx.add_window(Default::default(), |cx| build_editor(buffer.clone(), cx)); @@ -7495,7 +7518,7 @@ mod tests { #[gpui::test] fn test_newline(cx: &mut gpui::MutableAppContext) { - populate_settings(cx); + cx.set_global(Settings::test(cx)); let buffer = MultiBuffer::build_simple("aaaa\n bbbb\n", cx); let (_, view) = cx.add_window(Default::default(), |cx| build_editor(buffer.clone(), cx)); @@ -7516,7 +7539,7 @@ mod tests { #[gpui::test] fn test_newline_with_old_selections(cx: &mut gpui::MutableAppContext) { - populate_settings(cx); + cx.set_global(Settings::test(cx)); let buffer = MultiBuffer::build_simple( " a @@ -7601,7 +7624,7 @@ mod tests { #[gpui::test] fn test_insert_with_old_selections(cx: &mut gpui::MutableAppContext) { - populate_settings(cx); + cx.set_global(Settings::test(cx)); let buffer = MultiBuffer::build_simple("a( X ), b( Y ), c( Z )", cx); let (_, editor) = cx.add_window(Default::default(), |cx| { let mut editor = build_editor(buffer.clone(), cx); @@ -7628,81 +7651,226 @@ mod tests { #[gpui::test] fn test_indent_outdent(cx: &mut gpui::MutableAppContext) { - populate_settings(cx); - let buffer = MultiBuffer::build_simple(" one two\nthree\n four", cx); + cx.set_global(Settings::test(cx)); + let buffer = MultiBuffer::build_simple( + indoc! {" + one two + three + four"}, + cx, + ); let (_, view) = cx.add_window(Default::default(), |cx| build_editor(buffer.clone(), cx)); view.update(cx, |view, cx| { // two selections on the same line - view.select_display_ranges( - &[ - DisplayPoint::new(0, 2)..DisplayPoint::new(0, 5), - DisplayPoint::new(0, 6)..DisplayPoint::new(0, 9), - ], + select_ranges( + view, + indoc! {" + [one] [two] + three + four"}, cx, ); // indent from mid-tabstop to full tabstop view.tab(&Tab(Direction::Next), cx); - assert_eq!(view.text(cx), " one two\nthree\n four"); - assert_eq!( - view.selected_display_ranges(cx), - &[ - DisplayPoint::new(0, 4)..DisplayPoint::new(0, 7), - DisplayPoint::new(0, 8)..DisplayPoint::new(0, 11), - ] + assert_text_with_selections( + view, + indoc! {" + [one] [two] + three + four"}, + cx, ); // outdent from 1 tabstop to 0 tabstops view.tab(&Tab(Direction::Prev), cx); - assert_eq!(view.text(cx), "one two\nthree\n four"); - assert_eq!( - view.selected_display_ranges(cx), - &[ - DisplayPoint::new(0, 0)..DisplayPoint::new(0, 3), - DisplayPoint::new(0, 4)..DisplayPoint::new(0, 7), - ] + assert_text_with_selections( + view, + indoc! {" + [one] [two] + three + four"}, + cx, ); // select across line ending - view.select_display_ranges(&[DisplayPoint::new(1, 1)..DisplayPoint::new(2, 0)], cx); + select_ranges( + view, + indoc! {" + one two + t[hree + ] four"}, + cx, + ); // indent and outdent affect only the preceding line view.tab(&Tab(Direction::Next), cx); - assert_eq!(view.text(cx), "one two\n three\n four"); - assert_eq!( - view.selected_display_ranges(cx), - &[DisplayPoint::new(1, 5)..DisplayPoint::new(2, 0)] + assert_text_with_selections( + view, + indoc! {" + one two + t[hree + ] four"}, + cx, ); view.tab(&Tab(Direction::Prev), cx); - assert_eq!(view.text(cx), "one two\nthree\n four"); - assert_eq!( - view.selected_display_ranges(cx), - &[DisplayPoint::new(1, 1)..DisplayPoint::new(2, 0)] + assert_text_with_selections( + view, + indoc! {" + one two + t[hree + ] four"}, + cx, ); // Ensure that indenting/outdenting works when the cursor is at column 0. - view.select_display_ranges(&[DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0)], cx); + select_ranges( + view, + indoc! {" + one two + []three + four"}, + cx, + ); view.tab(&Tab(Direction::Next), cx); - assert_eq!(view.text(cx), "one two\n three\n four"); - assert_eq!( - view.selected_display_ranges(cx), - &[DisplayPoint::new(1, 4)..DisplayPoint::new(1, 4)] + assert_text_with_selections( + view, + indoc! {" + one two + []three + four"}, + cx, ); - view.select_display_ranges(&[DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0)], cx); + select_ranges( + view, + indoc! {" + one two + [] three + four"}, + cx, + ); view.tab(&Tab(Direction::Prev), cx); - assert_eq!(view.text(cx), "one two\nthree\n four"); + assert_text_with_selections( + view, + indoc! {" + one two + []three + four"}, + cx, + ); + }); + } + + #[gpui::test] + fn test_indent_outdent_with_excerpts(cx: &mut gpui::MutableAppContext) { + cx.set_global( + Settings::test(cx) + .with_overrides( + "TOML", + LanguageOverride { + tab_size: Some(2), + ..Default::default() + }, + ) + .with_overrides( + "Rust", + LanguageOverride { + tab_size: Some(4), + ..Default::default() + }, + ), + ); + let toml_language = Arc::new(Language::new( + LanguageConfig { + name: "TOML".into(), + ..Default::default() + }, + None, + )); + let rust_language = Arc::new(Language::new( + LanguageConfig { + name: "Rust".into(), + ..Default::default() + }, + None, + )); + + let toml_buffer = cx + .add_model(|cx| Buffer::new(0, "a = 1\nb = 2\n", cx).with_language(toml_language, cx)); + let rust_buffer = cx.add_model(|cx| { + Buffer::new(0, "const c: usize = 3;\n", cx).with_language(rust_language, cx) + }); + let multibuffer = cx.add_model(|cx| { + let mut multibuffer = MultiBuffer::new(0); + multibuffer.push_excerpts( + toml_buffer.clone(), + [Point::new(0, 0)..Point::new(2, 0)], + cx, + ); + multibuffer.push_excerpts( + rust_buffer.clone(), + [Point::new(0, 0)..Point::new(1, 0)], + cx, + ); + multibuffer + }); + + cx.add_window(Default::default(), |cx| { + let mut editor = build_editor(multibuffer, cx); + assert_eq!( - view.selected_display_ranges(cx), - &[DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0)] + editor.text(cx), + indoc! {" + a = 1 + b = 2 + + const c: usize = 3; + "} ); + + select_ranges( + &mut editor, + indoc! {" + [a] = 1 + b = 2 + + [const c:] usize = 3; + "}, + cx, + ); + + editor.tab(&Tab(Direction::Next), cx); + assert_text_with_selections( + &mut editor, + indoc! {" + [a] = 1 + b = 2 + + [const c:] usize = 3; + "}, + cx, + ); + editor.tab(&Tab(Direction::Prev), cx); + assert_text_with_selections( + &mut editor, + indoc! {" + [a] = 1 + b = 2 + + [const c:] usize = 3; + "}, + cx, + ); + + editor }); } #[gpui::test] fn test_backspace(cx: &mut gpui::MutableAppContext) { - populate_settings(cx); + cx.set_global(Settings::test(cx)); let (_, view) = cx.add_window(Default::default(), |cx| { build_editor(MultiBuffer::build_simple("", cx), cx) }); @@ -7747,7 +7915,7 @@ mod tests { #[gpui::test] fn test_delete(cx: &mut gpui::MutableAppContext) { - populate_settings(cx); + cx.set_global(Settings::test(cx)); let buffer = MultiBuffer::build_simple("one two three\nfour five six\nseven eight nine\nten\n", cx); let (_, view) = cx.add_window(Default::default(), |cx| build_editor(buffer.clone(), cx)); @@ -7775,7 +7943,7 @@ mod tests { #[gpui::test] fn test_delete_line(cx: &mut gpui::MutableAppContext) { - populate_settings(cx); + cx.set_global(Settings::test(cx)); let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx); let (_, view) = cx.add_window(Default::default(), |cx| build_editor(buffer, cx)); view.update(cx, |view, cx| { @@ -7798,7 +7966,7 @@ mod tests { ); }); - populate_settings(cx); + cx.set_global(Settings::test(cx)); let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx); let (_, view) = cx.add_window(Default::default(), |cx| build_editor(buffer, cx)); view.update(cx, |view, cx| { @@ -7814,7 +7982,7 @@ mod tests { #[gpui::test] fn test_duplicate_line(cx: &mut gpui::MutableAppContext) { - populate_settings(cx); + cx.set_global(Settings::test(cx)); let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx); let (_, view) = cx.add_window(Default::default(), |cx| build_editor(buffer, cx)); view.update(cx, |view, cx| { @@ -7864,7 +8032,7 @@ mod tests { #[gpui::test] fn test_move_line_up_down(cx: &mut gpui::MutableAppContext) { - populate_settings(cx); + cx.set_global(Settings::test(cx)); let buffer = MultiBuffer::build_simple(&sample_text(10, 5, 'a'), cx); let (_, view) = cx.add_window(Default::default(), |cx| build_editor(buffer, cx)); view.update(cx, |view, cx| { @@ -7960,7 +8128,7 @@ mod tests { #[gpui::test] fn test_move_line_up_down_with_blocks(cx: &mut gpui::MutableAppContext) { - populate_settings(cx); + cx.set_global(Settings::test(cx)); let buffer = MultiBuffer::build_simple(&sample_text(10, 5, 'a'), cx); let snapshot = buffer.read(cx).snapshot(cx); let (_, editor) = cx.add_window(Default::default(), |cx| build_editor(buffer, cx)); @@ -7981,7 +8149,7 @@ mod tests { #[gpui::test] fn test_clipboard(cx: &mut gpui::MutableAppContext) { - populate_settings(cx); + cx.set_global(Settings::test(cx)); let buffer = MultiBuffer::build_simple("one✅ two three four five six ", cx); let view = cx .add_window(Default::default(), |cx| build_editor(buffer.clone(), cx)) @@ -8110,7 +8278,7 @@ mod tests { #[gpui::test] fn test_select_all(cx: &mut gpui::MutableAppContext) { - populate_settings(cx); + cx.set_global(Settings::test(cx)); let buffer = MultiBuffer::build_simple("abc\nde\nfgh", cx); let (_, view) = cx.add_window(Default::default(), |cx| build_editor(buffer, cx)); view.update(cx, |view, cx| { @@ -8124,7 +8292,7 @@ mod tests { #[gpui::test] fn test_select_line(cx: &mut gpui::MutableAppContext) { - populate_settings(cx); + cx.set_global(Settings::test(cx)); let buffer = MultiBuffer::build_simple(&sample_text(6, 5, 'a'), cx); let (_, view) = cx.add_window(Default::default(), |cx| build_editor(buffer, cx)); view.update(cx, |view, cx| { @@ -8169,7 +8337,7 @@ mod tests { #[gpui::test] fn test_split_selection_into_lines(cx: &mut gpui::MutableAppContext) { - populate_settings(cx); + cx.set_global(Settings::test(cx)); let buffer = MultiBuffer::build_simple(&sample_text(9, 5, 'a'), cx); let (_, view) = cx.add_window(Default::default(), |cx| build_editor(buffer, cx)); view.update(cx, |view, cx| { @@ -8235,7 +8403,7 @@ mod tests { #[gpui::test] fn test_add_selection_above_below(cx: &mut gpui::MutableAppContext) { - populate_settings(cx); + cx.set_global(Settings::test(cx)); let buffer = MultiBuffer::build_simple("abc\ndefghi\n\njk\nlmno\n", cx); let (_, view) = cx.add_window(Default::default(), |cx| build_editor(buffer, cx)); @@ -8419,7 +8587,7 @@ mod tests { #[gpui::test] fn test_select_next(cx: &mut gpui::MutableAppContext) { - populate_settings(cx); + cx.set_global(Settings::test(cx)); let (text, ranges) = marked_text_ranges("[abc]\n[abc] [abc]\ndefabc\n[abc]"); let buffer = MultiBuffer::build_simple(&text, cx); @@ -8449,7 +8617,7 @@ mod tests { #[gpui::test] async fn test_select_larger_smaller_syntax_node(cx: &mut gpui::TestAppContext) { - cx.update(populate_settings); + cx.update(|cx| cx.set_global(Settings::test(cx))); let language = Arc::new(Language::new( LanguageConfig::default(), Some(tree_sitter_rust::language()), @@ -8590,7 +8758,7 @@ mod tests { #[gpui::test] async fn test_autoindent_selections(cx: &mut gpui::TestAppContext) { - cx.update(populate_settings); + cx.update(|cx| cx.set_global(Settings::test(cx))); let language = Arc::new( Language::new( LanguageConfig { @@ -8647,7 +8815,7 @@ mod tests { #[gpui::test] async fn test_autoclose_pairs(cx: &mut gpui::TestAppContext) { - cx.update(populate_settings); + cx.update(|cx| cx.set_global(Settings::test(cx))); let language = Arc::new(Language::new( LanguageConfig { brackets: vec![ @@ -8794,7 +8962,7 @@ mod tests { #[gpui::test] async fn test_snippets(cx: &mut gpui::TestAppContext) { - cx.update(populate_settings); + cx.update(|cx| cx.set_global(Settings::test(cx))); let text = " a. b @@ -8902,7 +9070,7 @@ mod tests { #[gpui::test] async fn test_format_during_save(cx: &mut gpui::TestAppContext) { cx.foreground().forbid_parking(); - cx.update(populate_settings); + cx.update(|cx| cx.set_global(Settings::test(cx))); let mut language = Language::new( LanguageConfig { @@ -8954,6 +9122,7 @@ mod tests { params.text_document.uri, lsp::Url::from_file_path("/file.rs").unwrap() ); + assert_eq!(params.options.tab_size, 4); Ok(Some(vec![lsp::TextEdit::new( lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)), ", ".to_string(), @@ -8990,11 +9159,39 @@ mod tests { "one\ntwo\nthree\n" ); assert!(!cx.read(|cx| editor.is_dirty(cx))); + + // Set rust language override and assert overriden tabsize is sent to language server + cx.update(|cx| { + cx.update_global::(|settings, _| { + settings.language_overrides.insert( + "Rust".into(), + LanguageOverride { + tab_size: Some(8), + ..Default::default() + }, + ); + }) + }); + + let save = cx.update(|cx| editor.save(project.clone(), cx)); + fake_server + .handle_request::(move |params, _| async move { + assert_eq!( + params.text_document.uri, + lsp::Url::from_file_path("/file.rs").unwrap() + ); + assert_eq!(params.options.tab_size, 8); + Ok(Some(vec![])) + }) + .next() + .await; + cx.foreground().start_waiting(); + save.await.unwrap(); } #[gpui::test] async fn test_completion(cx: &mut gpui::TestAppContext) { - cx.update(populate_settings); + cx.update(|cx| cx.set_global(Settings::test(cx))); let mut language = Language::new( LanguageConfig { @@ -9235,7 +9432,7 @@ mod tests { #[gpui::test] async fn test_toggle_comment(cx: &mut gpui::TestAppContext) { - cx.update(populate_settings); + cx.update(|cx| cx.set_global(Settings::test(cx))); let language = Arc::new(Language::new( LanguageConfig { line_comment: Some("// ".to_string()), @@ -9315,7 +9512,7 @@ mod tests { #[gpui::test] fn test_editing_disjoint_excerpts(cx: &mut gpui::MutableAppContext) { - populate_settings(cx); + cx.set_global(Settings::test(cx)); let buffer = cx.add_model(|cx| Buffer::new(0, sample_text(3, 4, 'a'), cx)); let multibuffer = cx.add_model(|cx| { let mut multibuffer = MultiBuffer::new(0); @@ -9358,7 +9555,7 @@ mod tests { #[gpui::test] fn test_editing_overlapping_excerpts(cx: &mut gpui::MutableAppContext) { - populate_settings(cx); + cx.set_global(Settings::test(cx)); let buffer = cx.add_model(|cx| Buffer::new(0, sample_text(3, 4, 'a'), cx)); let multibuffer = cx.add_model(|cx| { let mut multibuffer = MultiBuffer::new(0); @@ -9413,7 +9610,7 @@ mod tests { #[gpui::test] fn test_refresh_selections(cx: &mut gpui::MutableAppContext) { - populate_settings(cx); + cx.set_global(Settings::test(cx)); let buffer = cx.add_model(|cx| Buffer::new(0, sample_text(3, 4, 'a'), cx)); let mut excerpt1_id = None; let multibuffer = cx.add_model(|cx| { @@ -9491,7 +9688,7 @@ mod tests { #[gpui::test] fn test_refresh_selections_while_selecting_with_mouse(cx: &mut gpui::MutableAppContext) { - populate_settings(cx); + cx.set_global(Settings::test(cx)); let buffer = cx.add_model(|cx| Buffer::new(0, sample_text(3, 4, 'a'), cx)); let mut excerpt1_id = None; let multibuffer = cx.add_model(|cx| { @@ -9545,7 +9742,7 @@ mod tests { #[gpui::test] async fn test_extra_newline_insertion(cx: &mut gpui::TestAppContext) { - cx.update(populate_settings); + cx.update(|cx| cx.set_global(Settings::test(cx))); let language = Arc::new(Language::new( LanguageConfig { brackets: vec![ @@ -9613,7 +9810,8 @@ mod tests { #[gpui::test] fn test_highlighted_ranges(cx: &mut gpui::MutableAppContext) { let buffer = MultiBuffer::build_simple(&sample_text(16, 8, 'a'), cx); - populate_settings(cx); + + cx.set_global(Settings::test(cx)); let (_, editor) = cx.add_window(Default::default(), |cx| build_editor(buffer.clone(), cx)); editor.update(cx, |editor, cx| { @@ -9692,7 +9890,8 @@ mod tests { #[gpui::test] fn test_following(cx: &mut gpui::MutableAppContext) { let buffer = MultiBuffer::build_simple(&sample_text(16, 8, 'a'), cx); - populate_settings(cx); + + cx.set_global(Settings::test(cx)); let (_, leader) = cx.add_window(Default::default(), |cx| build_editor(buffer.clone(), cx)); let (_, follower) = cx.add_window( @@ -9859,11 +10058,6 @@ mod tests { Editor::new(EditorMode::Full, buffer, None, None, cx) } - fn populate_settings(cx: &mut gpui::MutableAppContext) { - let settings = Settings::test(cx); - cx.set_global(settings); - } - fn assert_selection_ranges( marked_text: &str, selection_marker_pairs: Vec<(char, char)>, diff --git a/crates/editor/src/multi_buffer.rs b/crates/editor/src/multi_buffer.rs index f1e39688be5f363458e4217bd860c71cf0c8ecf9..cf88473435edca75d02042c7ee5ead2da96e1326 100644 --- a/crates/editor/src/multi_buffer.rs +++ b/crates/editor/src/multi_buffer.rs @@ -11,6 +11,7 @@ use language::{ Language, OffsetRangeExt, Outline, OutlineItem, Selection, ToOffset as _, ToPoint as _, ToPointUtf16 as _, TransactionId, }; +use settings::Settings; use std::{ cell::{Ref, RefCell}, cmp, fmt, io, @@ -287,8 +288,6 @@ impl MultiBuffer { S: ToOffset, T: Into, { - let indent_size = crate::INDENT_SIZE; - if self.buffers.borrow().is_empty() { return; } @@ -299,6 +298,8 @@ impl MultiBuffer { .into_iter() .map(|range| range.start.to_offset(&snapshot)..range.end.to_offset(&snapshot)); return buffer.update(cx, |buffer, cx| { + let language_name = buffer.language().map(|language| language.name()); + let indent_size = cx.global::().tab_size(language_name.as_deref()); if autoindent { buffer.edit_with_autoindent(ranges, new_text, indent_size, cx); } else { @@ -394,6 +395,8 @@ impl MultiBuffer { ); } } + let language_name = buffer.language().map(|l| l.name()); + let indent_size = cx.global::().tab_size(language_name.as_deref()); if autoindent { buffer.edit_with_autoindent(deletions, "", indent_size, cx); @@ -863,6 +866,29 @@ impl MultiBuffer { }) } + // If point is at the end of the buffer, the last excerpt is returned + pub fn point_to_buffer_offset<'a, T: ToOffset>( + &'a self, + point: T, + cx: &AppContext, + ) -> Option<(ModelHandle, usize)> { + let snapshot = self.read(cx); + let offset = point.to_offset(&snapshot); + let mut cursor = snapshot.excerpts.cursor::(); + cursor.seek(&offset, Bias::Right, &()); + if cursor.item().is_none() { + cursor.prev(&()); + } + + cursor.item().map(|excerpt| { + let excerpt_start = excerpt.range.start.to_offset(&excerpt.buffer); + let buffer_point = excerpt_start + offset - *cursor.start(); + let buffer = self.buffers.borrow()[&excerpt.buffer_id].buffer.clone(); + + (buffer, buffer_point) + }) + } + pub fn range_to_buffer_ranges<'a, T: ToOffset>( &'a self, range: Range, @@ -1059,12 +1085,13 @@ impl MultiBuffer { .unwrap_or(false) } - pub fn language<'a>(&self, cx: &'a AppContext) -> Option<&'a Arc> { - self.buffers - .borrow() - .values() - .next() - .and_then(|state| state.buffer.read(cx).language()) + pub fn language_at<'a, T: ToOffset>( + &self, + point: T, + cx: &'a AppContext, + ) -> Option<&'a Arc> { + self.point_to_buffer_offset(point, cx) + .and_then(|(buffer, _)| buffer.read(cx).language()) } pub fn file<'a>(&self, cx: &'a AppContext) -> Option<&'a dyn File> { @@ -3762,6 +3789,7 @@ mod tests { #[gpui::test] fn test_history(cx: &mut MutableAppContext) { + cx.set_global(Settings::test(cx)); let buffer_1 = cx.add_model(|cx| Buffer::new(0, "1234", cx)); let buffer_2 = cx.add_model(|cx| Buffer::new(0, "5678", cx)); let multibuffer = cx.add_model(|_| MultiBuffer::new(0)); diff --git a/crates/editor/src/test.rs b/crates/editor/src/test.rs index 7de488a7c70ea1d3cc93e20fd79dbeab80141835..eb23d7e15fd572ae515b5506879ba8118cc09c77 100644 --- a/crates/editor/src/test.rs +++ b/crates/editor/src/test.rs @@ -1,8 +1,9 @@ -use util::test::marked_text; +use gpui::ViewContext; +use util::test::{marked_text, marked_text_ranges}; use crate::{ display_map::{DisplayMap, DisplaySnapshot, ToDisplayPoint}, - DisplayPoint, MultiBuffer, + DisplayPoint, Editor, MultiBuffer, }; #[cfg(test)] @@ -38,3 +39,20 @@ pub fn marked_display_snapshot( (snapshot, markers) } + +pub fn select_ranges(editor: &mut Editor, marked_text: &str, cx: &mut ViewContext) { + let (umarked_text, text_ranges) = marked_text_ranges(marked_text); + assert_eq!(editor.text(cx), umarked_text); + editor.select_ranges(text_ranges, None, cx); +} + +pub fn assert_text_with_selections( + editor: &mut Editor, + marked_text: &str, + cx: &mut ViewContext, +) { + let (unmarked_text, text_ranges) = marked_text_ranges(marked_text); + + assert_eq!(editor.text(cx), unmarked_text); + assert_eq!(editor.selected_ranges(cx), text_ranges); +} diff --git a/crates/workspace/src/pane.rs b/crates/workspace/src/pane.rs index d3fa9dbfbf75d2ef26cf6384ff1cce11b8ff6614..3f018206b04752135635fb827c2f9896d24662cc 100644 --- a/crates/workspace/src/pane.rs +++ b/crates/workspace/src/pane.rs @@ -12,7 +12,7 @@ use gpui::{ AppContext, Entity, MutableAppContext, PromptLevel, Quad, RenderContext, Task, View, ViewContext, ViewHandle, WeakViewHandle, }; -use project::{Project, ProjectEntryId, ProjectPath}; +use project::{ProjectEntryId, ProjectPath}; use settings::Settings; use std::{any::Any, cell::RefCell, cmp, mem, path::Path, rc::Rc}; use util::ResultExt; diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index d15e2ce475bdcce8772fbb383581bc68d5acfdc0..86a059e6fde51125be49713311e654e78e934cc3 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -578,7 +578,7 @@ mod tests { assert!(!editor.is_dirty(cx)); assert_eq!(editor.title(cx), "untitled"); assert!(Arc::ptr_eq( - editor.language(cx).unwrap(), + editor.language_at(0, cx).unwrap(), &languages::PLAIN_TEXT )); editor.handle_input(&editor::Input("hi".into()), cx); @@ -602,7 +602,7 @@ mod tests { editor.read_with(cx, |editor, cx| { assert!(!editor.is_dirty(cx)); assert_eq!(editor.title(cx), "the-new-name.rs"); - assert_eq!(editor.language(cx).unwrap().name().as_ref(), "Rust"); + assert_eq!(editor.language_at(0, cx).unwrap().name().as_ref(), "Rust"); }); // Edit the file and save it again. This time, there is no filename prompt. @@ -668,7 +668,7 @@ mod tests { editor.update(cx, |editor, cx| { assert!(Arc::ptr_eq( - editor.language(cx).unwrap(), + editor.language_at(0, cx).unwrap(), &languages::PLAIN_TEXT )); editor.handle_input(&editor::Input("hi".into()), cx); @@ -682,7 +682,7 @@ mod tests { // The buffer is not dirty anymore and the language is assigned based on the path. editor.read_with(cx, |editor, cx| { assert!(!editor.is_dirty(cx)); - assert_eq!(editor.language(cx).unwrap().name().as_ref(), "Rust") + assert_eq!(editor.language_at(0, cx).unwrap().name().as_ref(), "Rust") }); }