Allow copy-pasting dev-server-token (#11992)

Release Notes:

- N/A
This commit is contained in:
Conrad Irwin 2024-05-17 16:41:46 -06:00 committed by GitHub
parent 84affa96ff
commit 1f611a9c90
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 184 additions and 44 deletions

3
Cargo.lock generated
View file

@ -8044,6 +8044,7 @@ dependencies = [
"fuzzy", "fuzzy",
"gpui", "gpui",
"language", "language",
"markdown",
"menu", "menu",
"ordered-float 2.10.0", "ordered-float 2.10.0",
"picker", "picker",
@ -8051,9 +8052,7 @@ dependencies = [
"rpc", "rpc",
"serde", "serde",
"serde_json", "serde_json",
"settings",
"smol", "smol",
"theme",
"ui", "ui",
"ui_text_field", "ui_text_field",
"util", "util",

View file

@ -191,6 +191,12 @@
"ctrl-shift-enter": "editor::NewlineBelow" "ctrl-shift-enter": "editor::NewlineBelow"
} }
}, },
{
"context": "Markdown",
"bindings": {
"ctrl-c": "markdown::Copy"
}
},
{ {
"context": "AssistantPanel", "context": "AssistantPanel",
"bindings": { "bindings": {

View file

@ -207,6 +207,12 @@
"ctrl-shift-enter": "editor::NewlineBelow" "ctrl-shift-enter": "editor::NewlineBelow"
} }
}, },
{
"context": "Markdown",
"bindings": {
"cmd-c": "markdown::Copy"
}
},
{ {
"context": "AssistantPanel", // Used in the assistant crate, which we're replacing "context": "AssistantPanel", // Used in the assistant crate, which we're replacing
"bindings": { "bindings": {

View file

@ -440,7 +440,7 @@ impl AssistantChat {
Markdown::new( Markdown::new(
text, text,
self.markdown_style.clone(), self.markdown_style.clone(),
self.language_registry.clone(), Some(self.language_registry.clone()),
cx, cx,
) )
}); });
@ -573,7 +573,7 @@ impl AssistantChat {
Markdown::new( Markdown::new(
"".into(), "".into(),
this.markdown_style.clone(), this.markdown_style.clone(),
this.language_registry.clone(), Some(this.language_registry.clone()),
cx, cx,
) )
}), }),
@ -667,7 +667,7 @@ impl AssistantChat {
Markdown::new( Markdown::new(
"".into(), "".into(),
self.markdown_style.clone(), self.markdown_style.clone(),
self.language_registry.clone(), Some(self.language_registry.clone()),
cx, cx,
) )
}), }),
@ -683,7 +683,7 @@ impl AssistantChat {
Markdown::new( Markdown::new(
"".into(), "".into(),
self.markdown_style.clone(), self.markdown_style.clone(),
self.language_registry.clone(), Some(self.language_registry.clone()),
cx, cx,
) )
}), }),

View file

@ -432,6 +432,19 @@ impl TextLayout {
pub fn line_height(&self) -> Pixels { pub fn line_height(&self) -> Pixels {
self.0.lock().as_ref().unwrap().line_height 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::<Vec<_>>()
.join("\n")
}
} }
/// A text element that can be interacted with. /// A text element that can be interacted with.

View file

@ -1,5 +1,5 @@
use assets::Assets; 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 language::{language_settings::AllLanguageSettings, LanguageRegistry};
use markdown::{Markdown, MarkdownStyle}; use markdown::{Markdown, MarkdownStyle};
use node_runtime::FakeNodeRuntime; use node_runtime::FakeNodeRuntime;
@ -91,6 +91,7 @@ pub fn main() {
SettingsStore::update(cx, |store, cx| { SettingsStore::update(cx, |store, cx| {
store.update_user_settings::<AllLanguageSettings>(cx, |_| {}); store.update_user_settings::<AllLanguageSettings>(cx, |_| {});
}); });
cx.bind_keys([KeyBinding::new("cmd-c", markdown::Copy, None)]);
let node_runtime = FakeNodeRuntime::new(); let node_runtime = FakeNodeRuntime::new();
let language_registry = Arc::new(LanguageRegistry::new( let language_registry = Arc::new(LanguageRegistry::new(
@ -161,7 +162,7 @@ impl MarkdownExample {
language_registry: Arc<LanguageRegistry>, language_registry: Arc<LanguageRegistry>,
cx: &mut WindowContext, cx: &mut WindowContext,
) -> Self { ) -> 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 } Self { markdown }
} }
} }

