From 361428a4509b060079f3863d02e519245490677f Mon Sep 17 00:00:00 2001 From: Ian Chamberlain Date: Tue, 31 Mar 2026 01:39:44 -0700 Subject: [PATCH] editor: Limit `CopyHighlightJson` to selection (#46555) Closes https://github.com/zed-industries/zed/issues/36618 I'm not sure if there is a specific issue for this, but I noticed it while working on some other PRs, as have some other people: https://github.com/zed-industries/zed/issues/20525#issuecomment-2469507157 Cause: when running `copy highlight JSON` from the command palette, input was disabled due to the modal, and the selection method call always returns `None`. This most likely works as expected when bound to a keyboard shortcut, but since there is no default shortcut most people probably don't execute this action that way. Fix: just grab the selection directly; I don't think this command needs to be IME-aware, so it doesn't need to use `selected_text_range`. NOTE: There still seems to be an issue where `VISUAL LINE` mode doesn't select anything, even when I called `selections.newest_adjusted`, so I opted to try and keep the implementation closest to what it was doing before. (edit: actually this might just be the same as #45799?). Release Notes: - Fixed `editor: copy highlight JSON` not limiting to the current selection --- crates/editor/src/editor.rs | 36 +++--- crates/editor/src/editor_tests.rs | 206 ++++++++++++++++++++++++++++++ 2 files changed, 224 insertions(+), 18 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 6d9ee235f01782d43bca485d50272bddf306b837..76ec95928dc729e12060e75f8ec7d61197624c5f 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -25043,7 +25043,7 @@ impl Editor { fn copy_highlight_json( &mut self, _: &CopyHighlightJson, - window: &mut Window, + _: &mut Window, cx: &mut Context, ) { #[derive(Serialize)] @@ -25053,23 +25053,19 @@ impl Editor { } let snapshot = self.buffer.read(cx).snapshot(cx); - let range = self - .selected_text_range(false, window, cx) - .and_then(|selection| { - if selection.range.is_empty() { - None - } else { - Some( - snapshot.offset_utf16_to_offset(MultiBufferOffsetUtf16(OffsetUtf16( - selection.range.start, - ))) - ..snapshot.offset_utf16_to_offset(MultiBufferOffsetUtf16(OffsetUtf16( - selection.range.end, - ))), - ) - } - }) - .unwrap_or_else(|| MultiBufferOffset(0)..snapshot.len()); + let mut selection = self.selections.newest::(&self.display_snapshot(cx)); + let max_point = snapshot.max_point(); + + let range = if self.selections.line_mode() { + selection.start = Point::new(selection.start.row, 0); + selection.end = cmp::min(max_point, Point::new(selection.end.row + 1, 0)); + selection.goal = SelectionGoal::None; + selection.range() + } else if selection.is_empty() { + Point::new(0, 0)..max_point + } else { + selection.range() + }; let chunks = snapshot.chunks(range, true); let mut lines = Vec::new(); @@ -25114,6 +25110,10 @@ impl Editor { } } + if line.iter().any(|chunk| !chunk.text.is_empty()) { + lines.push(line); + } + let Some(lines) = serde_json::to_string_pretty(&lines).log_err() else { return; }; diff --git a/crates/editor/src/editor_tests.rs b/crates/editor/src/editor_tests.rs index 3fcd0f08fd5faef55f4c77df674259cd30728c2b..dc84e826f6a74ae439137e1eb7592e3b2c6413a8 100644 --- a/crates/editor/src/editor_tests.rs +++ b/crates/editor/src/editor_tests.rs @@ -19253,6 +19253,212 @@ fn test_highlighted_ranges(cx: &mut TestAppContext) { }); } +#[gpui::test] +async fn test_copy_highlight_json(cx: &mut TestAppContext) { + init_test(cx, |_| {}); + + let mut cx = EditorTestContext::new(cx).await; + cx.set_state(indoc! {" + fn main() { + let x = 1;ˇ + } + "}); + setup_rust_syntax_highlighting(&mut cx); + + cx.update_editor(|editor, window, cx| { + editor.copy_highlight_json(&CopyHighlightJson, window, cx); + }); + + let clipboard_json: serde_json::Value = + serde_json::from_str(&cx.read_from_clipboard().unwrap().text().unwrap()).unwrap(); + assert_eq!( + clipboard_json, + json!([ + [ + {"text": "fn", "highlight": "keyword"}, + {"text": " ", "highlight": null}, + {"text": "main", "highlight": "function"}, + {"text": "()", "highlight": "punctuation.bracket"}, + {"text": " ", "highlight": null}, + {"text": "{", "highlight": "punctuation.bracket"}, + ], + [ + {"text": " ", "highlight": null}, + {"text": "let", "highlight": "keyword"}, + {"text": " ", "highlight": null}, + {"text": "x", "highlight": "variable"}, + {"text": " ", "highlight": null}, + {"text": "=", "highlight": "operator"}, + {"text": " ", "highlight": null}, + {"text": "1", "highlight": "number"}, + {"text": ";", "highlight": "punctuation.delimiter"}, + ], + [ + {"text": "}", "highlight": "punctuation.bracket"}, + ], + ]) + ); +} + +#[gpui::test] +async fn test_copy_highlight_json_selected_range(cx: &mut TestAppContext) { + init_test(cx, |_| {}); + + let mut cx = EditorTestContext::new(cx).await; + cx.set_state(indoc! {" + fn main() { + «let x = 1; + let yˇ» = 2; + } + "}); + setup_rust_syntax_highlighting(&mut cx); + + cx.update_editor(|editor, window, cx| { + editor.copy_highlight_json(&CopyHighlightJson, window, cx); + }); + + let clipboard_json: serde_json::Value = + serde_json::from_str(&cx.read_from_clipboard().unwrap().text().unwrap()).unwrap(); + assert_eq!( + clipboard_json, + json!([ + [ + {"text": "let", "highlight": "keyword"}, + {"text": " ", "highlight": null}, + {"text": "x", "highlight": "variable"}, + {"text": " ", "highlight": null}, + {"text": "=", "highlight": "operator"}, + {"text": " ", "highlight": null}, + {"text": "1", "highlight": "number"}, + {"text": ";", "highlight": "punctuation.delimiter"}, + ], + [ + {"text": " ", "highlight": null}, + {"text": "let", "highlight": "keyword"}, + {"text": " ", "highlight": null}, + {"text": "y", "highlight": "variable"}, + ], + ]) + ); +} + +#[gpui::test] +async fn test_copy_highlight_json_selected_line_range(cx: &mut TestAppContext) { + init_test(cx, |_| {}); + + let mut cx = EditorTestContext::new(cx).await; + + cx.set_state(indoc! {" + fn main() { + «let x = 1; + let yˇ» = 2; + } + "}); + setup_rust_syntax_highlighting(&mut cx); + + cx.update_editor(|editor, window, cx| { + editor.selections.set_line_mode(true); + editor.copy_highlight_json(&CopyHighlightJson, window, cx); + }); + + let clipboard_json: serde_json::Value = + serde_json::from_str(&cx.read_from_clipboard().unwrap().text().unwrap()).unwrap(); + assert_eq!( + clipboard_json, + json!([ + [ + {"text": " ", "highlight": null}, + {"text": "let", "highlight": "keyword"}, + {"text": " ", "highlight": null}, + {"text": "x", "highlight": "variable"}, + {"text": " ", "highlight": null}, + {"text": "=", "highlight": "operator"}, + {"text": " ", "highlight": null}, + {"text": "1", "highlight": "number"}, + {"text": ";", "highlight": "punctuation.delimiter"}, + ], + [ + {"text": " ", "highlight": null}, + {"text": "let", "highlight": "keyword"}, + {"text": " ", "highlight": null}, + {"text": "y", "highlight": "variable"}, + {"text": " ", "highlight": null}, + {"text": "=", "highlight": "operator"}, + {"text": " ", "highlight": null}, + {"text": "2", "highlight": "number"}, + {"text": ";", "highlight": "punctuation.delimiter"}, + ], + ]) + ); +} + +#[gpui::test] +async fn test_copy_highlight_json_single_line(cx: &mut TestAppContext) { + init_test(cx, |_| {}); + + let mut cx = EditorTestContext::new(cx).await; + + cx.set_state(indoc! {" + fn main() { + let ˇx = 1; + let y = 2; + } + "}); + setup_rust_syntax_highlighting(&mut cx); + + cx.update_editor(|editor, window, cx| { + editor.selections.set_line_mode(true); + editor.copy_highlight_json(&CopyHighlightJson, window, cx); + }); + + let clipboard_json: serde_json::Value = + serde_json::from_str(&cx.read_from_clipboard().unwrap().text().unwrap()).unwrap(); + assert_eq!( + clipboard_json, + json!([ + [ + {"text": " ", "highlight": null}, + {"text": "let", "highlight": "keyword"}, + {"text": " ", "highlight": null}, + {"text": "x", "highlight": "variable"}, + {"text": " ", "highlight": null}, + {"text": "=", "highlight": "operator"}, + {"text": " ", "highlight": null}, + {"text": "1", "highlight": "number"}, + {"text": ";", "highlight": "punctuation.delimiter"}, + ] + ]) + ); +} + +fn setup_rust_syntax_highlighting(cx: &mut EditorTestContext) { + let syntax = SyntaxTheme::new_test(vec![ + ("keyword", Hsla::red()), + ("function", Hsla::blue()), + ("variable", Hsla::green()), + ("number", Hsla::default()), + ("operator", Hsla::default()), + ("punctuation.bracket", Hsla::default()), + ("punctuation.delimiter", Hsla::default()), + ]); + + let language = rust_lang(); + language.set_theme(&syntax); + + cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx)); + cx.executor().run_until_parked(); + cx.update_editor(|editor, window, cx| { + editor.set_style( + EditorStyle { + syntax: Arc::new(syntax), + ..Default::default() + }, + window, + cx, + ); + }); +} + #[gpui::test] async fn test_following(cx: &mut TestAppContext) { init_test(cx, |_| {});