From 1f611a9c90a0fc636d11a520b62ea0cf4064299a Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Fri, 17 May 2024 16:41:46 -0600 Subject: [PATCH] Allow copy-pasting dev-server-token (#11992) Release Notes: - N/A --- Cargo.lock | 3 +- assets/keymaps/default-linux.json | 6 ++ assets/keymaps/default-macos.json | 6 ++ crates/assistant2/src/assistant2.rs | 8 +- crates/gpui/src/elements/text.rs | 13 +++ crates/markdown/examples/markdown.rs | 5 +- crates/markdown/src/markdown.rs | 124 ++++++++++++++++++++-- crates/project_panel/src/project_panel.rs | 4 +- crates/recent_projects/Cargo.toml | 3 +- crates/recent_projects/src/dev_servers.rs | 56 ++++++---- 10 files changed, 184 insertions(+), 44 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index bd8615bc6d..88c7eaf0e5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8044,6 +8044,7 @@ dependencies = [ "fuzzy", "gpui", "language", + "markdown", "menu", "ordered-float 2.10.0", "picker", @@ -8051,9 +8052,7 @@ dependencies = [ "rpc", "serde", "serde_json", - "settings", "smol", - "theme", "ui", "ui_text_field", "util", diff --git a/assets/keymaps/default-linux.json b/assets/keymaps/default-linux.json index 86649b7990..bee4d63381 100644 --- a/assets/keymaps/default-linux.json +++ b/assets/keymaps/default-linux.json @@ -191,6 +191,12 @@ "ctrl-shift-enter": "editor::NewlineBelow" } }, + { + "context": "Markdown", + "bindings": { + "ctrl-c": "markdown::Copy" + } + }, { "context": "AssistantPanel", "bindings": { diff --git a/assets/keymaps/default-macos.json b/assets/keymaps/default-macos.json index d99f78ffa6..d655a5045e 100644 --- a/assets/keymaps/default-macos.json +++ b/assets/keymaps/default-macos.json @@ -207,6 +207,12 @@ "ctrl-shift-enter": "editor::NewlineBelow" } }, + { + "context": "Markdown", + "bindings": { + "cmd-c": "markdown::Copy" + } + }, { "context": "AssistantPanel", // Used in the assistant crate, which we're replacing "bindings": { diff --git a/crates/assistant2/src/assistant2.rs b/crates/assistant2/src/assistant2.rs index 39e7581988..9ad1b7b99b 100644 --- a/crates/assistant2/src/assistant2.rs +++ b/crates/assistant2/src/assistant2.rs @@ -440,7 +440,7 @@ impl AssistantChat { Markdown::new( text, self.markdown_style.clone(), - self.language_registry.clone(), + Some(self.language_registry.clone()), cx, ) }); @@ -573,7 +573,7 @@ impl AssistantChat { Markdown::new( "".into(), this.markdown_style.clone(), - this.language_registry.clone(), + Some(this.language_registry.clone()), cx, ) }), @@ -667,7 +667,7 @@ impl AssistantChat { Markdown::new( "".into(), self.markdown_style.clone(), - self.language_registry.clone(), + Some(self.language_registry.clone()), cx, ) }), @@ -683,7 +683,7 @@ impl AssistantChat { Markdown::new( "".into(), self.markdown_style.clone(), - self.language_registry.clone(), + Some(self.language_registry.clone()), cx, ) }), diff --git a/crates/gpui/src/elements/text.rs b/crates/gpui/src/elements/text.rs index 2adb2dbf54..b892571e0f 100644 --- a/crates/gpui/src/elements/text.rs +++ b/crates/gpui/src/elements/text.rs @@ -432,6 +432,19 @@ impl TextLayout { pub fn line_height(&self) -> Pixels { self.0.lock().as_ref().unwrap().line_height } + + /// todo!() + pub fn text(&self) -> String { + self.0 + .lock() + .as_ref() + .unwrap() + .lines + .iter() + .map(|s| s.text.to_string()) + .collect::>() + .join("\n") + } } /// A text element that can be interacted with. diff --git a/crates/markdown/examples/markdown.rs b/crates/markdown/examples/markdown.rs index 576637953e..e975ec801a 100644 --- a/crates/markdown/examples/markdown.rs +++ b/crates/markdown/examples/markdown.rs @@ -1,5 +1,5 @@ use assets::Assets; -use gpui::{prelude::*, App, Task, View, WindowOptions}; +use gpui::{prelude::*, App, KeyBinding, Task, View, WindowOptions}; use language::{language_settings::AllLanguageSettings, LanguageRegistry}; use markdown::{Markdown, MarkdownStyle}; use node_runtime::FakeNodeRuntime; @@ -91,6 +91,7 @@ pub fn main() { SettingsStore::update(cx, |store, cx| { store.update_user_settings::(cx, |_| {}); }); + cx.bind_keys([KeyBinding::new("cmd-c", markdown::Copy, None)]); let node_runtime = FakeNodeRuntime::new(); let language_registry = Arc::new(LanguageRegistry::new( @@ -161,7 +162,7 @@ impl MarkdownExample { language_registry: Arc, cx: &mut WindowContext, ) -> Self { - let markdown = cx.new_view(|cx| Markdown::new(text, style, language_registry, cx)); + let markdown = cx.new_view(|cx| Markdown::new(text, style, Some(language_registry), cx)); Self { markdown } } } diff --git a/crates/markdown/src/markdown.rs b/crates/markdown/src/markdown.rs index aaf7811c70..5f0621d60e 100644 --- a/crates/markdown/src/markdown.rs +++ b/crates/markdown/src/markdown.rs @@ -3,10 +3,11 @@ mod parser; use crate::parser::CodeBlockKind; use futures::FutureExt; use gpui::{ - point, quad, AnyElement, AppContext, Bounds, CursorStyle, DispatchPhase, Edges, FocusHandle, - FocusableView, FontStyle, FontWeight, GlobalElementId, Hitbox, Hsla, KeyContext, - MouseDownEvent, MouseEvent, MouseMoveEvent, MouseUpEvent, Point, Render, StrikethroughStyle, - Style, StyledText, Task, TextLayout, TextRun, TextStyle, TextStyleRefinement, View, + actions, point, quad, AnyElement, AppContext, Bounds, ClipboardItem, CursorStyle, + DispatchPhase, Edges, FocusHandle, FocusableView, FontStyle, FontWeight, GlobalElementId, + Hitbox, Hsla, KeyContext, MouseDownEvent, MouseEvent, MouseMoveEvent, MouseUpEvent, Point, + Render, StrikethroughStyle, Style, StyledText, Task, TextLayout, TextRun, TextStyle, + TextStyleRefinement, View, }; use language::{Language, LanguageRegistry, Rope}; use parser::{parse_markdown, MarkdownEvent, MarkdownTag, MarkdownTagEnd}; @@ -37,14 +38,16 @@ pub struct Markdown { should_reparse: bool, pending_parse: Option>>, focus_handle: FocusHandle, - language_registry: Arc, + language_registry: Option>, } +actions!(markdown, [Copy]); + impl Markdown { pub fn new( source: String, style: MarkdownStyle, - language_registry: Arc, + language_registry: Option>, cx: &mut ViewContext, ) -> Self { let focus_handle = cx.focus_handle(); @@ -83,6 +86,11 @@ impl Markdown { &self.source } + fn copy(&self, text: &RenderedText, cx: &mut ViewContext) { + let text = text.text_for_range(self.selection.start..self.selection.end); + cx.write_to_clipboard(ClipboardItem::new(text)); + } + fn parse(&mut self, cx: &mut ViewContext) { if self.source.is_empty() { return; @@ -191,14 +199,14 @@ impl Default for ParsedMarkdown { pub struct MarkdownElement { markdown: View, style: MarkdownStyle, - language_registry: Arc, + language_registry: Option>, } impl MarkdownElement { fn new( markdown: View, style: MarkdownStyle, - language_registry: Arc, + language_registry: Option>, ) -> Self { Self { markdown, @@ -210,6 +218,7 @@ impl MarkdownElement { fn load_language(&self, name: &str, cx: &mut WindowContext) -> Option> { let language = self .language_registry + .as_ref()? .language_for_name(name) .map(|language| language.ok()) .shared(); @@ -322,13 +331,21 @@ impl MarkdownElement { match rendered_text.source_index_for_position(event.position) { Ok(ix) | Err(ix) => ix, }; + let range = if event.click_count == 2 { + rendered_text.surrounding_word_range(source_index) + } else if event.click_count == 3 { + rendered_text.surrounding_line_range(source_index) + } else { + source_index..source_index + }; markdown.selection = Selection { - start: source_index, - end: source_index, + start: range.start, + end: range.end, reversed: false, pending: true, }; cx.focus(&markdown.focus_handle); + cx.prevent_default() } cx.notify(); @@ -378,6 +395,12 @@ impl MarkdownElement { } else { if markdown.selection.pending { markdown.selection.pending = false; + #[cfg(target_os = "linux")] + { + let text = rendered_text + .text_for_range(markdown.selection.start..markdown.selection.end); + cx.write_to_primary(ClipboardItem::new(text)) + } cx.notify(); } } @@ -619,6 +642,16 @@ impl Element for MarkdownElement { let mut context = KeyContext::default(); context.add("Markdown"); cx.set_key_context(context); + let view = self.markdown.clone(); + cx.on_action(std::any::TypeId::of::(), { + let text = rendered_markdown.text.clone(); + move |_, phase, cx| { + let text = text.clone(); + if phase == DispatchPhase::Bubble { + view.update(cx, move |this, cx| this.copy(&text, cx)) + } + } + }); self.paint_mouse_listeners(hitbox, &rendered_markdown.text, cx); rendered_markdown.element.paint(cx); @@ -920,6 +953,77 @@ impl RenderedText { None } + fn surrounding_word_range(&self, source_index: usize) -> Range { + for line in self.lines.iter() { + if source_index > line.source_end { + continue; + } + + let line_rendered_start = line.source_mappings.first().unwrap().rendered_index; + let rendered_index_in_line = + line.rendered_index_for_source_index(source_index) - line_rendered_start; + let text = line.layout.text(); + let previous_space = if let Some(idx) = text[0..rendered_index_in_line].rfind(' ') { + idx + ' '.len_utf8() + } else { + 0 + }; + let next_space = if let Some(idx) = text[rendered_index_in_line..].find(' ') { + rendered_index_in_line + idx + } else { + text.len() + }; + + return line.source_index_for_rendered_index(line_rendered_start + previous_space) + ..line.source_index_for_rendered_index(line_rendered_start + next_space); + } + + source_index..source_index + } + + fn surrounding_line_range(&self, source_index: usize) -> Range { + for line in self.lines.iter() { + if source_index > line.source_end { + continue; + } + let line_source_start = line.source_mappings.first().unwrap().source_index; + return line_source_start..line.source_end; + } + + source_index..source_index + } + + fn text_for_range(&self, range: Range) -> String { + let mut ret = vec![]; + + for line in self.lines.iter() { + if range.start > line.source_end { + continue; + } + let line_source_start = line.source_mappings.first().unwrap().source_index; + if range.end < line_source_start { + break; + } + + let text = line.layout.text(); + + let start = if range.start < line_source_start { + 0 + } else { + line.rendered_index_for_source_index(range.start) + }; + let end = if range.end > line.source_end { + line.rendered_index_for_source_index(line.source_end) + } else { + line.rendered_index_for_source_index(range.end) + } + .min(text.len()); + + ret.push(text[start..end].to_string()); + } + ret.join("\n") + } + fn link_for_position(&self, position: Point) -> Option<&RenderedLink> { let source_index = self.source_index_for_position(position).ok()?; self.links diff --git a/crates/project_panel/src/project_panel.rs b/crates/project_panel/src/project_panel.rs index fe4acccc86..db771b4ce1 100644 --- a/crates/project_panel/src/project_panel.rs +++ b/crates/project_panel/src/project_panel.rs @@ -102,7 +102,7 @@ pub struct EntryDetails { is_processing: bool, is_cut: bool, git_status: Option, - is_dotenv: bool, + is_private: bool, } #[derive(PartialEq, Clone, Default, Debug, Deserialize)] @@ -1592,7 +1592,7 @@ impl ProjectPanel { .clipboard_entry .map_or(false, |e| e.is_cut() && e.entry_id() == entry.id), git_status: status, - is_dotenv: entry.is_private, + is_private: entry.is_private, }; if let Some(edit_state) = &self.edit_state { diff --git a/crates/recent_projects/Cargo.toml b/crates/recent_projects/Cargo.toml index 057aa6d368..c3527329fe 100644 --- a/crates/recent_projects/Cargo.toml +++ b/crates/recent_projects/Cargo.toml @@ -18,15 +18,14 @@ editor.workspace = true feature_flags.workspace = true fuzzy.workspace = true gpui.workspace = true +markdown.workspace = true menu.workspace = true ordered-float.workspace = true picker.workspace = true dev_server_projects.workspace = true rpc.workspace = true serde.workspace = true -settings.workspace = true smol.workspace = true -theme.workspace = true ui.workspace = true ui_text_field.workspace = true util.workspace = true diff --git a/crates/recent_projects/src/dev_servers.rs b/crates/recent_projects/src/dev_servers.rs index 757f6516af..dc3f136d8f 100644 --- a/crates/recent_projects/src/dev_servers.rs +++ b/crates/recent_projects/src/dev_servers.rs @@ -10,12 +10,12 @@ use gpui::{ DismissEvent, EventEmitter, FocusHandle, FocusableView, Model, ScrollHandle, Transformation, View, ViewContext, }; +use markdown::Markdown; +use markdown::MarkdownStyle; use rpc::{ proto::{CreateDevServerResponse, DevServerStatus, RegenerateDevServerTokenResponse}, ErrorCode, ErrorExt, }; -use settings::Settings; -use theme::ThemeSettings; use ui::CheckboxWithLabel; use ui::{prelude::*, Indicator, List, ListHeader, ListItem, ModalContent, ModalHeader, Tooltip}; use ui_text_field::{FieldLabelLayout, TextField}; @@ -33,6 +33,7 @@ pub struct DevServerProjects { dev_server_name_input: View, use_server_name_in_ssh: Selection, rename_dev_server_input: View, + markdown: View, _dev_server_subscription: Subscription, } @@ -113,6 +114,23 @@ impl DevServerProjects { cx.notify(); }); + let markdown_style = MarkdownStyle { + code_block: gpui::TextStyleRefinement { + font_family: Some("Zed Mono".into()), + color: Some(cx.theme().colors().editor_foreground), + background_color: Some(cx.theme().colors().editor_background), + ..Default::default() + }, + inline_code: Default::default(), + block_quote: Default::default(), + link: Default::default(), + rule_color: Default::default(), + block_quote_border_color: Default::default(), + syntax: cx.theme().syntax().clone(), + selection_background_color: cx.theme().players().local().selection, + }; + let markdown = cx.new_view(|cx| Markdown::new("".to_string(), markdown_style, None, cx)); + Self { mode: Mode::Default(None), focus_handle, @@ -121,6 +139,7 @@ impl DevServerProjects { project_path_input, dev_server_name_input, rename_dev_server_input, + markdown, use_server_name_in_ssh: Selection::Unselected, _dev_server_subscription: subscription, } @@ -726,7 +745,7 @@ impl DevServerProjects { .child( CheckboxWithLabel::new( "use-server-name-in-ssh", - Label::new("Use name as ssh connection string"), + Label::new("Use SSH for terminals"), self.use_server_name_in_ssh, |&_, _| {} ) @@ -748,7 +767,7 @@ impl DevServerProjects { }; div.px_2().child(Label::new(format!( "Once you have created a dev server, you will be given a command to run on the server to register it.\n\n\ - Ssh connection string enables remote terminals, which runs `ssh {ssh_host_name}` when creating terminal tabs." + If you enable SSH, then the terminal will automatically `ssh {ssh_host_name}` on open." ))) }) .when_some(dev_server.clone(), |div, dev_server| { @@ -758,7 +777,7 @@ impl DevServerProjects { .dev_server_status(DevServerId(dev_server.dev_server_id)); div.child( - Self::render_dev_server_token_instructions(&dev_server.access_token, &dev_server.name, status, cx) + self.render_dev_server_token_instructions(&dev_server.access_token, &dev_server.name, status, cx) ) }), ) @@ -766,12 +785,18 @@ impl DevServerProjects { } fn render_dev_server_token_instructions( + &self, access_token: &str, dev_server_name: &str, status: DevServerStatus, cx: &mut ViewContext, ) -> Div { let instructions = SharedString::from(format!("zed --dev-server-token {}", access_token)); + self.markdown.update(cx, |markdown, cx| { + if !markdown.source().contains(access_token) { + markdown.reset(format!("```\n{}\n```", instructions), cx); + } + }); v_flex() .pl_2() @@ -799,19 +824,7 @@ impl DevServerProjects { }), ), ) - .child( - v_flex() - .w_full() - .bg(cx.theme().colors().title_bar_background) // todo: this should be distinct - .border_1() - .border_color(cx.theme().colors().border_variant) - .rounded_md() - .my_1() - .py_0p5() - .px_3() - .font_family(ThemeSettings::get_global(cx).buffer_font.family.clone()) - .child(Label::new(instructions)), - ) + .child(v_flex().w_full().child(self.markdown.clone())) .when(status == DevServerStatus::Offline, |this| { this.child(Self::render_loading_spinner("Waiting for connection…")) }) @@ -926,14 +939,13 @@ impl DevServerProjects { EditDevServerState::RegeneratingToken => { Self::render_loading_spinner("Generating token...") } - EditDevServerState::RegeneratedToken(response) => { - Self::render_dev_server_token_instructions( + EditDevServerState::RegeneratedToken(response) => self + .render_dev_server_token_instructions( &response.access_token, &dev_server_name, dev_server_status, cx, - ) - } + ), _ => h_flex().items_end().w_full().child( Button::new("regenerate-dev-server-token", "Generate new access token") .icon(IconName::Update)