View file

@ -3,10 +3,11 @@ mod parser;
use crate::parser::CodeBlockKind; use crate::parser::CodeBlockKind;
use futures::FutureExt; use futures::FutureExt;
use gpui::{ use gpui::{
point, quad, AnyElement, AppContext, Bounds, CursorStyle, DispatchPhase, Edges, FocusHandle, actions, point, quad, AnyElement, AppContext, Bounds, ClipboardItem, CursorStyle,
FocusableView, FontStyle, FontWeight, GlobalElementId, Hitbox, Hsla, KeyContext, DispatchPhase, Edges, FocusHandle, FocusableView, FontStyle, FontWeight, GlobalElementId,
MouseDownEvent, MouseEvent, MouseMoveEvent, MouseUpEvent, Point, Render, StrikethroughStyle, Hitbox, Hsla, KeyContext, MouseDownEvent, MouseEvent, MouseMoveEvent, MouseUpEvent, Point,
Style, StyledText, Task, TextLayout, TextRun, TextStyle, TextStyleRefinement, View, Render, StrikethroughStyle, Style, StyledText, Task, TextLayout, TextRun, TextStyle,
TextStyleRefinement, View,
}; };
use language::{Language, LanguageRegistry, Rope}; use language::{Language, LanguageRegistry, Rope};
use parser::{parse_markdown, MarkdownEvent, MarkdownTag, MarkdownTagEnd}; use parser::{parse_markdown, MarkdownEvent, MarkdownTag, MarkdownTagEnd};
@ -37,14 +38,16 @@ pub struct Markdown {
should_reparse: bool, should_reparse: bool,
pending_parse: Option<Task<Option<()>>>, pending_parse: Option<Task<Option<()>>>,
focus_handle: FocusHandle, focus_handle: FocusHandle,
language_registry: Arc<LanguageRegistry>, language_registry: Option<Arc<LanguageRegistry>>,
} }
actions!(markdown, [Copy]);
impl Markdown { impl Markdown {
pub fn new( pub fn new(
source: String, source: String,
style: MarkdownStyle, style: MarkdownStyle,
language_registry: Arc<LanguageRegistry>, language_registry: Option<Arc<LanguageRegistry>>,
cx: &mut ViewContext<Self>, cx: &mut ViewContext<Self>,
) -> Self { ) -> Self {
let focus_handle = cx.focus_handle(); let focus_handle = cx.focus_handle();
@ -83,6 +86,11 @@ impl Markdown {
&self.source &self.source
} }
fn copy(&self, text: &RenderedText, cx: &mut ViewContext<Self>) {
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<Self>) { fn parse(&mut self, cx: &mut ViewContext<Self>) {
if self.source.is_empty() { if self.source.is_empty() {
return; return;
@ -191,14 +199,14 @@ impl Default for ParsedMarkdown {
pub struct MarkdownElement { pub struct MarkdownElement {
markdown: View<Markdown>, markdown: View<Markdown>,
style: MarkdownStyle, style: MarkdownStyle,
language_registry: Arc<LanguageRegistry>, language_registry: Option<Arc<LanguageRegistry>>,
} }
impl MarkdownElement { impl MarkdownElement {
fn new( fn new(
markdown: View<Markdown>, markdown: View<Markdown>,
style: MarkdownStyle, style: MarkdownStyle,
language_registry: Arc<LanguageRegistry>, language_registry: Option<Arc<LanguageRegistry>>,
) -> Self { ) -> Self {
Self { Self {
markdown, markdown,
@ -210,6 +218,7 @@ impl MarkdownElement {
fn load_language(&self, name: &str, cx: &mut WindowContext) -> Option<Arc<Language>> { fn load_language(&self, name: &str, cx: &mut WindowContext) -> Option<Arc<Language>> {
let language = self let language = self
.language_registry .language_registry
.as_ref()?
.language_for_name(name) .language_for_name(name)
.map(|language| language.ok()) .map(|language| language.ok())
.shared(); .shared();
@ -322,13 +331,21 @@ impl MarkdownElement {
match rendered_text.source_index_for_position(event.position) { match rendered_text.source_index_for_position(event.position) {
Ok(ix) | Err(ix) => ix, 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 { markdown.selection = Selection {
start: source_index, start: range.start,
end: source_index, end: range.end,
reversed: false, reversed: false,
pending: true, pending: true,
}; };
cx.focus(&markdown.focus_handle); cx.focus(&markdown.focus_handle);
cx.prevent_default()
} }
cx.notify(); cx.notify();
@ -378,6 +395,12 @@ impl MarkdownElement {
} else { } else {
if markdown.selection.pending { if markdown.selection.pending {
markdown.selection.pending = false; 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(); cx.notify();
} }
} }
@ -619,6 +642,16 @@ impl Element for MarkdownElement {
let mut context = KeyContext::default(); let mut context = KeyContext::default();
context.add("Markdown"); context.add("Markdown");
cx.set_key_context(context); cx.set_key_context(context);
let view = self.markdown.clone();
cx.on_action(std::any::TypeId::of::<crate::Copy>(), {
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); self.paint_mouse_listeners(hitbox, &rendered_markdown.text, cx);
rendered_markdown.element.paint(cx); rendered_markdown.element.paint(cx);
@ -920,6 +953,77 @@ impl RenderedText {
None None
} }
fn surrounding_word_range(&self, source_index: usize) -> Range<usize> {
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<usize> {
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<usize>) -> 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<Pixels>) -> Option<&RenderedLink> { fn link_for_position(&self, position: Point<Pixels>) -> Option<&RenderedLink> {
let source_index = self.source_index_for_position(position).ok()?; let source_index = self.source_index_for_position(position).ok()?;
self.links self.links

View file

@ -102,7 +102,7 @@ pub struct EntryDetails {
is_processing: bool, is_processing: bool,
is_cut: bool, is_cut: bool,
git_status: Option<GitFileStatus>, git_status: Option<GitFileStatus>,
is_dotenv: bool, is_private: bool,
} }
#[derive(PartialEq, Clone, Default, Debug, Deserialize)] #[derive(PartialEq, Clone, Default, Debug, Deserialize)]
@ -1592,7 +1592,7 @@ impl ProjectPanel {
.clipboard_entry .clipboard_entry
.map_or(false, |e| e.is_cut() && e.entry_id() == entry.id), .map_or(false, |e| e.is_cut() && e.entry_id() == entry.id),
git_status: status, git_status: status,
is_dotenv: entry.is_private, is_private: entry.is_private,
}; };
if let Some(edit_state) = &self.edit_state { if let Some(edit_state) = &self.edit_state {

View file

@ -18,15 +18,14 @@ editor.workspace = true
feature_flags.workspace = true feature_flags.workspace = true
fuzzy.workspace = true fuzzy.workspace = true
gpui.workspace = true gpui.workspace = true
markdown.workspace = true
menu.workspace = true menu.workspace = true
ordered-float.workspace = true ordered-float.workspace = true
picker.workspace = true picker.workspace = true
dev_server_projects.workspace = true dev_server_projects.workspace = true
rpc.workspace = true rpc.workspace = true
serde.workspace = true serde.workspace = true
settings.workspace = true
smol.workspace = true smol.workspace = true
theme.workspace = true
ui.workspace = true ui.workspace = true
ui_text_field.workspace = true ui_text_field.workspace = true
util.workspace = true util.workspace = true

View file

@ -10,12 +10,12 @@ use gpui::{
DismissEvent, EventEmitter, FocusHandle, FocusableView, Model, ScrollHandle, Transformation, DismissEvent, EventEmitter, FocusHandle, FocusableView, Model, ScrollHandle, Transformation,
View, ViewContext, View, ViewContext,
}; };
use markdown::Markdown;
use markdown::MarkdownStyle;
use rpc::{ use rpc::{
proto::{CreateDevServerResponse, DevServerStatus, RegenerateDevServerTokenResponse}, proto::{CreateDevServerResponse, DevServerStatus, RegenerateDevServerTokenResponse},
ErrorCode, ErrorExt, ErrorCode, ErrorExt,
}; };
use settings::Settings;
use theme::ThemeSettings;
use ui::CheckboxWithLabel; use ui::CheckboxWithLabel;
use ui::{prelude::*, Indicator, List, ListHeader, ListItem, ModalContent, ModalHeader, Tooltip}; use ui::{prelude::*, Indicator, List, ListHeader, ListItem, ModalContent, ModalHeader, Tooltip};
use ui_text_field::{FieldLabelLayout, TextField}; use ui_text_field::{FieldLabelLayout, TextField};
@ -33,6 +33,7 @@ pub struct DevServerProjects {
dev_server_name_input: View<TextField>, dev_server_name_input: View<TextField>,
use_server_name_in_ssh: Selection, use_server_name_in_ssh: Selection,
rename_dev_server_input: View<TextField>, rename_dev_server_input: View<TextField>,
markdown: View<Markdown>,
_dev_server_subscription: Subscription, _dev_server_subscription: Subscription,
} }
@ -113,6 +114,23 @@ impl DevServerProjects {
cx.notify(); 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 { Self {
mode: Mode::Default(None), mode: Mode::Default(None),
focus_handle, focus_handle,
@ -121,6 +139,7 @@ impl DevServerProjects {
project_path_input, project_path_input,
dev_server_name_input, dev_server_name_input,
rename_dev_server_input, rename_dev_server_input,
markdown,
use_server_name_in_ssh: Selection::Unselected, use_server_name_in_ssh: Selection::Unselected,
_dev_server_subscription: subscription, _dev_server_subscription: subscription,
} }
@ -726,7 +745,7 @@ impl DevServerProjects {
.child( .child(
CheckboxWithLabel::new( CheckboxWithLabel::new(
"use-server-name-in-ssh", "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, self.use_server_name_in_ssh,
|&_, _| {} |&_, _| {}
) )
@ -748,7 +767,7 @@ impl DevServerProjects {
}; };
div.px_2().child(Label::new(format!( 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\ "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| { .when_some(dev_server.clone(), |div, dev_server| {
@ -758,7 +777,7 @@ impl DevServerProjects {
.dev_server_status(DevServerId(dev_server.dev_server_id)); .dev_server_status(DevServerId(dev_server.dev_server_id));
div.child( 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( fn render_dev_server_token_instructions(
&self,
access_token: &str, access_token: &str,
dev_server_name: &str, dev_server_name: &str,
status: DevServerStatus, status: DevServerStatus,
cx: &mut ViewContext<Self>, cx: &mut ViewContext<Self>,
) -> Div { ) -> Div {
let instructions = SharedString::from(format!("zed --dev-server-token {}", access_token)); 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() v_flex()
.pl_2() .pl_2()
@ -799,19 +824,7 @@ impl DevServerProjects {
}), }),
), ),
) )
.child( .child(v_flex().w_full().child(self.markdown.clone()))
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)),
)
.when(status == DevServerStatus::Offline, |this| { .when(status == DevServerStatus::Offline, |this| {
this.child(Self::render_loading_spinner("Waiting for connection…")) this.child(Self::render_loading_spinner("Waiting for connection…"))
}) })
@ -926,14 +939,13 @@ impl DevServerProjects {
EditDevServerState::RegeneratingToken => { EditDevServerState::RegeneratingToken => {
Self::render_loading_spinner("Generating token...") Self::render_loading_spinner("Generating token...")
} }
EditDevServerState::RegeneratedToken(response) => { EditDevServerState::RegeneratedToken(response) => self
Self::render_dev_server_token_instructions( .render_dev_server_token_instructions(
&response.access_token, &response.access_token,
&dev_server_name, &dev_server_name,
dev_server_status, dev_server_status,
cx, cx,
) ),
}
_ => h_flex().items_end().w_full().child( _ => h_flex().items_end().w_full().child(
Button::new("regenerate-dev-server-token", "Generate new access token") Button::new("regenerate-dev-server-token", "Generate new access token")
.icon(IconName::Update) .icon(IconName::Update)