ZIm/crates/language/src/buffer_tests.rs
2025-05-26 11:48:50 -04:00

3668 lines
108 KiB
Rust
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

use super::*;
use crate::Buffer;
use crate::language_settings::{
AllLanguageSettings, AllLanguageSettingsContent, LanguageSettingsContent,
};
use clock::ReplicaId;
use collections::BTreeMap;
use futures::FutureExt as _;
use gpui::{App, AppContext as _, BorrowAppContext, Entity};
use gpui::{HighlightStyle, TestAppContext};
use indoc::indoc;
use proto::deserialize_operation;
use rand::prelude::*;
use regex::RegexBuilder;
use settings::SettingsStore;
use std::collections::BTreeSet;
use std::{
env,
ops::Range,
sync::LazyLock,
time::{Duration, Instant},
};
use syntax_map::TreeSitterOptions;
use text::network::Network;
use text::{BufferId, LineEnding};
use text::{Point, ToPoint};
use theme::ActiveTheme;
use unindent::Unindent as _;
use util::test::marked_text_offsets;
use util::{RandomCharIter, assert_set_eq, post_inc, test::marked_text_ranges};
pub static TRAILING_WHITESPACE_REGEX: LazyLock<regex::Regex> = LazyLock::new(|| {
RegexBuilder::new(r"[ \t]+$")
.multi_line(true)
.build()
.expect("Failed to create TRAILING_WHITESPACE_REGEX")
});
#[cfg(test)]
#[ctor::ctor]
fn init_logger() {
zlog::init_test();
}
#[gpui::test]
fn test_line_endings(cx: &mut gpui::App) {
init_settings(cx, |_| {});
cx.new(|cx| {
let mut buffer =
Buffer::local("one\r\ntwo\rthree", cx).with_language(Arc::new(rust_lang()), cx);
assert_eq!(buffer.text(), "one\ntwo\nthree");
assert_eq!(buffer.line_ending(), LineEnding::Windows);
buffer.check_invariants();
buffer.edit(
[(buffer.len()..buffer.len(), "\r\nfour")],
Some(AutoindentMode::EachLine),
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();
buffer
});
}
#[gpui::test]
fn test_select_language(cx: &mut App) {
init_settings(cx, |_| {});
let registry = Arc::new(LanguageRegistry::test(cx.background_executor().clone()));
registry.add(Arc::new(Language::new(
LanguageConfig {
name: LanguageName::new("Rust"),
matcher: LanguageMatcher {
path_suffixes: vec!["rs".to_string()],
..Default::default()
},
..Default::default()
},
Some(tree_sitter_rust::LANGUAGE.into()),
)));
registry.add(Arc::new(Language::new(
LanguageConfig {
name: LanguageName::new("Make"),
matcher: LanguageMatcher {
path_suffixes: vec!["Makefile".to_string(), "mk".to_string()],
..Default::default()
},
..Default::default()
},
Some(tree_sitter_rust::LANGUAGE.into()),
)));
// matching file extension
assert_eq!(
registry
.language_for_file(&file("src/lib.rs"), None, cx)
.map(|l| l.name()),
Some("Rust".into())
);
assert_eq!(
registry
.language_for_file(&file("src/lib.mk"), None, cx)
.map(|l| l.name()),
Some("Make".into())
);
// matching filename
assert_eq!(
registry
.language_for_file(&file("src/Makefile"), None, cx)
.map(|l| l.name()),
Some("Make".into())
);
// matching suffix that is not the full file extension or filename
assert_eq!(
registry
.language_for_file(&file("zed/cars"), None, cx)
.map(|l| l.name()),
None
);
assert_eq!(
registry
.language_for_file(&file("zed/a.cars"), None, cx)
.map(|l| l.name()),
None
);
assert_eq!(
registry
.language_for_file(&file("zed/sumk"), None, cx)
.map(|l| l.name()),
None
);
}
#[gpui::test(iterations = 10)]
async fn test_first_line_pattern(cx: &mut TestAppContext) {
cx.update(|cx| init_settings(cx, |_| {}));
let languages = LanguageRegistry::test(cx.executor());
let languages = Arc::new(languages);
languages.register_test_language(LanguageConfig {
name: "JavaScript".into(),
matcher: LanguageMatcher {
path_suffixes: vec!["js".into()],
first_line_pattern: Some(Regex::new(r"\bnode\b").unwrap()),
},
..Default::default()
});
assert!(
cx.read(|cx| languages.language_for_file(&file("the/script"), None, cx))
.is_none()
);
assert!(
cx.read(|cx| languages.language_for_file(&file("the/script"), Some(&"nothing".into()), cx))
.is_none()
);
assert_eq!(
cx.read(|cx| languages.language_for_file(
&file("the/script"),
Some(&"#!/bin/env node".into()),
cx
))
.unwrap()
.name(),
"JavaScript".into()
);
}
#[gpui::test]
async fn test_language_for_file_with_custom_file_types(cx: &mut TestAppContext) {
cx.update(|cx| {
init_settings(cx, |settings| {
settings.file_types.extend([
("TypeScript".into(), vec!["js".into()]),
("C++".into(), vec!["c".into()]),
(
"Dockerfile".into(),
vec!["Dockerfile".into(), "Dockerfile.*".into()],
),
]);
})
});
let languages = Arc::new(LanguageRegistry::test(cx.executor()));
for config in [
LanguageConfig {
name: "JavaScript".into(),
matcher: LanguageMatcher {
path_suffixes: vec!["js".to_string()],
..Default::default()
},
..Default::default()
},
LanguageConfig {
name: "TypeScript".into(),
matcher: LanguageMatcher {
path_suffixes: vec!["js".to_string()],
..Default::default()
},
..Default::default()
},
LanguageConfig {
name: "C++".into(),
matcher: LanguageMatcher {
path_suffixes: vec!["cpp".to_string()],
..Default::default()
},
..Default::default()
},
LanguageConfig {
name: "C".into(),
matcher: LanguageMatcher {
path_suffixes: vec!["c".to_string()],
..Default::default()
},
..Default::default()
},
LanguageConfig {
name: "Dockerfile".into(),
matcher: LanguageMatcher {
path_suffixes: vec!["Dockerfile".to_string()],
..Default::default()
},
..Default::default()
},
] {
languages.add(Arc::new(Language::new(config, None)));
}
let language = cx
.read(|cx| languages.language_for_file(&file("foo.js"), None, cx))
.unwrap();
assert_eq!(language.name(), "TypeScript".into());
let language = cx
.read(|cx| languages.language_for_file(&file("foo.c"), None, cx))
.unwrap();
assert_eq!(language.name(), "C++".into());
let language = cx
.read(|cx| languages.language_for_file(&file("Dockerfile.dev"), None, cx))
.unwrap();
assert_eq!(language.name(), "Dockerfile".into());
}
fn file(path: &str) -> Arc<dyn File> {
Arc::new(TestFile {
path: Path::new(path).into(),
root_name: "zed".into(),
local_root: None,
})
}
#[gpui::test]
fn test_edit_events(cx: &mut gpui::App) {
let mut now = Instant::now();
let buffer_1_events = Arc::new(Mutex::new(Vec::new()));
let buffer_2_events = Arc::new(Mutex::new(Vec::new()));
let buffer1 = cx.new(|cx| Buffer::local("abcdef", cx));
let buffer2 = cx.new(|cx| {
Buffer::remote(
BufferId::from(cx.entity_id().as_non_zero_u64()),
1,
Capability::ReadWrite,
"abcdef",
)
});
let buffer1_ops = Arc::new(Mutex::new(Vec::new()));
buffer1.update(cx, {
let buffer1_ops = buffer1_ops.clone();
|buffer, cx| {
let buffer_1_events = buffer_1_events.clone();
cx.subscribe(&buffer1, move |_, _, event, _| match event.clone() {
BufferEvent::Operation {
operation,
is_local: true,
} => buffer1_ops.lock().push(operation),
event => buffer_1_events.lock().push(event),
})
.detach();
let buffer_2_events = buffer_2_events.clone();
cx.subscribe(&buffer2, move |_, _, event, _| match event.clone() {
BufferEvent::Operation {
is_local: false, ..
} => {}
event => buffer_2_events.lock().push(event),
})
.detach();
// 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")], None, cx);
// An empty transaction does not emit any events.
buffer.start_transaction();
buffer.end_transaction(cx);
// A transaction containing two edits emits one edited event.
now += Duration::from_secs(1);
buffer.start_transaction_at(now);
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.
buffer.undo(cx);
}
});
// Incorporating a set of remote ops emits a single edited event,
// followed by a dirty changed event.
buffer2.update(cx, |buffer, cx| {
buffer.apply_ops(buffer1_ops.lock().drain(..), cx);
});
assert_eq!(
mem::take(&mut *buffer_1_events.lock()),
vec![
BufferEvent::Edited,
BufferEvent::DirtyChanged,
BufferEvent::Edited,
BufferEvent::Edited,
]
);
assert_eq!(
mem::take(&mut *buffer_2_events.lock()),
vec![BufferEvent::Edited, BufferEvent::DirtyChanged]
);
buffer1.update(cx, |buffer, cx| {
// Undoing the first transaction emits edited event, followed by a
// dirty changed event, since the buffer is again in a clean state.
buffer.undo(cx);
});
// Incorporating the remote ops again emits a single edited event,
// followed by a dirty changed event.
buffer2.update(cx, |buffer, cx| {
buffer.apply_ops(buffer1_ops.lock().drain(..), cx);
});
assert_eq!(
mem::take(&mut *buffer_1_events.lock()),
vec![BufferEvent::Edited, BufferEvent::DirtyChanged,]
);
assert_eq!(
mem::take(&mut *buffer_2_events.lock()),
vec![BufferEvent::Edited, BufferEvent::DirtyChanged]
);
}
#[gpui::test]
async fn test_apply_diff(cx: &mut TestAppContext) {
let (text, offsets) = marked_text_offsets(
"one two three\nfour fiˇve six\nseven eightˇ nine\nten eleven twelve\n",
);
let buffer = cx.new(|cx| Buffer::local(text, cx));
let anchors = buffer.update(cx, |buffer, _| {
offsets
.iter()
.map(|offset| buffer.anchor_before(offset))
.collect::<Vec<_>>()
});
let (text, offsets) = marked_text_offsets(
"one two three\n{\nfour FIVEˇ six\n}\nseven AND EIGHTˇ nine\nten eleven twelve\n",
);
let diff = buffer.update(cx, |b, cx| b.diff(text.clone(), cx)).await;
buffer.update(cx, |buffer, cx| {
buffer.apply_diff(diff, cx).unwrap();
assert_eq!(buffer.text(), text);
let actual_offsets = anchors
.iter()
.map(|anchor| anchor.to_offset(buffer))
.collect::<Vec<_>>();
assert_eq!(actual_offsets, offsets);
});
let (text, offsets) =
marked_text_offsets("one two three\n{\nˇ}\nseven AND EIGHTEENˇ nine\nten eleven twelve\n");
let diff = buffer.update(cx, |b, cx| b.diff(text.clone(), cx)).await;
buffer.update(cx, |buffer, cx| {
buffer.apply_diff(diff, cx).unwrap();
assert_eq!(buffer.text(), text);
let actual_offsets = anchors
.iter()
.map(|anchor| anchor.to_offset(buffer))
.collect::<Vec<_>>();
assert_eq!(actual_offsets, offsets);
});
}
#[gpui::test(iterations = 10)]
async fn test_normalize_whitespace(cx: &mut gpui::TestAppContext) {
let text = [
"zero", //
"one ", // 2 trailing spaces
"two", //
"three ", // 3 trailing spaces
"four", //
"five ", // 4 trailing spaces
]
.join("\n");
let buffer = cx.new(|cx| Buffer::local(text, cx));
// Spawn a task to format the buffer's whitespace.
// Pause so that the formatting task starts running.
let format = buffer.update(cx, |buffer, cx| buffer.remove_trailing_whitespace(cx));
smol::future::yield_now().await;
// Edit the buffer while the normalization task is running.
let version_before_edit = buffer.update(cx, |buffer, _| buffer.version());
buffer.update(cx, |buffer, cx| {
buffer.edit(
[
(Point::new(0, 1)..Point::new(0, 1), "EE"),
(Point::new(3, 5)..Point::new(3, 5), "EEE"),
],
None,
cx,
);
});
let format_diff = format.await;
buffer.update(cx, |buffer, cx| {
let version_before_format = format_diff.base_version.clone();
buffer.apply_diff(format_diff, cx);
// The outcome depends on the order of concurrent tasks.
//
// If the edit occurred while searching for trailing whitespace ranges,
// then the trailing whitespace region touched by the edit is left intact.
if version_before_format == version_before_edit {
assert_eq!(
buffer.text(),
[
"zEEero", //
"one", //
"two", //
"threeEEE ", //
"four", //
"five", //
]
.join("\n")
);
}
// Otherwise, all trailing whitespace is removed.
else {
assert_eq!(
buffer.text(),
[
"zEEero", //
"one", //
"two", //
"threeEEE", //
"four", //
"five", //
]
.join("\n")
);
}
});
}
#[gpui::test]
async fn test_reparse(cx: &mut gpui::TestAppContext) {
let text = "fn a() {}";
let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(Arc::new(rust_lang()), cx));
// Wait for the initial text to parse
cx.executor().run_until_parked();
assert!(!buffer.update(cx, |buffer, _| buffer.is_parsing()));
assert_eq!(
get_tree_sexp(&buffer, cx),
concat!(
"(source_file (function_item name: (identifier) ",
"parameters: (parameters) ",
"body: (block)))"
)
);
buffer.update(cx, |buffer, _| {
buffer.set_sync_parse_timeout(Duration::ZERO)
});
// Perform some edits (add parameter and variable reference)
// Parsing doesn't begin until the transaction is complete
buffer.update(cx, |buf, cx| {
buf.start_transaction();
let offset = buf.text().find(')').unwrap();
buf.edit([(offset..offset, "b: C")], None, cx);
assert!(!buf.is_parsing());
let offset = buf.text().find('}').unwrap();
buf.edit([(offset..offset, " d; ")], None, cx);
assert!(!buf.is_parsing());
buf.end_transaction(cx);
assert_eq!(buf.text(), "fn a(b: C) { d; }");
assert!(buf.is_parsing());
});
cx.executor().run_until_parked();
assert!(!buffer.update(cx, |buffer, _| buffer.is_parsing()));
assert_eq!(
get_tree_sexp(&buffer, cx),
concat!(
"(source_file (function_item name: (identifier) ",
"parameters: (parameters (parameter pattern: (identifier) type: (type_identifier))) ",
"body: (block (expression_statement (identifier)))))"
)
);
// Perform a series of edits without waiting for the current parse to complete:
// * turn identifier into a field expression
// * turn field expression into a method call
// * add a turbofish to the method call
buffer.update(cx, |buf, cx| {
let offset = buf.text().find(';').unwrap();
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)")], 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, "::<G>")], None, cx);
assert_eq!(buf.text(), "fn a(b: C) { d.e::<G>(f); }");
assert!(buf.is_parsing());
});
cx.executor().run_until_parked();
assert_eq!(
get_tree_sexp(&buffer, cx),
concat!(
"(source_file (function_item name: (identifier) ",
"parameters: (parameters (parameter pattern: (identifier) type: (type_identifier))) ",
"body: (block (expression_statement (call_expression ",
"function: (generic_function ",
"function: (field_expression value: (identifier) field: (field_identifier)) ",
"type_arguments: (type_arguments (type_identifier))) ",
"arguments: (arguments (identifier)))))))",
)
);
buffer.update(cx, |buf, cx| {
buf.undo(cx);
buf.undo(cx);
buf.undo(cx);
buf.undo(cx);
assert_eq!(buf.text(), "fn a() {}");
assert!(buf.is_parsing());
});
cx.executor().run_until_parked();
assert_eq!(
get_tree_sexp(&buffer, cx),
concat!(
"(source_file (function_item name: (identifier) ",
"parameters: (parameters) ",
"body: (block)))"
)
);
buffer.update(cx, |buf, cx| {
buf.redo(cx);
buf.redo(cx);
buf.redo(cx);
buf.redo(cx);
assert_eq!(buf.text(), "fn a(b: C) { d.e::<G>(f); }");
assert!(buf.is_parsing());
});
cx.executor().run_until_parked();
assert_eq!(
get_tree_sexp(&buffer, cx),
concat!(
"(source_file (function_item name: (identifier) ",
"parameters: (parameters (parameter pattern: (identifier) type: (type_identifier))) ",
"body: (block (expression_statement (call_expression ",
"function: (generic_function ",
"function: (field_expression value: (identifier) field: (field_identifier)) ",
"type_arguments: (type_arguments (type_identifier))) ",
"arguments: (arguments (identifier)))))))",
)
);
}
#[gpui::test]
async fn test_resetting_language(cx: &mut gpui::TestAppContext) {
let buffer = cx.new(|cx| {
let mut buffer = Buffer::local("{}", cx).with_language(Arc::new(rust_lang()), cx);
buffer.set_sync_parse_timeout(Duration::ZERO);
buffer
});
// Wait for the initial text to parse
cx.executor().run_until_parked();
assert_eq!(
get_tree_sexp(&buffer, cx),
"(source_file (expression_statement (block)))"
);
buffer.update(cx, |buffer, cx| {
buffer.set_language(Some(Arc::new(json_lang())), cx)
});
cx.executor().run_until_parked();
assert_eq!(get_tree_sexp(&buffer, cx), "(document (object))");
}
#[gpui::test]
async fn test_outline(cx: &mut gpui::TestAppContext) {
let text = r#"
struct Person {
name: String,
age: usize,
}
mod module {
enum LoginState {
LoggedOut,
LoggingOn,
LoggedIn {
person: Person,
time: Instant,
}
}
}
impl Eq for Person {}
impl Drop for Person {
fn drop(&mut self) {
println!("bye");
}
}
"#
.unindent();
let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(Arc::new(rust_lang()), cx));
let outline = buffer
.update(cx, |buffer, _| buffer.snapshot().outline(None))
.unwrap();
assert_eq!(
outline
.items
.iter()
.map(|item| (item.text.as_str(), item.depth))
.collect::<Vec<_>>(),
&[
("struct Person", 0),
("name", 1),
("age", 1),
("mod module", 0),
("enum LoginState", 1),
("LoggedOut", 2),
("LoggingOn", 2),
("LoggedIn", 2),
("person", 3),
("time", 3),
("impl Eq for Person", 0),
("impl Drop for Person", 0),
("fn drop", 1),
]
);
// Without space, we only match on names
assert_eq!(
search(&outline, "oon", cx).await,
&[
("mod module", vec![]), // included as the parent of a match
("enum LoginState", vec![]), // included as the parent of a match
("LoggingOn", vec![1, 7, 8]), // matches
("impl Drop for Person", vec![7, 18, 19]), // matches in two disjoint names
]
);
assert_eq!(
search(&outline, "dp p", cx).await,
&[
("impl Drop for Person", vec![5, 8, 9, 14]),
("fn drop", vec![]),
]
);
assert_eq!(
search(&outline, "dpn", cx).await,
&[("impl Drop for Person", vec![5, 14, 19])]
);
assert_eq!(
search(&outline, "impl ", cx).await,
&[
("impl Eq for Person", vec![0, 1, 2, 3, 4]),
("impl Drop for Person", vec![0, 1, 2, 3, 4]),
("fn drop", vec![]),
]
);
async fn search<'a>(
outline: &'a Outline<Anchor>,
query: &'a str,
cx: &'a gpui::TestAppContext,
) -> Vec<(&'a str, Vec<usize>)> {
let matches = cx
.update(|cx| outline.search(query, cx.background_executor().clone()))
.await;
matches
.into_iter()
.map(|mat| (outline.items[mat.candidate_id].text.as_str(), mat.positions))
.collect::<Vec<_>>()
}
}
#[gpui::test]
async fn test_outline_nodes_with_newlines(cx: &mut gpui::TestAppContext) {
let text = r#"
impl A for B<
C
> {
};
"#
.unindent();
let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(Arc::new(rust_lang()), cx));
let outline = buffer
.update(cx, |buffer, _| buffer.snapshot().outline(None))
.unwrap();
assert_eq!(
outline
.items
.iter()
.map(|item| (item.text.as_str(), item.depth))
.collect::<Vec<_>>(),
&[("impl A for B<", 0)]
);
}
#[gpui::test]
async fn test_outline_with_extra_context(cx: &mut gpui::TestAppContext) {
let language = javascript_lang()
.with_outline_query(
r#"
(function_declaration
"function" @context
name: (_) @name
parameters: (formal_parameters
"(" @context.extra
")" @context.extra)) @item
"#,
)
.unwrap();
let text = r#"
function a() {}
function b(c) {}
"#
.unindent();
let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(Arc::new(language), cx));
let snapshot = buffer.update(cx, |buffer, _| buffer.snapshot());
// extra context nodes are included in the outline.
let outline = snapshot.outline(None).unwrap();
assert_eq!(
outline
.items
.iter()
.map(|item| (item.text.as_str(), item.depth))
.collect::<Vec<_>>(),
&[("function a()", 0), ("function b( )", 0),]
);
// extra context nodes do not appear in breadcrumbs.
let symbols = snapshot.symbols_containing(3, None).unwrap();
assert_eq!(
symbols
.iter()
.map(|item| (item.text.as_str(), item.depth))
.collect::<Vec<_>>(),
&[("function a", 0)]
);
}
#[gpui::test]
fn test_outline_annotations(cx: &mut App) {
// Add this new test case
let text = r#"
/// This is a doc comment
/// that spans multiple lines
fn annotated_function() {
// This is not an annotation
}
// This is a single-line annotation
fn another_function() {}
fn unannotated_function() {}
// This comment is not an annotation
fn function_after_blank_line() {}
"#
.unindent();
let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(Arc::new(rust_lang()), cx));
let outline = buffer
.update(cx, |buffer, _| buffer.snapshot().outline(None))
.unwrap();
assert_eq!(
outline
.items
.into_iter()
.map(|item| (
item.text,
item.depth,
item.annotation_range
.map(|range| { buffer.read(cx).text_for_range(range).collect::<String>() })
))
.collect::<Vec<_>>(),
&[
(
"fn annotated_function".to_string(),
0,
Some("/// This is a doc comment\n/// that spans multiple lines".to_string())
),
(
"fn another_function".to_string(),
0,
Some("// This is a single-line annotation".to_string())
),
("fn unannotated_function".to_string(), 0, None),
("fn function_after_blank_line".to_string(), 0, None),
]
);
}
#[gpui::test]
async fn test_symbols_containing(cx: &mut gpui::TestAppContext) {
let text = r#"
impl Person {
fn one() {
1
}
fn two() {
2
}fn three() {
3
}
}
"#
.unindent();
let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(Arc::new(rust_lang()), cx));
let snapshot = buffer.update(cx, |buffer, _| buffer.snapshot());
// point is at the start of an item
assert_eq!(
symbols_containing(Point::new(1, 4), &snapshot),
vec![
(
"impl Person".to_string(),
Point::new(0, 0)..Point::new(10, 1)
),
("fn one".to_string(), Point::new(1, 4)..Point::new(3, 5))
]
);
// point is in the middle of an item
assert_eq!(
symbols_containing(Point::new(2, 8), &snapshot),
vec![
(
"impl Person".to_string(),
Point::new(0, 0)..Point::new(10, 1)
),
("fn one".to_string(), Point::new(1, 4)..Point::new(3, 5))
]
);
// point is at the end of an item
assert_eq!(
symbols_containing(Point::new(3, 5), &snapshot),
vec![
(
"impl Person".to_string(),
Point::new(0, 0)..Point::new(10, 1)
),
("fn one".to_string(), Point::new(1, 4)..Point::new(3, 5))
]
);
// point is in between two adjacent items
assert_eq!(
symbols_containing(Point::new(7, 5), &snapshot),
vec![
(
"impl Person".to_string(),
Point::new(0, 0)..Point::new(10, 1)
),
("fn two".to_string(), Point::new(5, 4)..Point::new(7, 5))
]
);
fn symbols_containing(
position: Point,
snapshot: &BufferSnapshot,
) -> Vec<(String, Range<Point>)> {
snapshot
.symbols_containing(position, None)
.unwrap()
.into_iter()
.map(|item| {
(
item.text,
item.range.start.to_point(snapshot)..item.range.end.to_point(snapshot),
)
})
.collect()
}
}
#[gpui::test]
fn test_text_objects(cx: &mut App) {
let (text, ranges) = marked_text_ranges(
indoc! {r#"
impl Hello {
fn say() -> u8 { return /* ˇhi */ 1 }
}"#
},
false,
);
let buffer =
cx.new(|cx| Buffer::local(text.clone(), cx).with_language(Arc::new(rust_lang()), cx));
let snapshot = buffer.update(cx, |buffer, _| buffer.snapshot());
let matches = snapshot
.text_object_ranges(ranges[0].clone(), TreeSitterOptions::default())
.map(|(range, text_object)| (&text[range], text_object))
.collect::<Vec<_>>();
assert_eq!(
matches,
&[
("/* hi */", TextObject::AroundComment),
("return /* hi */ 1", TextObject::InsideFunction),
(
"fn say() -> u8 { return /* hi */ 1 }",
TextObject::AroundFunction
),
],
)
}
#[gpui::test]
fn test_enclosing_bracket_ranges(cx: &mut App) {
let mut assert = |selection_text, range_markers| {
assert_bracket_pairs(selection_text, range_markers, rust_lang(), cx)
};
assert(
indoc! {"
mod x {
moˇd y {
}
}
let foo = 1;"},
vec![indoc! {"
mod x «{»
mod y {
}
«}»
let foo = 1;"}],
);
assert(
indoc! {"
mod x {
mod y ˇ{
}
}
let foo = 1;"},
vec![
indoc! {"
mod x «{»
mod y {
}
«}»
let foo = 1;"},
indoc! {"
mod x {
mod y «{»
«}»
}
let foo = 1;"},
],
);
assert(
indoc! {"
mod x {
mod y {
}
let foo = 1;"},
vec![
indoc! {"
mod x «{»
mod y {
}
«}»
let foo = 1;"},
indoc! {"
mod x {
mod y «{»
«}»
}
let foo = 1;"},
],
);
assert(
indoc! {"
mod x {
mod y {
}
ˇ}
let foo = 1;"},
vec![indoc! {"
mod x «{»
mod y {
}
«}»
let foo = 1;"}],
);
assert(
indoc! {"
mod x {
mod y {
}
}
let fˇoo = 1;"},
vec![],
);
// Regression test: avoid crash when querying at the end of the buffer.
assert(
indoc! {"
mod x {
mod y {
}
}
let foo = 1;ˇ"},
vec![],
);
}
#[gpui::test]
fn test_enclosing_bracket_ranges_where_brackets_are_not_outermost_children(cx: &mut App) {
let mut assert = |selection_text, bracket_pair_texts| {
assert_bracket_pairs(selection_text, bracket_pair_texts, javascript_lang(), cx)
};
assert(
indoc! {"
for (const a in b)ˇ {
// a comment that's longer than the for-loop header
}"},
vec![indoc! {"
for «(»const a in b«)» {
// a comment that's longer than the for-loop header
}"}],
);
// Regression test: even though the parent node of the parentheses (the for loop) does
// intersect the given range, the parentheses themselves do not contain the range, so
// they should not be returned. Only the curly braces contain the range.
assert(
indoc! {"
for (const a in b) {ˇ
// a comment that's longer than the for-loop header
}"},
vec![indoc! {"
for (const a in b) «{»
// a comment that's longer than the for-loop header
«}»"}],
);
}
#[gpui::test]
fn test_range_for_syntax_ancestor(cx: &mut App) {
cx.new(|cx| {
let text = "fn a() { b(|c| {}) }";
let buffer = Buffer::local(text, cx).with_language(Arc::new(rust_lang()), cx);
let snapshot = buffer.snapshot();
assert_eq!(
snapshot
.syntax_ancestor(empty_range_at(text, "|"))
.unwrap()
.byte_range(),
range_of(text, "|")
);
assert_eq!(
snapshot
.syntax_ancestor(range_of(text, "|"))
.unwrap()
.byte_range(),
range_of(text, "|c|")
);
assert_eq!(
snapshot
.syntax_ancestor(range_of(text, "|c|"))
.unwrap()
.byte_range(),
range_of(text, "|c| {}")
);
assert_eq!(
snapshot
.syntax_ancestor(range_of(text, "|c| {}"))
.unwrap()
.byte_range(),
range_of(text, "(|c| {})")
);
buffer
});
fn empty_range_at(text: &str, part: &str) -> Range<usize> {
let start = text.find(part).unwrap();
start..start
}
fn range_of(text: &str, part: &str) -> Range<usize> {
let start = text.find(part).unwrap();
start..start + part.len()
}
}
#[gpui::test]
fn test_autoindent_with_soft_tabs(cx: &mut App) {
init_settings(cx, |_| {});
cx.new(|cx| {
let text = "fn a() {}";
let mut buffer = Buffer::local(text, cx).with_language(Arc::new(rust_lang()), 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::EachLine),
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(
[(Point::new(2, 4)..Point::new(2, 4), ".c")],
Some(AutoindentMode::EachLine),
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(
[(Point::new(2, 8)..Point::new(2, 9), "")],
Some(AutoindentMode::EachLine),
cx,
);
assert_eq!(buffer.text(), "fn a() {\n b()\n c\n}");
buffer
});
}
#[gpui::test]
fn test_autoindent_with_hard_tabs(cx: &mut App) {
init_settings(cx, |settings| {
settings.defaults.hard_tabs = Some(true);
});
cx.new(|cx| {
let text = "fn a() {}";
let mut buffer = Buffer::local(text, cx).with_language(Arc::new(rust_lang()), 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::EachLine),
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(
[(Point::new(2, 1)..Point::new(2, 1), ".c")],
Some(AutoindentMode::EachLine),
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(
[(Point::new(2, 2)..Point::new(2, 3), "")],
Some(AutoindentMode::EachLine),
cx,
);
assert_eq!(buffer.text(), "fn a() {\n\tb()\n\tc\n}");
buffer
});
}
#[gpui::test]
fn test_autoindent_does_not_adjust_lines_with_unchanged_suggestion(cx: &mut App) {
init_settings(cx, |_| {});
cx.new(|cx| {
let mut buffer = Buffer::local(
"
fn a() {
c;
d;
}
"
.unindent(),
cx,
)
.with_language(Arc::new(rust_lang()), cx);
// Lines 2 and 3 don't match the indentation suggestion. When editing these lines,
// their indentation is not adjusted.
buffer.edit_via_marked_text(
&"
fn a() {
c«()»;
d«()»;
}
"
.unindent(),
Some(AutoindentMode::EachLine),
cx,
);
assert_eq!(
buffer.text(),
"
fn a() {
c();
d();
}
"
.unindent()
);
// When appending new content after these lines, the indentation is based on the
// preceding lines' actual indentation.
buffer.edit_via_marked_text(
&"
fn a() {
.f
.g()»;
.f
.g()»;
}
"
.unindent(),
Some(AutoindentMode::EachLine),
cx,
);
assert_eq!(
buffer.text(),
"
fn a() {
c
.f
.g();
d
.f
.g();
}
"
.unindent()
);
// Insert a newline after the open brace. It is auto-indented
buffer.edit_via_marked_text(
&"
fn a() {«
»
c
.f
.g();
d
.f
.g();
}
"
.unindent(),
Some(AutoindentMode::EachLine),
cx,
);
assert_eq!(
buffer.text(),
"
fn a() {
ˇ
c
.f
.g();
d
.f
.g();
}
"
.unindent()
.replace("ˇ", "")
);
// Manually outdent the line. It stays outdented.
buffer.edit_via_marked_text(
&"
fn a() {
«»
c
.f
.g();
d
.f
.g();
}
"
.unindent(),
Some(AutoindentMode::EachLine),
cx,
);
assert_eq!(
buffer.text(),
"
fn a() {
c
.f
.g();
d
.f
.g();
}
"
.unindent()
);
buffer
});
cx.new(|cx| {
eprintln!("second buffer: {:?}", cx.entity_id());
let mut buffer = Buffer::local(
"
fn a() {
b();
|
"
.replace('|', "") // marker to preserve trailing whitespace
.unindent(),
cx,
)
.with_language(Arc::new(rust_lang()), cx);
// Insert a closing brace. It is outdented.
buffer.edit_via_marked_text(
&"
fn a() {
b();
«}»
"
.unindent(),
Some(AutoindentMode::EachLine),
cx,
);
assert_eq!(
buffer.text(),
"
fn a() {
b();
}
"
.unindent()
);
// Manually edit the leading whitespace. The edit is preserved.
buffer.edit_via_marked_text(
&"
fn a() {
b();
« »}
"
.unindent(),
Some(AutoindentMode::EachLine),
cx,
);
assert_eq!(
buffer.text(),
"
fn a() {
b();
}
"
.unindent()
);
buffer
});
eprintln!("DONE");
}
#[gpui::test]
fn test_autoindent_does_not_adjust_lines_within_newly_created_errors(cx: &mut App) {
init_settings(cx, |_| {});
cx.new(|cx| {
let mut buffer = Buffer::local(
"
fn a() {
i
}
"
.unindent(),
cx,
)
.with_language(Arc::new(rust_lang()), cx);
// Regression test: line does not get outdented due to syntax error
buffer.edit_via_marked_text(
&"
fn a() {
i«f let Some(x) = y»
}
"
.unindent(),
Some(AutoindentMode::EachLine),
cx,
);
assert_eq!(
buffer.text(),
"
fn a() {
if let Some(x) = y
}
"
.unindent()
);
buffer.edit_via_marked_text(
&"
fn a() {
if let Some(x) = y« {»
}
"
.unindent(),
Some(AutoindentMode::EachLine),
cx,
);
assert_eq!(
buffer.text(),
"
fn a() {
if let Some(x) = y {
}
"
.unindent()
);
buffer
});
}
#[gpui::test]
fn test_autoindent_adjusts_lines_when_only_text_changes(cx: &mut App) {
init_settings(cx, |_| {});
cx.new(|cx| {
let mut buffer = Buffer::local(
"
fn a() {}
"
.unindent(),
cx,
)
.with_language(Arc::new(rust_lang()), cx);
buffer.edit_via_marked_text(
&"
fn a(«
b») {}
"
.unindent(),
Some(AutoindentMode::EachLine),
cx,
);
assert_eq!(
buffer.text(),
"
fn a(
b) {}
"
.unindent()
);
// The indentation suggestion changed because `@end` node (a close paren)
// is now at the beginning of the line.
buffer.edit_via_marked_text(
&"
fn a(
ˇ) {}
"
.unindent(),
Some(AutoindentMode::EachLine),
cx,
);
assert_eq!(
buffer.text(),
"
fn a(
) {}
"
.unindent()
);
buffer
});
}
#[gpui::test]
fn test_autoindent_with_edit_at_end_of_buffer(cx: &mut App) {
init_settings(cx, |_| {});
cx.new(|cx| {
let text = "a\nb";
let mut buffer = Buffer::local(text, cx).with_language(Arc::new(rust_lang()), cx);
buffer.edit(
[(0..1, "\n"), (2..3, "\n")],
Some(AutoindentMode::EachLine),
cx,
);
assert_eq!(buffer.text(), "\n\n\n");
buffer
});
}
#[gpui::test]
fn test_autoindent_multi_line_insertion(cx: &mut App) {
init_settings(cx, |_| {});
cx.new(|cx| {
let text = "
const a: usize = 1;
fn b() {
if c {
let d = 2;
}
}
"
.unindent();
let mut buffer = Buffer::local(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::EachLine),
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_block_mode(cx: &mut App) {
init_settings(cx, |_| {});
cx.new(|cx| {
let text = r#"
fn a() {
b();
}
"#
.unindent();
let mut buffer = Buffer::local(text, cx).with_language(Arc::new(rust_lang()), cx);
// When this text was copied, both of the quotation marks were at the same
// indent level, but the indentation of the first line was not included in
// the copied text. This information is retained in the
// 'original_indent_columns' vector.
let original_indent_columns = vec![Some(4)];
let inserted_text = r#"
"
c
d
e
"
"#
.unindent();
// 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 {
original_indent_columns: original_indent_columns.clone(),
}),
cx,
);
assert_eq!(
buffer.text(),
r#"
fn a() {
b();
"
c
d
e
"
}
"#
.unindent()
);
// Grouping is disabled in tests, so we need 2 undos
buffer.undo(cx); // Undo the auto-indent
buffer.undo(cx); // Undo the original edit
// Insert the block at a deeper indent level. The entire block is outdented.
buffer.edit([(Point::new(2, 0)..Point::new(2, 0), " ")], None, cx);
buffer.edit(
[(Point::new(2, 8)..Point::new(2, 8), inserted_text)],
Some(AutoindentMode::Block {
original_indent_columns: original_indent_columns.clone(),
}),
cx,
);
assert_eq!(
buffer.text(),
r#"
fn a() {
b();
"
c
d
e
"
}
"#
.unindent()
);
buffer
});
}
#[gpui::test]
fn test_autoindent_block_mode_with_newline(cx: &mut App) {
init_settings(cx, |_| {});
cx.new(|cx| {
let text = r#"
fn a() {
b();
}
"#
.unindent();
let mut buffer = Buffer::local(text, cx).with_language(Arc::new(rust_lang()), cx);
// First line contains just '\n', it's indentation is stored in "original_indent_columns"
let original_indent_columns = vec![Some(4)];
let inserted_text = r#"
c();
d();
e();
"#
.unindent();
buffer.edit(
[(Point::new(2, 0)..Point::new(2, 0), inserted_text.clone())],
Some(AutoindentMode::Block {
original_indent_columns: original_indent_columns.clone(),
}),
cx,
);
// While making edit, we ignore first line as it only contains '\n'
// hence second line indent is used to calculate delta
assert_eq!(
buffer.text(),
r#"
fn a() {
b();
c();
d();
e();
}
"#
.unindent()
);
buffer
});
}
#[gpui::test]
fn test_autoindent_block_mode_without_original_indent_columns(cx: &mut App) {
init_settings(cx, |_| {});
cx.new(|cx| {
let text = r#"
fn a() {
if b() {
}
}
"#
.unindent();
let mut buffer = Buffer::local(text, cx).with_language(Arc::new(rust_lang()), cx);
// The original indent columns are not known, so this text is
// auto-indented in a block as if the first line was copied in
// its entirety.
let original_indent_columns = Vec::new();
let inserted_text = " c\n .d()\n .e();";
// 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)],
Some(AutoindentMode::Block {
original_indent_columns: original_indent_columns.clone(),
}),
cx,
);
assert_eq!(
buffer.text(),
r#"
fn a() {
if b() {
c
.d()
.e();
}
}
"#
.unindent()
);
// Grouping is disabled in tests, so we need 2 undos
buffer.undo(cx); // Undo the auto-indent
buffer.undo(cx); // Undo the original edit
// Insert the block at a deeper indent level. The entire block is outdented.
buffer.edit(
[(Point::new(2, 0)..Point::new(2, 0), " ".repeat(12))],
None,
cx,
);
buffer.edit(
[(Point::new(2, 12)..Point::new(2, 12), inserted_text)],
Some(AutoindentMode::Block {
original_indent_columns: Vec::new(),
}),
cx,
);
assert_eq!(
buffer.text(),
r#"
fn a() {
if b() {
c
.d()
.e();
}
}
"#
.unindent()
);
buffer
});
}
#[gpui::test]
fn test_autoindent_block_mode_multiple_adjacent_ranges(cx: &mut App) {
init_settings(cx, |_| {});
cx.new(|cx| {
let (text, ranges_to_replace) = marked_text_ranges(
&"
mod numbers {
«fn one() {
1
}
»
«fn two() {
2
}
»
«fn three() {
3
}
»}
"
.unindent(),
false,
);
let mut buffer = Buffer::local(text, cx).with_language(Arc::new(rust_lang()), cx);
buffer.edit(
[
(ranges_to_replace[0].clone(), "fn one() {\n 101\n}\n"),
(ranges_to_replace[1].clone(), "fn two() {\n 102\n}\n"),
(ranges_to_replace[2].clone(), "fn three() {\n 103\n}\n"),
],
Some(AutoindentMode::Block {
original_indent_columns: vec![Some(0), Some(0), Some(0)],
}),
cx,
);
pretty_assertions::assert_eq!(
buffer.text(),
"
mod numbers {
fn one() {
101
}
fn two() {
102
}
fn three() {
103
}
}
"
.unindent()
);
buffer
});
}
#[gpui::test]
fn test_autoindent_language_without_indents_query(cx: &mut App) {
init_settings(cx, |_| {});
cx.new(|cx| {
let text = "
* one
- a
- b
* two
"
.unindent();
let mut buffer = Buffer::local(text, cx).with_language(
Arc::new(Language::new(
LanguageConfig {
name: "Markdown".into(),
auto_indent_using_last_non_empty_line: false,
..Default::default()
},
Some(tree_sitter_json::LANGUAGE.into()),
)),
cx,
);
buffer.edit(
[(Point::new(3, 0)..Point::new(3, 0), "\n")],
Some(AutoindentMode::EachLine),
cx,
);
assert_eq!(
buffer.text(),
"
* one
- a
- b
* two
"
.unindent()
);
buffer
});
}
#[gpui::test]
fn test_autoindent_with_injected_languages(cx: &mut App) {
init_settings(cx, |settings| {
settings.languages.extend([
(
"HTML".into(),
LanguageSettingsContent {
tab_size: Some(2.try_into().unwrap()),
..Default::default()
},
),
(
"JavaScript".into(),
LanguageSettingsContent {
tab_size: Some(8.try_into().unwrap()),
..Default::default()
},
),
])
});
let html_language = Arc::new(html_lang());
let javascript_language = Arc::new(javascript_lang());
let language_registry = Arc::new(LanguageRegistry::test(cx.background_executor().clone()));
language_registry.add(html_language.clone());
language_registry.add(javascript_language.clone());
cx.new(|cx| {
let (text, ranges) = marked_text_ranges(
&"
<div>ˇ
</div>
<script>
init({ˇ
})
</script>
<span>ˇ
</span>
"
.unindent(),
false,
);
let mut buffer = Buffer::local(text, cx);
buffer.set_language_registry(language_registry);
buffer.set_language(Some(html_language), cx);
buffer.edit(
ranges.into_iter().map(|range| (range, "\na")),
Some(AutoindentMode::EachLine),
cx,
);
assert_eq!(
buffer.text(),
"
<div>
a
</div>
<script>
init({
a
})
</script>
<span>
a
</span>
"
.unindent()
);
buffer
});
}
#[gpui::test]
fn test_autoindent_query_with_outdent_captures(cx: &mut App) {
init_settings(cx, |settings| {
settings.defaults.tab_size = Some(2.try_into().unwrap());
});
cx.new(|cx| {
let mut buffer = Buffer::local("", cx).with_language(Arc::new(ruby_lang()), cx);
let text = r#"
class C
def a(b, c)
puts b
puts c
rescue
puts "errored"
exit 1
end
end
"#
.unindent();
buffer.edit([(0..0, text)], Some(AutoindentMode::EachLine), cx);
assert_eq!(
buffer.text(),
r#"
class C
def a(b, c)
puts b
puts c
rescue
puts "errored"
exit 1
end
end
"#
.unindent()
);
buffer
});
}
#[gpui::test]
async fn test_async_autoindents_preserve_preview(cx: &mut TestAppContext) {
cx.update(|cx| init_settings(cx, |_| {}));
// First we insert some newlines to request an auto-indent (asynchronously).
// Then we request that a preview tab be preserved for the new version, even though it's edited.
let buffer = cx.new(|cx| {
let text = "fn a() {}";
let mut buffer = Buffer::local(text, cx).with_language(Arc::new(rust_lang()), cx);
// This causes autoindent to be async.
buffer.set_sync_parse_timeout(Duration::ZERO);
buffer.edit([(8..8, "\n\n")], Some(AutoindentMode::EachLine), cx);
buffer.refresh_preview();
// Synchronously, we haven't auto-indented and we're still preserving the preview.
assert_eq!(buffer.text(), "fn a() {\n\n}");
assert!(buffer.preserve_preview());
buffer
});
// Now let the autoindent finish
cx.executor().run_until_parked();
// The auto-indent applied, but didn't dismiss our preview
buffer.update(cx, |buffer, cx| {
assert_eq!(buffer.text(), "fn a() {\n \n}");
assert!(buffer.preserve_preview());
// Edit inserting another line. It will autoindent async.
// Then refresh the preview version.
buffer.edit(
[(Point::new(1, 4)..Point::new(1, 4), "\n")],
Some(AutoindentMode::EachLine),
cx,
);
buffer.refresh_preview();
assert_eq!(buffer.text(), "fn a() {\n \n\n}");
assert!(buffer.preserve_preview());
// Then perform another edit, this time without refreshing the preview version.
buffer.edit([(Point::new(1, 4)..Point::new(1, 4), "x")], None, cx);
// This causes the preview to not be preserved.
assert!(!buffer.preserve_preview());
});
// Let the async autoindent from the first edit finish.
cx.executor().run_until_parked();
// The autoindent applies, but it shouldn't restore the preview status because we had an edit in the meantime.
buffer.update(cx, |buffer, _| {
assert_eq!(buffer.text(), "fn a() {\n x\n \n}");
assert!(!buffer.preserve_preview());
});
}
#[gpui::test]
fn test_insert_empty_line(cx: &mut App) {
init_settings(cx, |_| {});
// Insert empty line at the beginning, requesting an empty line above
cx.new(|cx| {
let mut buffer = Buffer::local("abc\ndef\nghi", cx);
let point = buffer.insert_empty_line(Point::new(0, 0), true, false, cx);
assert_eq!(buffer.text(), "\nabc\ndef\nghi");
assert_eq!(point, Point::new(0, 0));
buffer
});
// Insert empty line at the beginning, requesting an empty line above and below
cx.new(|cx| {
let mut buffer = Buffer::local("abc\ndef\nghi", cx);
let point = buffer.insert_empty_line(Point::new(0, 0), true, true, cx);
assert_eq!(buffer.text(), "\n\nabc\ndef\nghi");
assert_eq!(point, Point::new(0, 0));
buffer
});
// Insert empty line at the start of a line, requesting empty lines above and below
cx.new(|cx| {
let mut buffer = Buffer::local("abc\ndef\nghi", cx);
let point = buffer.insert_empty_line(Point::new(2, 0), true, true, cx);
assert_eq!(buffer.text(), "abc\ndef\n\n\n\nghi");
assert_eq!(point, Point::new(3, 0));
buffer
});
// Insert empty line in the middle of a line, requesting empty lines above and below
cx.new(|cx| {
let mut buffer = Buffer::local("abc\ndefghi\njkl", cx);
let point = buffer.insert_empty_line(Point::new(1, 3), true, true, cx);
assert_eq!(buffer.text(), "abc\ndef\n\n\n\nghi\njkl");
assert_eq!(point, Point::new(3, 0));
buffer
});
// Insert empty line in the middle of a line, requesting empty line above only
cx.new(|cx| {
let mut buffer = Buffer::local("abc\ndefghi\njkl", cx);
let point = buffer.insert_empty_line(Point::new(1, 3), true, false, cx);
assert_eq!(buffer.text(), "abc\ndef\n\n\nghi\njkl");
assert_eq!(point, Point::new(3, 0));
buffer
});
// Insert empty line in the middle of a line, requesting empty line below only
cx.new(|cx| {
let mut buffer = Buffer::local("abc\ndefghi\njkl", cx);
let point = buffer.insert_empty_line(Point::new(1, 3), false, true, cx);
assert_eq!(buffer.text(), "abc\ndef\n\n\nghi\njkl");
assert_eq!(point, Point::new(2, 0));
buffer
});
// Insert empty line at the end, requesting empty lines above and below
cx.new(|cx| {
let mut buffer = Buffer::local("abc\ndef\nghi", cx);
let point = buffer.insert_empty_line(Point::new(2, 3), true, true, cx);
assert_eq!(buffer.text(), "abc\ndef\nghi\n\n\n");
assert_eq!(point, Point::new(4, 0));
buffer
});
// Insert empty line at the end, requesting empty line above only
cx.new(|cx| {
let mut buffer = Buffer::local("abc\ndef\nghi", cx);
let point = buffer.insert_empty_line(Point::new(2, 3), true, false, cx);
assert_eq!(buffer.text(), "abc\ndef\nghi\n\n");
assert_eq!(point, Point::new(4, 0));
buffer
});
// Insert empty line at the end, requesting empty line below only
cx.new(|cx| {
let mut buffer = Buffer::local("abc\ndef\nghi", cx);
let point = buffer.insert_empty_line(Point::new(2, 3), false, true, cx);
assert_eq!(buffer.text(), "abc\ndef\nghi\n\n");
assert_eq!(point, Point::new(3, 0));
buffer
});
}
#[gpui::test]
fn test_language_scope_at_with_javascript(cx: &mut App) {
init_settings(cx, |_| {});
cx.new(|cx| {
let language = Language::new(
LanguageConfig {
name: "JavaScript".into(),
line_comments: vec!["// ".into()],
brackets: BracketPairConfig {
pairs: vec![
BracketPair {
start: "{".into(),
end: "}".into(),
close: true,
surround: true,
newline: false,
},
BracketPair {
start: "'".into(),
end: "'".into(),
close: true,
surround: true,
newline: false,
},
],
disabled_scopes_by_bracket_ix: vec![
Vec::new(), //
vec!["string".into(), "comment".into()], // single quotes disabled
],
},
overrides: [(
"element".into(),
LanguageConfigOverride {
line_comments: Override::Remove { remove: true },
block_comment: Override::Set(("{/*".into(), "*/}".into())),
..Default::default()
},
)]
.into_iter()
.collect(),
..Default::default()
},
Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
)
.with_override_query(
r#"
(jsx_element) @element
(string) @string
(comment) @comment.inclusive
[
(jsx_opening_element)
(jsx_closing_element)
(jsx_expression)
] @default
"#,
)
.unwrap();
let text = r#"
a["b"] = <C d="e">
<F></F>
{ g() }
</C>; // a comment
"#
.unindent();
let buffer = Buffer::local(&text, cx).with_language(Arc::new(language), cx);
let snapshot = buffer.snapshot();
let config = snapshot.language_scope_at(0).unwrap();
assert_eq!(config.line_comment_prefixes(), &[Arc::from("// ")]);
// Both bracket pairs are enabled
assert_eq!(
config.brackets().map(|e| e.1).collect::<Vec<_>>(),
&[true, true]
);
let comment_config = snapshot
.language_scope_at(text.find("comment").unwrap() + "comment".len())
.unwrap();
assert_eq!(
comment_config.brackets().map(|e| e.1).collect::<Vec<_>>(),
&[true, false]
);
let string_config = snapshot
.language_scope_at(text.find("b\"").unwrap())
.unwrap();
assert_eq!(string_config.line_comment_prefixes(), &[Arc::from("// ")]);
// Second bracket pair is disabled
assert_eq!(
string_config.brackets().map(|e| e.1).collect::<Vec<_>>(),
&[true, false]
);
// In between JSX tags: use the `element` override.
let element_config = snapshot
.language_scope_at(text.find("<F>").unwrap())
.unwrap();
// TODO nested blocks after newlines are captured with all whitespaces
// https://github.com/tree-sitter/tree-sitter-typescript/issues/306
// assert_eq!(element_config.line_comment_prefixes(), &[]);
// assert_eq!(
// element_config.block_comment_delimiters(),
// Some((&"{/*".into(), &"*/}".into()))
// );
assert_eq!(
element_config.brackets().map(|e| e.1).collect::<Vec<_>>(),
&[true, true]
);
// Within a JSX tag: use the default config.
let tag_config = snapshot
.language_scope_at(text.find(" d=").unwrap() + 1)
.unwrap();
assert_eq!(tag_config.line_comment_prefixes(), &[Arc::from("// ")]);
assert_eq!(
tag_config.brackets().map(|e| e.1).collect::<Vec<_>>(),
&[true, true]
);
// In a JSX expression: use the default config.
let expression_in_element_config = snapshot
.language_scope_at(text.find('{').unwrap() + 1)
.unwrap();
assert_eq!(
expression_in_element_config.line_comment_prefixes(),
&[Arc::from("// ")]
);
assert_eq!(
expression_in_element_config
.brackets()
.map(|e| e.1)
.collect::<Vec<_>>(),
&[true, true]
);
buffer
});
}
#[gpui::test]
fn test_language_scope_at_with_rust(cx: &mut App) {
init_settings(cx, |_| {});
cx.new(|cx| {
let language = Language::new(
LanguageConfig {
name: "Rust".into(),
brackets: BracketPairConfig {
pairs: vec![
BracketPair {
start: "{".into(),
end: "}".into(),
close: true,
surround: true,
newline: false,
},
BracketPair {
start: "'".into(),
end: "'".into(),
close: true,
surround: true,
newline: false,
},
],
disabled_scopes_by_bracket_ix: vec![
Vec::new(), //
vec!["string".into()],
],
},
..Default::default()
},
Some(tree_sitter_rust::LANGUAGE.into()),
)
.with_override_query(
r#"
(string_literal) @string
"#,
)
.unwrap();
let text = r#"
const S: &'static str = "hello";
"#
.unindent();
let buffer = Buffer::local(text.clone(), cx).with_language(Arc::new(language), cx);
let snapshot = buffer.snapshot();
// By default, all brackets are enabled
let config = snapshot.language_scope_at(0).unwrap();
assert_eq!(
config.brackets().map(|e| e.1).collect::<Vec<_>>(),
&[true, true]
);
// Within a string, the quotation brackets are disabled.
let string_config = snapshot
.language_scope_at(text.find("ello").unwrap())
.unwrap();
assert_eq!(
string_config.brackets().map(|e| e.1).collect::<Vec<_>>(),
&[true, false]
);
buffer
});
}
#[gpui::test]
fn test_language_scope_at_with_combined_injections(cx: &mut App) {
init_settings(cx, |_| {});
cx.new(|cx| {
let text = r#"
<ol>
<% people.each do |person| %>
<li>
<%= person.name %>
</li>
<% end %>
</ol>
"#
.unindent();
let language_registry = Arc::new(LanguageRegistry::test(cx.background_executor().clone()));
language_registry.add(Arc::new(ruby_lang()));
language_registry.add(Arc::new(html_lang()));
language_registry.add(Arc::new(erb_lang()));
let mut buffer = Buffer::local(text, cx);
buffer.set_language_registry(language_registry.clone());
buffer.set_language(
language_registry
.language_for_name("ERB")
.now_or_never()
.unwrap()
.ok(),
cx,
);
let snapshot = buffer.snapshot();
let html_config = snapshot.language_scope_at(Point::new(2, 4)).unwrap();
assert_eq!(html_config.line_comment_prefixes(), &[]);
assert_eq!(
html_config.block_comment_delimiters(),
Some((&"<!--".into(), &"-->".into()))
);
let ruby_config = snapshot.language_scope_at(Point::new(3, 12)).unwrap();
assert_eq!(ruby_config.line_comment_prefixes(), &[Arc::from("# ")]);
assert_eq!(ruby_config.block_comment_delimiters(), None);
buffer
});
}
#[gpui::test]
fn test_language_at_with_hidden_languages(cx: &mut App) {
init_settings(cx, |_| {});
cx.new(|cx| {
let text = r#"
this is an *emphasized* word.
"#
.unindent();
let language_registry = Arc::new(LanguageRegistry::test(cx.background_executor().clone()));
language_registry.add(Arc::new(markdown_lang()));
language_registry.add(Arc::new(markdown_inline_lang()));
let mut buffer = Buffer::local(text, cx);
buffer.set_language_registry(language_registry.clone());
buffer.set_language(
language_registry
.language_for_name("Markdown")
.now_or_never()
.unwrap()
.ok(),
cx,
);
let snapshot = buffer.snapshot();
for point in [Point::new(0, 4), Point::new(0, 16)] {
let config = snapshot.language_scope_at(point).unwrap();
assert_eq!(config.language_name(), "Markdown".into());
let language = snapshot.language_at(point).unwrap();
assert_eq!(language.name().as_ref(), "Markdown");
}
buffer
});
}
#[gpui::test]
fn test_language_at_for_markdown_code_block(cx: &mut App) {
init_settings(cx, |_| {});
cx.new(|cx| {
let text = r#"
```rs
let a = 2;
// let b = 3;
```
"#
.unindent();
let language_registry = Arc::new(LanguageRegistry::test(cx.background_executor().clone()));
language_registry.add(Arc::new(markdown_lang()));
language_registry.add(Arc::new(markdown_inline_lang()));
language_registry.add(Arc::new(rust_lang()));
let mut buffer = Buffer::local(text, cx);
buffer.set_language_registry(language_registry.clone());
buffer.set_language(
language_registry
.language_for_name("Markdown")
.now_or_never()
.unwrap()
.ok(),
cx,
);
let snapshot = buffer.snapshot();
// Test points in the code line
for point in [Point::new(1, 4), Point::new(1, 6)] {
let config = snapshot.language_scope_at(point).unwrap();
assert_eq!(config.language_name(), "Rust".into());
let language = snapshot.language_at(point).unwrap();
assert_eq!(language.name().as_ref(), "Rust");
}
// Test points in the comment line to verify it's still detected as Rust
for point in [Point::new(2, 4), Point::new(2, 6)] {
let config = snapshot.language_scope_at(point).unwrap();
assert_eq!(config.language_name(), "Rust".into());
let language = snapshot.language_at(point).unwrap();
assert_eq!(language.name().as_ref(), "Rust");
}
buffer
});
}
#[gpui::test]
fn test_serialization(cx: &mut gpui::App) {
let mut now = Instant::now();
let buffer1 = cx.new(|cx| {
let mut buffer = Buffer::local("abc", cx);
buffer.edit([(3..3, "D")], None, cx);
now += Duration::from_secs(1);
buffer.start_transaction_at(now);
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")], None, cx);
assert_eq!(buffer.text(), "abcDF");
buffer
});
assert_eq!(buffer1.read(cx).text(), "abcDF");
let state = buffer1.read(cx).to_proto(cx);
let ops = cx
.background_executor()
.block(buffer1.read(cx).serialize_ops(None, cx));
let buffer2 = cx.new(|cx| {
let mut buffer = Buffer::from_proto(1, Capability::ReadWrite, state, None).unwrap();
buffer.apply_ops(
ops.into_iter()
.map(|op| proto::deserialize_operation(op).unwrap()),
cx,
);
buffer
});
assert_eq!(buffer2.read(cx).text(), "abcDF");
}
#[gpui::test]
fn test_branch_and_merge(cx: &mut TestAppContext) {
cx.update(|cx| init_settings(cx, |_| {}));
let base = cx.new(|cx| Buffer::local("one\ntwo\nthree\n", cx));
// Create a remote replica of the base buffer.
let base_replica = cx.new(|cx| {
Buffer::from_proto(1, Capability::ReadWrite, base.read(cx).to_proto(cx), None).unwrap()
});
base.update(cx, |_buffer, cx| {
cx.subscribe(&base_replica, |this, _, event, cx| {
if let BufferEvent::Operation {
operation,
is_local: true,
} = event
{
this.apply_ops([operation.clone()], cx);
}
})
.detach();
});
// Create a branch, which initially has the same state as the base buffer.
let branch = base.update(cx, |buffer, cx| buffer.branch(cx));
branch.read_with(cx, |buffer, _| {
assert_eq!(buffer.text(), "one\ntwo\nthree\n");
});
// Edits to the branch are not applied to the base.
branch.update(cx, |buffer, cx| {
buffer.edit(
[
(Point::new(1, 0)..Point::new(1, 0), "1.5\n"),
(Point::new(2, 0)..Point::new(2, 5), "THREE"),
],
None,
cx,
)
});
branch.read_with(cx, |buffer, cx| {
assert_eq!(base.read(cx).text(), "one\ntwo\nthree\n");
assert_eq!(buffer.text(), "one\n1.5\ntwo\nTHREE\n");
});
// Convert from branch buffer ranges to the corresponding ranges in the
// base buffer.
branch.read_with(cx, |buffer, cx| {
assert_eq!(
buffer.range_to_version(4..7, &base.read(cx).version()),
4..4
);
assert_eq!(
buffer.range_to_version(2..9, &base.read(cx).version()),
2..5
);
});
// Edits to the base are applied to the branch.
base.update(cx, |buffer, cx| {
buffer.edit([(Point::new(0, 0)..Point::new(0, 0), "ZERO\n")], None, cx)
});
branch.read_with(cx, |buffer, cx| {
assert_eq!(base.read(cx).text(), "ZERO\none\ntwo\nthree\n");
assert_eq!(buffer.text(), "ZERO\none\n1.5\ntwo\nTHREE\n");
});
// Edits to any replica of the base are applied to the branch.
base_replica.update(cx, |buffer, cx| {
buffer.edit([(Point::new(2, 0)..Point::new(2, 0), "2.5\n")], None, cx)
});
branch.read_with(cx, |buffer, cx| {
assert_eq!(base.read(cx).text(), "ZERO\none\ntwo\n2.5\nthree\n");
assert_eq!(buffer.text(), "ZERO\none\n1.5\ntwo\n2.5\nTHREE\n");
});
// Merging the branch applies all of its changes to the base.
branch.update(cx, |buffer, cx| {
buffer.merge_into_base(Vec::new(), cx);
});
branch.update(cx, |buffer, cx| {
assert_eq!(base.read(cx).text(), "ZERO\none\n1.5\ntwo\n2.5\nTHREE\n");
assert_eq!(buffer.text(), "ZERO\none\n1.5\ntwo\n2.5\nTHREE\n");
});
}
#[gpui::test]
fn test_merge_into_base(cx: &mut TestAppContext) {
cx.update(|cx| init_settings(cx, |_| {}));
let base = cx.new(|cx| Buffer::local("abcdefghijk", cx));
let branch = base.update(cx, |buffer, cx| buffer.branch(cx));
// Make 3 edits, merge one into the base.
branch.update(cx, |branch, cx| {
branch.edit([(0..3, "ABC"), (7..9, "HI"), (11..11, "LMN")], None, cx);
branch.merge_into_base(vec![5..8], cx);
});
branch.read_with(cx, |branch, _| assert_eq!(branch.text(), "ABCdefgHIjkLMN"));
base.read_with(cx, |base, _| assert_eq!(base.text(), "abcdefgHIjk"));
// Undo the one already-merged edit. Merge that into the base.
branch.update(cx, |branch, cx| {
branch.edit([(7..9, "hi")], None, cx);
branch.merge_into_base(vec![5..8], cx);
});
base.read_with(cx, |base, _| assert_eq!(base.text(), "abcdefghijk"));
// Merge an insertion into the base.
branch.update(cx, |branch, cx| {
branch.merge_into_base(vec![11..11], cx);
});
branch.read_with(cx, |branch, _| assert_eq!(branch.text(), "ABCdefghijkLMN"));
base.read_with(cx, |base, _| assert_eq!(base.text(), "abcdefghijkLMN"));
// Deleted the inserted text and merge that into the base.
branch.update(cx, |branch, cx| {
branch.edit([(11..14, "")], None, cx);
branch.merge_into_base(vec![10..11], cx);
});
base.read_with(cx, |base, _| assert_eq!(base.text(), "abcdefghijk"));
}
#[gpui::test]
fn test_undo_after_merge_into_base(cx: &mut TestAppContext) {
cx.update(|cx| init_settings(cx, |_| {}));
let base = cx.new(|cx| Buffer::local("abcdefghijk", cx));
let branch = base.update(cx, |buffer, cx| buffer.branch(cx));
// Make 2 edits, merge one into the base.
branch.update(cx, |branch, cx| {
branch.edit([(0..3, "ABC"), (7..9, "HI")], None, cx);
branch.merge_into_base(vec![7..7], cx);
});
base.read_with(cx, |base, _| assert_eq!(base.text(), "abcdefgHIjk"));
branch.read_with(cx, |branch, _| assert_eq!(branch.text(), "ABCdefgHIjk"));
// Undo the merge in the base buffer.
base.update(cx, |base, cx| {
base.undo(cx);
});
base.read_with(cx, |base, _| assert_eq!(base.text(), "abcdefghijk"));
branch.read_with(cx, |branch, _| assert_eq!(branch.text(), "ABCdefgHIjk"));
// Merge that operation into the base again.
branch.update(cx, |branch, cx| {
branch.merge_into_base(vec![7..7], cx);
});
base.read_with(cx, |base, _| assert_eq!(base.text(), "abcdefgHIjk"));
branch.read_with(cx, |branch, _| assert_eq!(branch.text(), "ABCdefgHIjk"));
}
#[gpui::test]
async fn test_preview_edits(cx: &mut TestAppContext) {
cx.update(|cx| {
init_settings(cx, |_| {});
theme::init(theme::LoadThemes::JustBase, cx);
});
let insertion_style = HighlightStyle {
background_color: Some(cx.read(|cx| cx.theme().status().created_background)),
..Default::default()
};
let deletion_style = HighlightStyle {
background_color: Some(cx.read(|cx| cx.theme().status().deleted_background)),
..Default::default()
};
// no edits
assert_preview_edits(
indoc! {"
fn test_empty() -> bool {
false
}"
},
vec![],
true,
cx,
|hl| {
assert!(hl.text.is_empty());
assert!(hl.highlights.is_empty());
},
)
.await;
// only insertions
assert_preview_edits(
indoc! {"
fn calculate_area(: f64) -> f64 {
std::f64::consts::PI * .powi(2)
}"
},
vec![
(Point::new(0, 18)..Point::new(0, 18), "radius"),
(Point::new(1, 27)..Point::new(1, 27), "radius"),
],
true,
cx,
|hl| {
assert_eq!(
hl.text,
indoc! {"
fn calculate_area(radius: f64) -> f64 {
std::f64::consts::PI * radius.powi(2)"
}
);
assert_eq!(hl.highlights.len(), 2);
assert_eq!(hl.highlights[0], ((18..24), insertion_style));
assert_eq!(hl.highlights[1], ((67..73), insertion_style));
},
)
.await;
// insertions & deletions
assert_preview_edits(
indoc! {"
struct Person {
first_name: String,
}
impl Person {
fn first_name(&self) -> &String {
&self.first_name
}
}"
},
vec![
(Point::new(1, 4)..Point::new(1, 9), "last"),
(Point::new(5, 7)..Point::new(5, 12), "last"),
(Point::new(6, 14)..Point::new(6, 19), "last"),
],
true,
cx,
|hl| {
assert_eq!(
hl.text,
indoc! {"
firstlast_name: String,
}
impl Person {
fn firstlast_name(&self) -> &String {
&self.firstlast_name"
}
);
assert_eq!(hl.highlights.len(), 6);
assert_eq!(hl.highlights[0], ((4..9), deletion_style));
assert_eq!(hl.highlights[1], ((9..13), insertion_style));
assert_eq!(hl.highlights[2], ((52..57), deletion_style));
assert_eq!(hl.highlights[3], ((57..61), insertion_style));
assert_eq!(hl.highlights[4], ((101..106), deletion_style));
assert_eq!(hl.highlights[5], ((106..110), insertion_style));
},
)
.await;
async fn assert_preview_edits(
text: &str,
edits: Vec<(Range<Point>, &str)>,
include_deletions: bool,
cx: &mut TestAppContext,
assert_fn: impl Fn(HighlightedText),
) {
let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(Arc::new(rust_lang()), cx));
let edits = buffer.read_with(cx, |buffer, _| {
edits
.into_iter()
.map(|(range, text)| {
(
buffer.anchor_before(range.start)..buffer.anchor_after(range.end),
text.to_string(),
)
})
.collect::<Vec<_>>()
});
let edit_preview = buffer
.read_with(cx, |buffer, cx| {
buffer.preview_edits(edits.clone().into(), cx)
})
.await;
let highlighted_edits = cx.read(|cx| {
edit_preview.highlight_edits(&buffer.read(cx).snapshot(), &edits, include_deletions, cx)
});
assert_fn(highlighted_edits);
}
}
#[gpui::test(iterations = 100)]
fn test_random_collaboration(cx: &mut App, mut rng: StdRng) {
let min_peers = env::var("MIN_PEERS")
.map(|i| i.parse().expect("invalid `MIN_PEERS` variable"))
.unwrap_or(1);
let max_peers = env::var("MAX_PEERS")
.map(|i| i.parse().expect("invalid `MAX_PEERS` variable"))
.unwrap_or(5);
let operations = env::var("OPERATIONS")
.map(|i| i.parse().expect("invalid `OPERATIONS` variable"))
.unwrap_or(10);
let base_text_len = rng.gen_range(0..10);
let base_text = RandomCharIter::new(&mut rng)
.take(base_text_len)
.collect::<String>();
let mut replica_ids = Vec::new();
let mut buffers = Vec::new();
let network = Arc::new(Mutex::new(Network::new(rng.clone())));
let base_buffer = cx.new(|cx| Buffer::local(base_text.as_str(), cx));
for i in 0..rng.gen_range(min_peers..=max_peers) {
let buffer = cx.new(|cx| {
let state = base_buffer.read(cx).to_proto(cx);
let ops = cx
.background_executor()
.block(base_buffer.read(cx).serialize_ops(None, cx));
let mut buffer =
Buffer::from_proto(i as ReplicaId, Capability::ReadWrite, state, None).unwrap();
buffer.apply_ops(
ops.into_iter()
.map(|op| proto::deserialize_operation(op).unwrap()),
cx,
);
buffer.set_group_interval(Duration::from_millis(rng.gen_range(0..=200)));
let network = network.clone();
cx.subscribe(&cx.entity(), move |buffer, _, event, _| {
if let BufferEvent::Operation {
operation,
is_local: true,
} = event
{
network.lock().broadcast(
buffer.replica_id(),
vec![proto::serialize_operation(operation)],
);
}
})
.detach();
buffer
});
buffers.push(buffer);
replica_ids.push(i as ReplicaId);
network.lock().add_peer(i as ReplicaId);
log::info!("Adding initial peer with replica id {}", i);
}
log::info!("initial text: {:?}", base_text);
let mut now = Instant::now();
let mut mutation_count = operations;
let mut next_diagnostic_id = 0;
let mut active_selections = BTreeMap::default();
loop {
let replica_index = rng.gen_range(0..replica_ids.len());
let replica_id = replica_ids[replica_index];
let buffer = &mut buffers[replica_index];
let mut new_buffer = None;
match rng.gen_range(0..100) {
0..=29 if mutation_count != 0 => {
buffer.update(cx, |buffer, cx| {
buffer.start_transaction_at(now);
buffer.randomly_edit(&mut rng, 5, cx);
buffer.end_transaction_at(now, cx);
log::info!("buffer {} text: {:?}", buffer.replica_id(), buffer.text());
});
mutation_count -= 1;
}
30..=39 if mutation_count != 0 => {
buffer.update(cx, |buffer, cx| {
if rng.gen_bool(0.2) {
log::info!("peer {} clearing active selections", replica_id);
active_selections.remove(&replica_id);
buffer.remove_active_selections(cx);
} else {
let mut selections = Vec::new();
for id in 0..rng.gen_range(1..=5) {
let range = buffer.random_byte_range(0, &mut rng);
selections.push(Selection {
id,
start: buffer.anchor_before(range.start),
end: buffer.anchor_before(range.end),
reversed: false,
goal: SelectionGoal::None,
});
}
let selections: Arc<[Selection<Anchor>]> = selections.into();
log::info!(
"peer {} setting active selections: {:?}",
replica_id,
selections
);
active_selections.insert(replica_id, selections.clone());
buffer.set_active_selections(selections, false, Default::default(), cx);
}
});
mutation_count -= 1;
}
40..=49 if mutation_count != 0 && replica_id == 0 => {
let entry_count = rng.gen_range(1..=5);
buffer.update(cx, |buffer, cx| {
let diagnostics = DiagnosticSet::new(
(0..entry_count).map(|_| {
let range = buffer.random_byte_range(0, &mut rng);
let range = range.to_point_utf16(buffer);
let range = range.start..range.end;
DiagnosticEntry {
range,
diagnostic: Diagnostic {
message: post_inc(&mut next_diagnostic_id).to_string(),
..Default::default()
},
}
}),
buffer,
);
log::info!("peer {} setting diagnostics: {:?}", replica_id, diagnostics);
buffer.update_diagnostics(LanguageServerId(0), diagnostics, cx);
});
mutation_count -= 1;
}
50..=59 if replica_ids.len() < max_peers => {
let old_buffer_state = buffer.read(cx).to_proto(cx);
let old_buffer_ops = cx
.background_executor()
.block(buffer.read(cx).serialize_ops(None, cx));
let new_replica_id = (0..=replica_ids.len() as ReplicaId)
.filter(|replica_id| *replica_id != buffer.read(cx).replica_id())
.choose(&mut rng)
.unwrap();
log::info!(
"Adding new replica {} (replicating from {})",
new_replica_id,
replica_id
);
new_buffer = Some(cx.new(|cx| {
let mut new_buffer = Buffer::from_proto(
new_replica_id,
Capability::ReadWrite,
old_buffer_state,
None,
)
.unwrap();
new_buffer.apply_ops(
old_buffer_ops
.into_iter()
.map(|op| deserialize_operation(op).unwrap()),
cx,
);
log::info!(
"New replica {} text: {:?}",
new_buffer.replica_id(),
new_buffer.text()
);
new_buffer.set_group_interval(Duration::from_millis(rng.gen_range(0..=200)));
let network = network.clone();
cx.subscribe(&cx.entity(), move |buffer, _, event, _| {
if let BufferEvent::Operation {
operation,
is_local: true,
} = event
{
network.lock().broadcast(
buffer.replica_id(),
vec![proto::serialize_operation(operation)],
);
}
})
.detach();
new_buffer
}));
network.lock().replicate(replica_id, new_replica_id);
if new_replica_id as usize == replica_ids.len() {
replica_ids.push(new_replica_id);
} else {
let new_buffer = new_buffer.take().unwrap();
while network.lock().has_unreceived(new_replica_id) {
let ops = network
.lock()
.receive(new_replica_id)
.into_iter()
.map(|op| proto::deserialize_operation(op).unwrap());
if ops.len() > 0 {
log::info!(
"peer {} (version: {:?}) applying {} ops from the network. {:?}",
new_replica_id,
buffer.read(cx).version(),
ops.len(),
ops
);
new_buffer.update(cx, |new_buffer, cx| {
new_buffer.apply_ops(ops, cx);
});
}
}
buffers[new_replica_id as usize] = new_buffer;
}
}
60..=69 if mutation_count != 0 => {
buffer.update(cx, |buffer, cx| {
buffer.randomly_undo_redo(&mut rng, cx);
log::info!("buffer {} text: {:?}", buffer.replica_id(), buffer.text());
});
mutation_count -= 1;
}
_ if network.lock().has_unreceived(replica_id) => {
let ops = network
.lock()
.receive(replica_id)
.into_iter()
.map(|op| proto::deserialize_operation(op).unwrap());
if ops.len() > 0 {
log::info!(
"peer {} (version: {:?}) applying {} ops from the network. {:?}",
replica_id,
buffer.read(cx).version(),
ops.len(),
ops
);
buffer.update(cx, |buffer, cx| buffer.apply_ops(ops, cx));
}
}
_ => {}
}
now += Duration::from_millis(rng.gen_range(0..=200));
buffers.extend(new_buffer);
for buffer in &buffers {
buffer.read(cx).check_invariants();
}
if mutation_count == 0 && network.lock().is_idle() {
break;
}
}
let first_buffer = buffers[0].read(cx).snapshot();
for buffer in &buffers[1..] {
let buffer = buffer.read(cx).snapshot();
assert_eq!(
buffer.version(),
first_buffer.version(),
"Replica {} version != Replica 0 version",
buffer.replica_id()
);
assert_eq!(
buffer.text(),
first_buffer.text(),
"Replica {} text != Replica 0 text",
buffer.replica_id()
);
assert_eq!(
buffer
.diagnostics_in_range::<_, usize>(0..buffer.len(), false)
.collect::<Vec<_>>(),
first_buffer
.diagnostics_in_range::<_, usize>(0..first_buffer.len(), false)
.collect::<Vec<_>>(),
"Replica {} diagnostics != Replica 0 diagnostics",
buffer.replica_id()
);
}
for buffer in &buffers {
let buffer = buffer.read(cx).snapshot();
let actual_remote_selections = buffer
.selections_in_range(Anchor::MIN..Anchor::MAX, false)
.map(|(replica_id, _, _, selections)| (replica_id, selections.collect::<Vec<_>>()))
.collect::<Vec<_>>();
let expected_remote_selections = active_selections
.iter()
.filter(|(replica_id, _)| **replica_id != buffer.replica_id())
.map(|(replica_id, selections)| (*replica_id, selections.iter().collect::<Vec<_>>()))
.collect::<Vec<_>>();
assert_eq!(
actual_remote_selections,
expected_remote_selections,
"Replica {} remote selections != expected selections",
buffer.replica_id()
);
}
}
#[test]
fn test_contiguous_ranges() {
assert_eq!(
contiguous_ranges([1, 2, 3, 5, 6, 9, 10, 11, 12].into_iter(), 100).collect::<Vec<_>>(),
&[1..4, 5..7, 9..13]
);
// Respects the `max_len` parameter
assert_eq!(
contiguous_ranges(
[2, 3, 4, 5, 6, 7, 8, 9, 23, 24, 25, 26, 30, 31].into_iter(),
3
)
.collect::<Vec<_>>(),
&[2..5, 5..8, 8..10, 23..26, 26..27, 30..32],
);
}
#[gpui::test(iterations = 500)]
fn test_trailing_whitespace_ranges(mut rng: StdRng) {
// Generate a random multi-line string containing
// some lines with trailing whitespace.
let mut text = String::new();
for _ in 0..rng.gen_range(0..16) {
for _ in 0..rng.gen_range(0..36) {
text.push(match rng.gen_range(0..10) {
0..=1 => ' ',
3 => '\t',
_ => rng.gen_range('a'..='z'),
});
}
text.push('\n');
}
match rng.gen_range(0..10) {
// sometimes remove the last newline
0..=1 => drop(text.pop()), //
// sometimes add extra newlines
2..=3 => text.push_str(&"\n".repeat(rng.gen_range(1..5))),
_ => {}
}
let rope = Rope::from(text.as_str());
let actual_ranges = trailing_whitespace_ranges(&rope);
let expected_ranges = TRAILING_WHITESPACE_REGEX
.find_iter(&text)
.map(|m| m.range())
.collect::<Vec<_>>();
assert_eq!(
actual_ranges,
expected_ranges,
"wrong ranges for text lines:\n{:?}",
text.split('\n').collect::<Vec<_>>()
);
}
#[gpui::test]
fn test_words_in_range(cx: &mut gpui::App) {
init_settings(cx, |_| {});
// The first line are words excluded from the results with heuristics, we do not expect them in the test assertions.
let contents = r#"
0_isize 123 3.4 4  
let word=öäpple.bar你 Öäpple word2-öÄpPlE-Pizza-word ÖÄPPLE word
"#;
let buffer = cx.new(|cx| {
let buffer = Buffer::local(contents, cx).with_language(Arc::new(rust_lang()), cx);
assert_eq!(buffer.text(), contents);
buffer.check_invariants();
buffer
});
buffer.update(cx, |buffer, _| {
let snapshot = buffer.snapshot();
assert_eq!(
BTreeSet::from_iter(["Pizza".to_string()]),
snapshot
.words_in_range(WordsQuery {
fuzzy_contents: Some("piz"),
skip_digits: true,
range: 0..snapshot.len(),
})
.into_keys()
.collect::<BTreeSet<_>>()
);
assert_eq!(
BTreeSet::from_iter([
"öäpple".to_string(),
"Öäpple".to_string(),
"öÄpPlE".to_string(),
"ÖÄPPLE".to_string(),
]),
snapshot
.words_in_range(WordsQuery {
fuzzy_contents: Some("öp"),
skip_digits: true,
range: 0..snapshot.len(),
})
.into_keys()
.collect::<BTreeSet<_>>()
);
assert_eq!(
BTreeSet::from_iter([
"öÄpPlE".to_string(),
"Öäpple".to_string(),
"ÖÄPPLE".to_string(),
"öäpple".to_string(),
]),
snapshot
.words_in_range(WordsQuery {
fuzzy_contents: Some("öÄ"),
skip_digits: true,
range: 0..snapshot.len(),
})
.into_keys()
.collect::<BTreeSet<_>>()
);
assert_eq!(
BTreeSet::default(),
snapshot
.words_in_range(WordsQuery {
fuzzy_contents: Some("öÄ好"),
skip_digits: true,
range: 0..snapshot.len(),
})
.into_keys()
.collect::<BTreeSet<_>>()
);
assert_eq!(
BTreeSet::from_iter(["bar你".to_string(),]),
snapshot
.words_in_range(WordsQuery {
fuzzy_contents: Some(""),
skip_digits: true,
range: 0..snapshot.len(),
})
.into_keys()
.collect::<BTreeSet<_>>()
);
assert_eq!(
BTreeSet::default(),
snapshot
.words_in_range(WordsQuery {
fuzzy_contents: Some(""),
skip_digits: true,
range: 0..snapshot.len(),
},)
.into_keys()
.collect::<BTreeSet<_>>()
);
assert_eq!(
BTreeSet::from_iter([
"bar你".to_string(),
"öÄpPlE".to_string(),
"Öäpple".to_string(),
"ÖÄPPLE".to_string(),
"öäpple".to_string(),
"let".to_string(),
"Pizza".to_string(),
"word".to_string(),
"word2".to_string(),
]),
snapshot
.words_in_range(WordsQuery {
fuzzy_contents: None,
skip_digits: true,
range: 0..snapshot.len(),
})
.into_keys()
.collect::<BTreeSet<_>>()
);
assert_eq!(
BTreeSet::from_iter([
"0_isize".to_string(),
"123".to_string(),
"3".to_string(),
"4".to_string(),
"bar你".to_string(),
"öÄpPlE".to_string(),
"Öäpple".to_string(),
"ÖÄPPLE".to_string(),
"öäpple".to_string(),
"let".to_string(),
"Pizza".to_string(),
"word".to_string(),
"word2".to_string(),
]),
snapshot
.words_in_range(WordsQuery {
fuzzy_contents: None,
skip_digits: false,
range: 0..snapshot.len(),
})
.into_keys()
.collect::<BTreeSet<_>>()
);
});
}
fn ruby_lang() -> Language {
Language::new(
LanguageConfig {
name: "Ruby".into(),
matcher: LanguageMatcher {
path_suffixes: vec!["rb".to_string()],
..Default::default()
},
line_comments: vec!["# ".into()],
..Default::default()
},
Some(tree_sitter_ruby::LANGUAGE.into()),
)
.with_indents_query(
r#"
(class "end" @end) @indent
(method "end" @end) @indent
(rescue) @outdent
(then) @indent
"#,
)
.unwrap()
}
fn html_lang() -> Language {
Language::new(
LanguageConfig {
name: LanguageName::new("HTML"),
block_comment: Some(("<!--".into(), "-->".into())),
..Default::default()
},
Some(tree_sitter_html::LANGUAGE.into()),
)
.with_indents_query(
"
(element
(start_tag) @start
(end_tag)? @end) @indent
",
)
.unwrap()
.with_injection_query(
r#"
(script_element
(raw_text) @injection.content
(#set! injection.language "javascript"))
"#,
)
.unwrap()
}
fn erb_lang() -> Language {
Language::new(
LanguageConfig {
name: "ERB".into(),
matcher: LanguageMatcher {
path_suffixes: vec!["erb".to_string()],
..Default::default()
},
block_comment: Some(("<%#".into(), "%>".into())),
..Default::default()
},
Some(tree_sitter_embedded_template::LANGUAGE.into()),
)
.with_injection_query(
r#"
(
(code) @injection.content
(#set! injection.language "ruby")
(#set! injection.combined)
)
(
(content) @injection.content
(#set! injection.language "html")
(#set! injection.combined)
)
"#,
)
.unwrap()
}
fn rust_lang() -> Language {
Language::new(
LanguageConfig {
name: "Rust".into(),
matcher: LanguageMatcher {
path_suffixes: vec!["rs".to_string()],
..Default::default()
},
..Default::default()
},
Some(tree_sitter_rust::LANGUAGE.into()),
)
.with_indents_query(
r#"
(call_expression) @indent
(field_expression) @indent
(_ "(" ")" @end) @indent
(_ "{" "}" @end) @indent
"#,
)
.unwrap()
.with_brackets_query(
r#"
("{" @open "}" @close)
"#,
)
.unwrap()
.with_text_object_query(
r#"
(function_item
body: (_
"{"
(_)* @function.inside
"}" )) @function.around
(line_comment)+ @comment.around
(block_comment) @comment.around
"#,
)
.unwrap()
.with_outline_query(
r#"
(line_comment) @annotation
(struct_item
"struct" @context
name: (_) @name) @item
(enum_item
"enum" @context
name: (_) @name) @item
(enum_variant
name: (_) @name) @item
(field_declaration
name: (_) @name) @item
(impl_item
"impl" @context
trait: (_)? @name
"for"? @context
type: (_) @name
body: (_ "{" (_)* "}")) @item
(function_item
"fn" @context
name: (_) @name) @item
(mod_item
"mod" @context
name: (_) @name) @item
"#,
)
.unwrap()
}
fn json_lang() -> Language {
Language::new(
LanguageConfig {
name: "Json".into(),
matcher: LanguageMatcher {
path_suffixes: vec!["js".to_string()],
..Default::default()
},
..Default::default()
},
Some(tree_sitter_json::LANGUAGE.into()),
)
}
fn javascript_lang() -> Language {
Language::new(
LanguageConfig {
name: "JavaScript".into(),
..Default::default()
},
Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
)
.with_brackets_query(
r#"
("{" @open "}" @close)
("(" @open ")" @close)
"#,
)
.unwrap()
.with_indents_query(
r#"
(object "}" @end) @indent
"#,
)
.unwrap()
}
pub fn markdown_lang() -> Language {
Language::new(
LanguageConfig {
name: "Markdown".into(),
matcher: LanguageMatcher {
path_suffixes: vec!["md".into()],
..Default::default()
},
..Default::default()
},
Some(tree_sitter_md::LANGUAGE.into()),
)
.with_injection_query(
r#"
(fenced_code_block
(info_string
(language) @injection.language)
(code_fence_content) @injection.content)
((inline) @injection.content
(#set! injection.language "markdown-inline"))
"#,
)
.unwrap()
}
pub fn markdown_inline_lang() -> Language {
Language::new(
LanguageConfig {
name: "Markdown-Inline".into(),
hidden: true,
..LanguageConfig::default()
},
Some(tree_sitter_md::INLINE_LANGUAGE.into()),
)
.with_highlights_query("(emphasis) @emphasis")
.unwrap()
}
fn get_tree_sexp(buffer: &Entity<Buffer>, cx: &mut gpui::TestAppContext) -> String {
buffer.update(cx, |buffer, _| {
let snapshot = buffer.snapshot();
let layers = snapshot.syntax.layers(buffer.as_text_snapshot());
layers[0].node().to_sexp()
})
}
// Assert that the enclosing bracket ranges around the selection match the pairs indicated by the marked text in `range_markers`
fn assert_bracket_pairs(
selection_text: &'static str,
bracket_pair_texts: Vec<&'static str>,
language: Language,
cx: &mut App,
) {
let (expected_text, selection_ranges) = marked_text_ranges(selection_text, false);
let buffer =
cx.new(|cx| Buffer::local(expected_text.clone(), cx).with_language(Arc::new(language), cx));
let buffer = buffer.update(cx, |buffer, _cx| buffer.snapshot());
let selection_range = selection_ranges[0].clone();
let bracket_pairs = bracket_pair_texts
.into_iter()
.map(|pair_text| {
let (bracket_text, ranges) = marked_text_ranges(pair_text, false);
assert_eq!(bracket_text, expected_text);
(ranges[0].clone(), ranges[1].clone())
})
.collect::<Vec<_>>();
assert_set_eq!(
buffer
.bracket_ranges(selection_range)
.map(|pair| (pair.open_range, pair.close_range))
.collect::<Vec<_>>(),
bracket_pairs
);
}
fn init_settings(cx: &mut App, f: fn(&mut AllLanguageSettingsContent)) {
let settings_store = SettingsStore::test(cx);
cx.set_global(settings_store);
crate::init(cx);
cx.update_global::<SettingsStore, _>(|settings, cx| {
settings.update_user_settings::<AllLanguageSettings>(cx, f);
});
}