From 195215f1e0b2ac03d04327c41ff0b6c734b8e98d Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Fri, 24 Mar 2023 16:37:57 -0600 Subject: [PATCH 1/3] Add "editor: copy highlight json" command Nate needs this to feed to Figma for highlighted code in designs. --- crates/editor/src/editor.rs | 41 ++++++++++++++++++++++++++-- crates/language/src/highlight_map.rs | 1 - 2 files changed, 39 insertions(+), 3 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index c9c448b09f..d83af9d7a9 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -39,7 +39,7 @@ use gpui::{ impl_actions, impl_internal_actions, keymap_matcher::KeymapContext, platform::CursorStyle, - serde_json::json, + serde_json::{self, json}, AnyViewHandle, AppContext, AsyncAppContext, ClipboardItem, Element, ElementBox, Entity, ModelHandle, MouseButton, MutableAppContext, RenderContext, Subscription, Task, View, ViewContext, ViewHandle, WeakViewHandle, @@ -258,7 +258,8 @@ actions!( Hover, Format, ToggleSoftWrap, - RevealInFinder + RevealInFinder, + CopyHighlightJson ] ); @@ -378,6 +379,7 @@ pub fn init(cx: &mut MutableAppContext) { cx.add_action(Editor::jump); cx.add_action(Editor::toggle_soft_wrap); cx.add_action(Editor::reveal_in_finder); + cx.add_action(Editor::copy_highlight_json); cx.add_async_action(Editor::format); cx.add_action(Editor::restart_language_server); cx.add_action(Editor::show_character_palette); @@ -6333,6 +6335,41 @@ impl Editor { ); } } + + /// Copy the highlighted chunks to the clipboard as JSON. The format is an array of lines, + /// with each line being an array of {text, highlight} objects. + fn copy_highlight_json(&mut self, _: &CopyHighlightJson, cx: &mut ViewContext) { + let Some(buffer) = self.buffer.read(cx).as_singleton() else { + return; + }; + + #[derive(Serialize)] + struct Chunk<'a> { + text: &'a str, + highlight: Option<&'a str>, + } + + let snapshot = buffer.read(cx).snapshot(); + let chunks = snapshot.chunks(0..snapshot.len(), true); + let mut lines = Vec::new(); + let mut line = Vec::new(); + + let theme = &cx.global::().theme.editor.syntax; + + for chunk in chunks { + let highlight = chunk.syntax_highlight_id.and_then(|id| id.name(theme)); + let mut chunk_lines = chunk.text.split("\n").peekable(); + while let Some(text) = chunk_lines.next() { + line.push(Chunk { text, highlight }); + if chunk_lines.peek().is_some() { + lines.push(mem::take(&mut line)); + } + } + } + + let Some(lines) = serde_json::to_string_pretty(&lines).log_err() else { return; }; + cx.write_to_clipboard(ClipboardItem::new(lines)); + } } fn consume_contiguous_rows( diff --git a/crates/language/src/highlight_map.rs b/crates/language/src/highlight_map.rs index ddb4dead79..109d79cf70 100644 --- a/crates/language/src/highlight_map.rs +++ b/crates/language/src/highlight_map.rs @@ -59,7 +59,6 @@ impl HighlightId { theme.highlights.get(self.0 as usize).map(|entry| entry.1) } - #[cfg(any(test, feature = "test-support"))] pub fn name<'a>(&self, theme: &'a SyntaxTheme) -> Option<&'a str> { theme.highlights.get(self.0 as usize).map(|e| e.0.as_str()) } From 3dfedd1b21ebf46d03715a86295d9d3f5ec01db8 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Fri, 24 Mar 2023 16:52:00 -0600 Subject: [PATCH 2/3] Merge adjacent chunks with the same highlight name in copied JSON --- crates/editor/src/editor.rs | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index d83af9d7a9..8c6641d8d7 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -6345,14 +6345,14 @@ impl Editor { #[derive(Serialize)] struct Chunk<'a> { - text: &'a str, + text: String, highlight: Option<&'a str>, } let snapshot = buffer.read(cx).snapshot(); let chunks = snapshot.chunks(0..snapshot.len(), true); let mut lines = Vec::new(); - let mut line = Vec::new(); + let mut line: Vec = Vec::new(); let theme = &cx.global::().theme.editor.syntax; @@ -6360,7 +6360,21 @@ impl Editor { let highlight = chunk.syntax_highlight_id.and_then(|id| id.name(theme)); let mut chunk_lines = chunk.text.split("\n").peekable(); while let Some(text) = chunk_lines.next() { - line.push(Chunk { text, highlight }); + let mut merged_with_last_token = false; + if let Some(last_token) = line.last_mut() { + if last_token.highlight == highlight { + last_token.text.push_str(text); + merged_with_last_token = true; + } + } + + if !merged_with_last_token { + line.push(Chunk { + text: text.into(), + highlight, + }); + } + if chunk_lines.peek().is_some() { lines.push(mem::take(&mut line)); } From f0992e7d679ea0342cf2c99f359eb86639c51aae Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Fri, 24 Mar 2023 17:10:50 -0600 Subject: [PATCH 3/3] Trim empty tokens; copy selected range if non-empty --- crates/editor/src/editor.rs | 26 ++++++++++++++++++++++---- 1 file changed, 22 insertions(+), 4 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 8c6641d8d7..b9388dca78 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -6350,9 +6350,20 @@ impl Editor { } let snapshot = buffer.read(cx).snapshot(); - let chunks = snapshot.chunks(0..snapshot.len(), true); + let range = self + .selected_text_range(cx) + .and_then(|selected_range| { + if selected_range.is_empty() { + None + } else { + Some(selected_range) + } + }) + .unwrap_or_else(|| 0..snapshot.len()); + + let chunks = snapshot.chunks(range, true); let mut lines = Vec::new(); - let mut line: Vec = Vec::new(); + let mut line: VecDeque = VecDeque::new(); let theme = &cx.global::().theme.editor.syntax; @@ -6361,7 +6372,7 @@ impl Editor { let mut chunk_lines = chunk.text.split("\n").peekable(); while let Some(text) = chunk_lines.next() { let mut merged_with_last_token = false; - if let Some(last_token) = line.last_mut() { + if let Some(last_token) = line.back_mut() { if last_token.highlight == highlight { last_token.text.push_str(text); merged_with_last_token = true; @@ -6369,13 +6380,20 @@ impl Editor { } if !merged_with_last_token { - line.push(Chunk { + line.push_back(Chunk { text: text.into(), highlight, }); } if chunk_lines.peek().is_some() { + if line.len() > 1 && line.front().unwrap().text.is_empty() { + line.pop_front(); + } + if line.len() > 1 && line.back().unwrap().text.is_empty() { + line.pop_back(); + } + lines.push(mem::take(&mut line)); } }