Use language specific tabsize in editor commands

Co-authored-by: Max Brunsfeld <max@zed.dev>
This commit is contained in:
Keith Simmons 2022-04-06 14:41:51 -07:00
parent 36f4d8f9e4
commit 67b15ee037
11 changed files with 390 additions and 129 deletions

1
Cargo.lock generated
View File

@ -1638,6 +1638,7 @@ dependencies = [
"futures",
"fuzzy",
"gpui",
"indoc",
"itertools",
"language",
"lazy_static",

View File

@ -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"

View File

@ -204,7 +204,12 @@ impl DisplayMap {
}
fn tab_size(buffer: &ModelHandle<MultiBuffer>, cx: &mut ModelContext<Self>) -> 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::<Settings>().tab_size(language_name.as_deref())
}

View File

@ -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);

View File

@ -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);

View File

@ -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")

View File

@ -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<Language>> {
self.buffer.read(cx).language(cx)
pub fn language_at<'a, T: ToOffset>(
&self,
point: T,
cx: &'a AppContext,
) -> Option<&'a Arc<Language>> {
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::<Settings>().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::<Settings>().tab_size;
let mut selections = self.local_selections::<Point>(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::<Settings>().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<Self>) {
let tab_size = cx.global::<Settings>().tab_size;
let mut selections = self.local_selections::<Point>(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::<Settings>().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<Self>) {
let tab_size = cx.global::<Settings>().tab_size;
let selections = self.local_selections::<Point>(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::<Settings>().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<Self>) {
// 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::<Point>(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::<Settings>();
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_eq!(
view.selected_display_ranges(cx),
&[DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0)]
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!(
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, _| {
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::<lsp::request::Formatting, _, _>(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)>,

View File

@ -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<String>,
{
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::<Settings>().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::<Settings>().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<Buffer>, usize)> {
let snapshot = self.read(cx);
let offset = point.to_offset(&snapshot);
let mut cursor = snapshot.excerpts.cursor::<usize>();
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<T>,
@ -1059,12 +1085,13 @@ impl MultiBuffer {
.unwrap_or(false)
}
pub fn language<'a>(&self, cx: &'a AppContext) -> Option<&'a Arc<Language>> {
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<Language>> {
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));

View File

@ -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<Editor>) {
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<Editor>,
) {
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);
}

View File

@ -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;

View File

@ -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")
});
}