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",
"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",

View file

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

View file

@ -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": {

View file

@ -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,
)
}),

View file

@ -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::<Vec<_>>()
.join("\n")
}
}
/// A text element that can be interacted with.

View file

@ -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::<AllLanguageSettings>(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<LanguageRegistry>,
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 }
}
}

View file

@ -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<Task<Option<()>>>,
focus_handle: FocusHandle,
language_registry: Arc<LanguageRegistry>,
language_registry: Option<Arc<LanguageRegistry>>,
}
actions!(markdown, [Copy]);
impl Markdown {
pub fn new(
source: String,
style: MarkdownStyle,
language_registry: Arc<LanguageRegistry>,
language_registry: Option<Arc<LanguageRegistry>>,
cx: &mut ViewContext<Self>,
) -> Self {
let focus_handle = cx.focus_handle();
@ -83,6 +86,11 @@ impl Markdown {
&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>) {
if self.source.is_empty() {
return;
@ -191,14 +199,14 @@ impl Default for ParsedMarkdown {
pub struct MarkdownElement {
markdown: View<Markdown>,
style: MarkdownStyle,
language_registry: Arc<LanguageRegistry>,
language_registry: Option<Arc<LanguageRegistry>>,
}
impl MarkdownElement {
fn new(
markdown: View<Markdown>,
style: MarkdownStyle,
language_registry: Arc<LanguageRegistry>,
language_registry: Option<Arc<LanguageRegistry>>,
) -> Self {
Self {
markdown,
@ -210,6 +218,7 @@ impl MarkdownElement {
fn load_language(&self, name: &str, cx: &mut WindowContext) -> Option<Arc<Language>> {
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::<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);
rendered_markdown.element.paint(cx);
@ -920,6 +953,77 @@ impl RenderedText {
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> {
let source_index = self.source_index_for_position(position).ok()?;
self.links

View file

@ -102,7 +102,7 @@ pub struct EntryDetails {
is_processing: bool,
is_cut: bool,
git_status: Option<GitFileStatus>,
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 {

View file

@ -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

View file

@ -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<TextField>,
use_server_name_in_ssh: Selection,
rename_dev_server_input: View<TextField>,
markdown: View<Markdown>,
_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<Self>,
) -> 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)