From d3db700db421d2a505bb3a2d17863573608cc04d Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Wed, 6 Jul 2022 19:00:11 +0200 Subject: [PATCH 1/2] Fix panic on paste when editing with auto-indent Instead of accepting text as it's input by the user, we will read it out of the edit operation after it gets sanitized by the buffer. --- crates/language/src/buffer.rs | 3 ++- crates/language/src/tests.rs | 23 +++++++++++++++++++++++ 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/crates/language/src/buffer.rs b/crates/language/src/buffer.rs index 1e244ab4c5630f14660c18e028e3c2cd5723392f..42cca419008b987e13d3338b5983cfb8be0682df 100644 --- a/crates/language/src/buffer.rs +++ b/crates/language/src/buffer.rs @@ -1233,7 +1233,8 @@ impl Buffer { let inserted_ranges = edits .into_iter() - .filter_map(|(range, new_text)| { + .zip(&edit_operation.as_edit().unwrap().new_text) + .filter_map(|((range, _), new_text)| { let first_newline_ix = new_text.find('\n')?; let new_text_len = new_text.len(); let start = (delta + range.start as isize) as usize + first_newline_ix + 1; diff --git a/crates/language/src/tests.rs b/crates/language/src/tests.rs index a645af3c025e5722cf76c17600611bda1ab7d43a..ba8744624db8a222cf7a3825b15c1605e3903f7d 100644 --- a/crates/language/src/tests.rs +++ b/crates/language/src/tests.rs @@ -22,6 +22,29 @@ fn init_logger() { } } +#[gpui::test] +fn test_line_endings(cx: &mut gpui::MutableAppContext) { + cx.add_model(|cx| { + let mut buffer = + Buffer::new(0, "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_with_autoindent( + [(buffer.len()..buffer.len(), "\r\nfour")], + IndentSize::spaces(2), + cx, + ); + buffer.edit([(0..0, "zero\r\n")], cx); + assert_eq!(buffer.text(), "zero\none\ntwo\nthree\nfour"); + assert_eq!(buffer.line_ending(), LineEnding::Windows); + buffer.check_invariants(); + + buffer + }); +} + #[gpui::test] fn test_select_language() { let registry = LanguageRegistry::test(); From 2c1906d710fce949b5f2c4a96018b05824299cbf Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Wed, 6 Jul 2022 19:32:45 +0200 Subject: [PATCH 2/2] Normalize line endings when parsing completions Co-Authored-By: Max Brunsfeld --- crates/language/src/buffer.rs | 2 +- crates/project/src/project.rs | 9 +++-- crates/project/src/project_tests.rs | 53 +++++++++++++++++++++++++++++ crates/text/src/tests.rs | 2 +- crates/text/src/text.rs | 8 ++--- 5 files changed, 65 insertions(+), 9 deletions(-) diff --git a/crates/language/src/buffer.rs b/crates/language/src/buffer.rs index 42cca419008b987e13d3338b5983cfb8be0682df..cebf5f504ea8c634a38155e5c5654362fb345b12 100644 --- a/crates/language/src/buffer.rs +++ b/crates/language/src/buffer.rs @@ -964,7 +964,7 @@ impl Buffer { cx.background().spawn(async move { let old_text = old_text.to_string(); let line_ending = LineEnding::detect(&new_text); - LineEnding::strip_carriage_returns(&mut new_text); + LineEnding::normalize(&mut new_text); let changes = TextDiff::from_lines(old_text.as_str(), new_text.as_str()) .iter_all_changes() .map(|c| (c.tag(), c.value().len())) diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 0b9a0569c2e3e2613e916e3249e055bdb9a3a66f..e4425e341476dfb4a316bfdc9df5b010e97e0964 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -26,8 +26,9 @@ use language::{ }, range_from_lsp, range_to_lsp, Anchor, Bias, Buffer, CharKind, CodeAction, CodeLabel, Completion, Diagnostic, DiagnosticEntry, DiagnosticSet, Event as BufferEvent, File as _, - Language, LanguageRegistry, LanguageServerName, LocalFile, LspAdapter, OffsetRangeExt, - Operation, Patch, PointUtf16, TextBufferSnapshot, ToOffset, ToPointUtf16, Transaction, + Language, LanguageRegistry, LanguageServerName, LineEnding, LocalFile, LspAdapter, + OffsetRangeExt, Operation, Patch, PointUtf16, TextBufferSnapshot, ToOffset, ToPointUtf16, + Transaction, }; use lsp::{ DiagnosticSeverity, DiagnosticTag, DocumentHighlightKind, LanguageServer, LanguageString, @@ -3381,7 +3382,8 @@ impl Project { return None; } - let (old_range, new_text) = match lsp_completion.text_edit.as_ref() { + let (old_range, mut new_text) = match lsp_completion.text_edit.as_ref() + { // If the language server provides a range to overwrite, then // check that the range is valid. Some(lsp::CompletionTextEdit::Edit(edit)) => { @@ -3431,6 +3433,7 @@ impl Project { } }; + LineEnding::normalize(&mut new_text); Some(Completion { old_range, new_text, diff --git a/crates/project/src/project_tests.rs b/crates/project/src/project_tests.rs index 9d80dadb840a79db455e53e25c38c379317b6061..00f7bb8c9463c32079f6c7057cb0c57f1c2ea46e 100644 --- a/crates/project/src/project_tests.rs +++ b/crates/project/src/project_tests.rs @@ -1850,6 +1850,59 @@ async fn test_completions_without_edit_ranges(cx: &mut gpui::TestAppContext) { ); } +#[gpui::test] +async fn test_completions_with_carriage_returns(cx: &mut gpui::TestAppContext) { + let mut language = Language::new( + LanguageConfig { + name: "TypeScript".into(), + path_suffixes: vec!["ts".to_string()], + ..Default::default() + }, + Some(tree_sitter_typescript::language_typescript()), + ); + let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default()); + + let fs = FakeFs::new(cx.background()); + fs.insert_tree( + "/dir", + json!({ + "a.ts": "", + }), + ) + .await; + + let project = Project::test(fs, ["/dir".as_ref()], cx).await; + project.update(cx, |project, _| project.languages.add(Arc::new(language))); + let buffer = project + .update(cx, |p, cx| p.open_local_buffer("/dir/a.ts", cx)) + .await + .unwrap(); + + let fake_server = fake_language_servers.next().await.unwrap(); + + let text = "let a = b.fqn"; + buffer.update(cx, |buffer, cx| buffer.set_text(text, cx)); + let completions = project.update(cx, |project, cx| { + project.completions(&buffer, text.len(), cx) + }); + + fake_server + .handle_request::(|_, _| async move { + Ok(Some(lsp::CompletionResponse::Array(vec![ + lsp::CompletionItem { + label: "fullyQualifiedName?".into(), + insert_text: Some("fully\rQualified\r\nName".into()), + ..Default::default() + }, + ]))) + }) + .next() + .await; + let completions = completions.await.unwrap(); + assert_eq!(completions.len(), 1); + assert_eq!(completions[0].new_text, "fully\nQualified\nName"); +} + #[gpui::test(iterations = 10)] async fn test_apply_code_actions_with_commands(cx: &mut gpui::TestAppContext) { let mut language = Language::new( diff --git a/crates/text/src/tests.rs b/crates/text/src/tests.rs index c0cc0556d56fd6c50e27256ffbf304dc86b836c2..4da9edd7351d3c67bf128d5d4b94a656217c04a6 100644 --- a/crates/text/src/tests.rs +++ b/crates/text/src/tests.rs @@ -43,7 +43,7 @@ fn test_random_edits(mut rng: StdRng) { .take(reference_string_len) .collect::(); let mut buffer = Buffer::new(0, 0, reference_string.clone().into()); - LineEnding::strip_carriage_returns(&mut reference_string); + LineEnding::normalize(&mut reference_string); buffer.history.group_interval = Duration::from_millis(rng.gen_range(0..=200)); let mut buffer_versions = Vec::new(); diff --git a/crates/text/src/text.rs b/crates/text/src/text.rs index f73163438b13b0b6931af508d011ee385c11d68c..536b8329420628693f84dad0b9398de2577164f0 100644 --- a/crates/text/src/text.rs +++ b/crates/text/src/text.rs @@ -555,7 +555,7 @@ pub struct UndoOperation { impl Buffer { pub fn new(replica_id: u16, remote_id: u64, mut base_text: String) -> Buffer { let line_ending = LineEnding::detect(&base_text); - LineEnding::strip_carriage_returns(&mut base_text); + LineEnding::normalize(&mut base_text); let history = History::new(base_text.into()); let mut fragments = SumTree::new(); @@ -691,7 +691,7 @@ impl Buffer { let mut fragment_start = old_fragments.start().visible; for (range, new_text) in edits { - let new_text = LineEnding::strip_carriage_returns_from_arc(new_text.into()); + let new_text = LineEnding::normalize_arc(new_text.into()); let fragment_end = old_fragments.end(&None).visible; // If the current fragment ends before this range, then jump ahead to the first fragment @@ -2385,13 +2385,13 @@ impl LineEnding { } } - pub fn strip_carriage_returns(text: &mut String) { + pub fn normalize(text: &mut String) { if let Cow::Owned(replaced) = CARRIAGE_RETURNS_REGEX.replace_all(text, "\n") { *text = replaced; } } - fn strip_carriage_returns_from_arc(text: Arc) -> Arc { + fn normalize_arc(text: Arc) -> Arc { if let Cow::Owned(replaced) = CARRIAGE_RETURNS_REGEX.replace_all(&text, "\n") { replaced.into() } else {