Detailed changes
@@ -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",
@@ -191,6 +191,12 @@
"ctrl-shift-enter": "editor::NewlineBelow"
}
},
+ {
+ "context": "Markdown",
+ "bindings": {
+ "ctrl-c": "markdown::Copy"
+ }
+ },
{
"context": "AssistantPanel",
"bindings": {
@@ -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": {
@@ -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,
)
}),
@@ -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.
@@ -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 }
}
}
@@ -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
@@ -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 {
@@ -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
@@ -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)