Cargo.lock 🔗
@@ -7652,7 +7652,6 @@ dependencies = [
"anyhow",
"assets",
"env_logger 0.11.6",
- "futures 0.3.31",
"gpui",
"language",
"languages",
Conrad Irwin created
Closes #18641
Contributes: #13194
Release Notes:
- Open LSP documentation file links in Zed not the system opener
- Render completion documentation markdown consistently with
documentation markdown
Cargo.lock | 1
crates/assistant2/src/active_thread.rs | 2
crates/assistant_context_editor/src/slash_command.rs | 6
crates/editor/src/code_context_menus.rs | 79 ++++-
crates/editor/src/editor.rs | 33 +-
crates/editor/src/element.rs | 16
crates/editor/src/hover_popover.rs | 169 +++++++++----
crates/language/src/buffer.rs | 45 ---
crates/markdown/Cargo.toml | 3
crates/markdown/examples/markdown.rs | 90 +------
crates/markdown/src/markdown.rs | 137 ++++------
crates/markdown/src/parser.rs | 12
crates/project/src/lsp_store.rs | 93 ++++---
crates/project/src/project.rs | 6
crates/recent_projects/src/ssh_connections.rs | 2
crates/zed/src/zed/linux_prompts.rs | 11
16 files changed, 353 insertions(+), 352 deletions(-)
@@ -7652,7 +7652,6 @@ dependencies = [
"anyhow",
"assets",
"env_logger 0.11.6",
- "futures 0.3.31",
"gpui",
"language",
"languages",
@@ -179,7 +179,7 @@ impl ActiveThread {
let markdown = cx.new(|cx| {
Markdown::new(
- text,
+ text.into(),
markdown_style,
Some(self.language_registry.clone()),
None,
@@ -5,9 +5,9 @@ use assistant_slash_command::{AfterCompletion, SlashCommandLine, SlashCommandWor
use editor::{CompletionProvider, Editor};
use fuzzy::{match_strings, StringMatchCandidate};
use gpui::{App, AppContext as _, Context, Entity, Task, WeakEntity, Window};
-use language::{Anchor, Buffer, CompletionDocumentation, LanguageServerId, ToPoint};
+use language::{Anchor, Buffer, LanguageServerId, ToPoint};
use parking_lot::Mutex;
-use project::CompletionIntent;
+use project::{lsp_store::CompletionDocumentation, CompletionIntent};
use rope::Point;
use std::{
cell::RefCell,
@@ -121,7 +121,7 @@ impl SlashCommandCompletionProvider {
Some(project::Completion {
old_range: name_range.clone(),
documentation: Some(CompletionDocumentation::SingleLine(
- command.description(),
+ command.description().into(),
)),
new_text,
label: command.label(cx),
@@ -1,14 +1,16 @@
use fuzzy::{StringMatch, StringMatchCandidate};
use gpui::{
- div, px, uniform_list, AnyElement, BackgroundExecutor, Div, Entity, FontWeight,
+ div, px, uniform_list, AnyElement, BackgroundExecutor, Div, Entity, Focusable, FontWeight,
ListSizingBehavior, ScrollStrategy, SharedString, Size, StrikethroughStyle, StyledText,
- UniformListScrollHandle, WeakEntity,
+ UniformListScrollHandle,
};
use language::Buffer;
-use language::{CodeLabel, CompletionDocumentation};
+use language::CodeLabel;
use lsp::LanguageServerId;
+use markdown::Markdown;
use multi_buffer::{Anchor, ExcerptId};
use ordered_float::OrderedFloat;
+use project::lsp_store::CompletionDocumentation;
use project::{CodeAction, Completion, TaskSourceKind};
use std::{
@@ -21,12 +23,12 @@ use std::{
use task::ResolvedTask;
use ui::{prelude::*, Color, IntoElement, ListItem, Pixels, Popover, Styled};
use util::ResultExt;
-use workspace::Workspace;
+use crate::hover_popover::{hover_markdown_style, open_markdown_url};
use crate::{
actions::{ConfirmCodeAction, ConfirmCompletion},
- render_parsed_markdown, split_words, styled_runs_for_code_label, CodeActionProvider,
- CompletionId, CompletionProvider, DisplayRow, Editor, EditorStyle, ResolvedTasks,
+ split_words, styled_runs_for_code_label, CodeActionProvider, CompletionId, CompletionProvider,
+ DisplayRow, Editor, EditorStyle, ResolvedTasks,
};
pub const MENU_GAP: Pixels = px(4.);
@@ -137,17 +139,27 @@ impl CodeContextMenu {
}
pub fn render_aside(
- &self,
- style: &EditorStyle,
+ &mut self,
+ editor: &Editor,
max_size: Size<Pixels>,
- workspace: Option<WeakEntity<Workspace>>,
+ window: &mut Window,
cx: &mut Context<Editor>,
) -> Option<AnyElement> {
match self {
- CodeContextMenu::Completions(menu) => menu.render_aside(style, max_size, workspace, cx),
+ CodeContextMenu::Completions(menu) => menu.render_aside(editor, max_size, window, cx),
CodeContextMenu::CodeActions(_) => None,
}
}
+
+ pub fn focused(&self, window: &mut Window, cx: &mut Context<Editor>) -> bool {
+ match self {
+ CodeContextMenu::Completions(completions_menu) => completions_menu
+ .markdown_element
+ .as_ref()
+ .is_some_and(|markdown| markdown.focus_handle(cx).contains_focused(window, cx)),
+ CodeContextMenu::CodeActions(_) => false,
+ }
+ }
}
pub enum ContextMenuOrigin {
@@ -169,6 +181,7 @@ pub struct CompletionsMenu {
resolve_completions: bool,
show_completion_documentation: bool,
last_rendered_range: Rc<RefCell<Option<Range<usize>>>>,
+ markdown_element: Option<Entity<Markdown>>,
}
impl CompletionsMenu {
@@ -199,6 +212,7 @@ impl CompletionsMenu {
scroll_handle: UniformListScrollHandle::new(),
resolve_completions: true,
last_rendered_range: RefCell::new(None).into(),
+ markdown_element: None,
}
}
@@ -255,6 +269,7 @@ impl CompletionsMenu {
resolve_completions: false,
show_completion_documentation: false,
last_rendered_range: RefCell::new(None).into(),
+ markdown_element: None,
}
}
@@ -556,10 +571,10 @@ impl CompletionsMenu {
}
fn render_aside(
- &self,
- style: &EditorStyle,
+ &mut self,
+ editor: &Editor,
max_size: Size<Pixels>,
- workspace: Option<WeakEntity<Workspace>>,
+ window: &mut Window,
cx: &mut Context<Editor>,
) -> Option<AnyElement> {
if !self.show_completion_documentation {
@@ -571,17 +586,35 @@ impl CompletionsMenu {
.documentation
.as_ref()?
{
- CompletionDocumentation::MultiLinePlainText(text) => {
- div().child(SharedString::from(text.clone()))
+ CompletionDocumentation::MultiLinePlainText(text) => div().child(text.clone()),
+ CompletionDocumentation::MultiLineMarkdown(parsed) if !parsed.is_empty() => {
+ let markdown = self.markdown_element.get_or_insert_with(|| {
+ cx.new(|cx| {
+ let languages = editor
+ .workspace
+ .as_ref()
+ .and_then(|(workspace, _)| workspace.upgrade())
+ .map(|workspace| workspace.read(cx).app_state().languages.clone());
+ let language = editor
+ .language_at(self.initial_position, cx)
+ .map(|l| l.name().to_proto());
+ Markdown::new(
+ SharedString::default(),
+ hover_markdown_style(window, cx),
+ languages,
+ language,
+ window,
+ cx,
+ )
+ .copy_code_block_buttons(false)
+ .open_url(open_markdown_url)
+ })
+ });
+ markdown.update(cx, |markdown, cx| {
+ markdown.reset(parsed.clone(), window, cx);
+ });
+ div().child(markdown.clone())
}
- CompletionDocumentation::MultiLineMarkdown(parsed) if !parsed.text.is_empty() => div()
- .child(render_parsed_markdown(
- "completions_markdown",
- parsed,
- &style,
- workspace,
- cx,
- )),
CompletionDocumentation::MultiLineMarkdown(_) => return None,
CompletionDocumentation::SingleLine(_) => return None,
CompletionDocumentation::Undocumented => return None,
@@ -99,9 +99,9 @@ use itertools::Itertools;
use language::{
language_settings::{self, all_language_settings, language_settings, InlayHintSettings},
markdown, point_from_lsp, AutoindentMode, BracketPair, Buffer, Capability, CharKind, CodeLabel,
- CompletionDocumentation, CursorShape, Diagnostic, DiskState, EditPredictionsMode, EditPreview,
- HighlightedText, IndentKind, IndentSize, Language, OffsetRangeExt, Point, Selection,
- SelectionGoal, TextObject, TransactionId, TreeSitterOptions,
+ CursorShape, Diagnostic, DiskState, EditPredictionsMode, EditPreview, HighlightedText,
+ IndentKind, IndentSize, Language, OffsetRangeExt, Point, Selection, SelectionGoal, TextObject,
+ TransactionId, TreeSitterOptions,
};
use language::{point_to_lsp, BufferRow, CharClassifier, Runnable, RunnableRange};
use linked_editing_ranges::refresh_linked_ranges;
@@ -132,7 +132,7 @@ use multi_buffer::{
ToOffsetUtf16,
};
use project::{
- lsp_store::{FormatTrigger, LspFormatTarget, OpenLspBufferHandle},
+ lsp_store::{CompletionDocumentation, FormatTrigger, LspFormatTarget, OpenLspBufferHandle},
project_settings::{GitGutterSetting, ProjectSettings},
CodeAction, Completion, CompletionIntent, DocumentHighlight, InlayHint, Location, LocationLink,
PrepareRenameResponse, Project, ProjectItem, ProjectTransaction, TaskSourceKind,
@@ -6221,19 +6221,14 @@ impl Editor {
}
fn render_context_menu_aside(
- &self,
- style: &EditorStyle,
+ &mut self,
max_size: Size<Pixels>,
+ window: &mut Window,
cx: &mut Context<Editor>,
) -> Option<AnyElement> {
- self.context_menu.borrow().as_ref().and_then(|menu| {
+ self.context_menu.borrow_mut().as_mut().and_then(|menu| {
if menu.visible() {
- menu.render_aside(
- style,
- max_size,
- self.workspace.as_ref().map(|(w, _)| w.clone()),
- cx,
- )
+ menu.render_aside(self, max_size, window, cx)
} else {
None
}
@@ -14926,8 +14921,14 @@ impl Editor {
if !self.hover_state.focused(window, cx) {
hide_hover(self, cx);
}
-
- self.hide_context_menu(window, cx);
+ if !self
+ .context_menu
+ .borrow()
+ .as_ref()
+ .is_some_and(|context_menu| context_menu.focused(window, cx))
+ {
+ self.hide_context_menu(window, cx);
+ }
self.discard_inline_completion(false, cx);
cx.emit(EditorEvent::Blurred);
cx.notify();
@@ -15674,7 +15675,7 @@ fn snippet_completions(
documentation: snippet
.description
.clone()
- .map(CompletionDocumentation::SingleLine),
+ .map(|description| CompletionDocumentation::SingleLine(description.into())),
lsp_completion: lsp::CompletionItem {
label: snippet.prefix.first().unwrap().clone(),
kind: Some(CompletionItemKind::SNIPPET),
@@ -3426,9 +3426,11 @@ impl EditorElement {
available_within_viewport.right - px(1.),
MENU_ASIDE_MAX_WIDTH,
);
- let Some(mut aside) =
- self.render_context_menu_aside(size(max_width, max_height - POPOVER_Y_PADDING), cx)
- else {
+ let Some(mut aside) = self.render_context_menu_aside(
+ size(max_width, max_height - POPOVER_Y_PADDING),
+ window,
+ cx,
+ ) else {
return;
};
aside.layout_as_root(AvailableSpace::min_size(), window, cx);
@@ -3450,7 +3452,7 @@ impl EditorElement {
),
) - POPOVER_Y_PADDING,
);
- let Some(mut aside) = self.render_context_menu_aside(max_size, cx) else {
+ let Some(mut aside) = self.render_context_menu_aside(max_size, window, cx) else {
return;
};
let actual_size = aside.layout_as_root(AvailableSpace::min_size(), window, cx);
@@ -3491,7 +3493,7 @@ impl EditorElement {
// Skip drawing if it doesn't fit anywhere.
if let Some((aside, position)) = positioned_aside {
- window.defer_draw(aside, position, 1);
+ window.defer_draw(aside, position, 2);
}
}
@@ -3512,14 +3514,14 @@ impl EditorElement {
fn render_context_menu_aside(
&self,
max_size: Size<Pixels>,
-
+ window: &mut Window,
cx: &mut App,
) -> Option<AnyElement> {
if max_size.width < px(100.) || max_size.height < px(12.) {
None
} else {
self.editor.update(cx, |editor, cx| {
- editor.render_context_menu_aside(&self.style, max_size, cx)
+ editor.render_context_menu_aside(max_size, window, cx)
})
}
}
@@ -1,7 +1,7 @@
use crate::{
display_map::{invisibles::is_invisible, InlayOffset, ToDisplayPoint},
hover_links::{InlayHighlight, RangeInEditor},
- scroll::ScrollAmount,
+ scroll::{Autoscroll, ScrollAmount},
Anchor, AnchorRangeExt, DisplayPoint, DisplayRow, Editor, EditorSettings, EditorSnapshot,
Hover,
};
@@ -18,12 +18,14 @@ use markdown::{Markdown, MarkdownStyle};
use multi_buffer::ToOffset;
use project::{HoverBlock, HoverBlockKind, InlayHintLabelPart};
use settings::Settings;
-use std::rc::Rc;
use std::{borrow::Cow, cell::RefCell};
use std::{ops::Range, sync::Arc, time::Duration};
+use std::{path::PathBuf, rc::Rc};
use theme::ThemeSettings;
use ui::{prelude::*, theme_is_transparent, Scrollbar, ScrollbarState};
+use url::Url;
use util::TryFutureExt;
+use workspace::Workspace;
pub const HOVER_REQUEST_DELAY_MILLIS: u64 = 200;
pub const MIN_POPOVER_CHARACTER_WIDTH: f32 = 20.;
@@ -356,7 +358,15 @@ fn show_hover(
},
..Default::default()
};
- Markdown::new_text(text, markdown_style.clone(), None, None, window, cx)
+ Markdown::new_text(
+ SharedString::new(text),
+ markdown_style.clone(),
+ None,
+ None,
+ window,
+ cx,
+ )
+ .open_url(open_markdown_url)
})
.ok();
@@ -558,69 +568,122 @@ async fn parse_blocks(
let rendered_block = cx
.new_window_entity(|window, cx| {
- let settings = ThemeSettings::get_global(cx);
- let ui_font_family = settings.ui_font.family.clone();
- let ui_font_fallbacks = settings.ui_font.fallbacks.clone();
- let buffer_font_family = settings.buffer_font.family.clone();
- let buffer_font_fallbacks = settings.buffer_font.fallbacks.clone();
-
- let mut base_text_style = window.text_style();
- base_text_style.refine(&TextStyleRefinement {
- font_family: Some(ui_font_family.clone()),
- font_fallbacks: ui_font_fallbacks,
- color: Some(cx.theme().colors().editor_foreground),
- ..Default::default()
- });
-
- let markdown_style = MarkdownStyle {
- base_text_style,
- code_block: StyleRefinement::default().my(rems(1.)).font_buffer(cx),
- inline_code: TextStyleRefinement {
- background_color: Some(cx.theme().colors().background),
- font_family: Some(buffer_font_family),
- font_fallbacks: buffer_font_fallbacks,
- ..Default::default()
- },
- rule_color: cx.theme().colors().border,
- block_quote_border_color: Color::Muted.color(cx),
- block_quote: TextStyleRefinement {
- color: Some(Color::Muted.color(cx)),
- ..Default::default()
- },
- link: TextStyleRefinement {
- color: Some(cx.theme().colors().editor_foreground),
- underline: Some(gpui::UnderlineStyle {
- thickness: px(1.),
- color: Some(cx.theme().colors().editor_foreground),
- wavy: false,
- }),
- ..Default::default()
- },
- syntax: cx.theme().syntax().clone(),
- selection_background_color: { cx.theme().players().local().selection },
-
- heading: StyleRefinement::default()
- .font_weight(FontWeight::BOLD)
- .text_base()
- .mt(rems(1.))
- .mb_0(),
- };
-
Markdown::new(
- combined_text,
- markdown_style.clone(),
+ combined_text.into(),
+ hover_markdown_style(window, cx),
Some(language_registry.clone()),
fallback_language_name,
window,
cx,
)
.copy_code_block_buttons(false)
+ .open_url(open_markdown_url)
})
.ok();
rendered_block
}
+pub fn hover_markdown_style(window: &Window, cx: &App) -> MarkdownStyle {
+ let settings = ThemeSettings::get_global(cx);
+ let ui_font_family = settings.ui_font.family.clone();
+ let ui_font_fallbacks = settings.ui_font.fallbacks.clone();
+ let buffer_font_family = settings.buffer_font.family.clone();
+ let buffer_font_fallbacks = settings.buffer_font.fallbacks.clone();
+
+ let mut base_text_style = window.text_style();
+ base_text_style.refine(&TextStyleRefinement {
+ font_family: Some(ui_font_family.clone()),
+ font_fallbacks: ui_font_fallbacks,
+ color: Some(cx.theme().colors().editor_foreground),
+ ..Default::default()
+ });
+ MarkdownStyle {
+ base_text_style,
+ code_block: StyleRefinement::default().my(rems(1.)).font_buffer(cx),
+ inline_code: TextStyleRefinement {
+ background_color: Some(cx.theme().colors().background),
+ font_family: Some(buffer_font_family),
+ font_fallbacks: buffer_font_fallbacks,
+ ..Default::default()
+ },
+ rule_color: cx.theme().colors().border,
+ block_quote_border_color: Color::Muted.color(cx),
+ block_quote: TextStyleRefinement {
+ color: Some(Color::Muted.color(cx)),
+ ..Default::default()
+ },
+ link: TextStyleRefinement {
+ color: Some(cx.theme().colors().editor_foreground),
+ underline: Some(gpui::UnderlineStyle {
+ thickness: px(1.),
+ color: Some(cx.theme().colors().editor_foreground),
+ wavy: false,
+ }),
+ ..Default::default()
+ },
+ syntax: cx.theme().syntax().clone(),
+ selection_background_color: { cx.theme().players().local().selection },
+
+ heading: StyleRefinement::default()
+ .font_weight(FontWeight::BOLD)
+ .text_base()
+ .mt(rems(1.))
+ .mb_0(),
+ }
+}
+
+pub fn open_markdown_url(link: SharedString, window: &mut Window, cx: &mut App) {
+ if let Ok(uri) = Url::parse(&link) {
+ if uri.scheme() == "file" {
+ if let Some(workspace) = window.root::<Workspace>().flatten() {
+ workspace.update(cx, |workspace, cx| {
+ let task =
+ workspace.open_abs_path(PathBuf::from(uri.path()), false, window, cx);
+
+ cx.spawn_in(window, |_, mut cx| async move {
+ let item = task.await?;
+ // Ruby LSP uses URLs with #L1,1-4,4
+ // we'll just take the first number and assume it's a line number
+ let Some(fragment) = uri.fragment() else {
+ return anyhow::Ok(());
+ };
+ let mut accum = 0u32;
+ for c in fragment.chars() {
+ if c >= '0' && c <= '9' && accum < u32::MAX / 2 {
+ accum *= 10;
+ accum += c as u32 - '0' as u32;
+ } else if accum > 0 {
+ break;
+ }
+ }
+ if accum == 0 {
+ return Ok(());
+ }
+ let Some(editor) = cx.update(|_, cx| item.act_as::<Editor>(cx))? else {
+ return Ok(());
+ };
+ editor.update_in(&mut cx, |editor, window, cx| {
+ editor.change_selections(
+ Some(Autoscroll::fit()),
+ window,
+ cx,
+ |selections| {
+ selections.select_ranges([text::Point::new(accum - 1, 0)
+ ..text::Point::new(accum - 1, 0)]);
+ },
+ );
+ })
+ })
+ .detach_and_log_err(cx);
+ });
+ return;
+ }
+ }
+ }
+ cx.open_url(&link);
+}
+
#[derive(Default, Debug)]
pub struct HoverState {
pub info_popovers: Vec<InfoPopover>,
@@ -7,7 +7,6 @@ pub use crate::{
use crate::{
diagnostic_set::{DiagnosticEntry, DiagnosticGroup},
language_settings::{language_settings, LanguageSettings},
- markdown::parse_markdown,
outline::OutlineItem,
syntax_map::{
SyntaxLayer, SyntaxMap, SyntaxMapCapture, SyntaxMapCaptures, SyntaxMapMatch,
@@ -231,50 +230,6 @@ pub struct Diagnostic {
pub data: Option<Value>,
}
-/// TODO - move this into the `project` crate and make it private.
-pub async fn prepare_completion_documentation(
- documentation: &lsp::Documentation,
- language_registry: &Arc<LanguageRegistry>,
- language: Option<Arc<Language>>,
-) -> CompletionDocumentation {
- match documentation {
- lsp::Documentation::String(text) => {
- if text.lines().count() <= 1 {
- CompletionDocumentation::SingleLine(text.clone())
- } else {
- CompletionDocumentation::MultiLinePlainText(text.clone())
- }
- }
-
- lsp::Documentation::MarkupContent(lsp::MarkupContent { kind, value }) => match kind {
- lsp::MarkupKind::PlainText => {
- if value.lines().count() <= 1 {
- CompletionDocumentation::SingleLine(value.clone())
- } else {
- CompletionDocumentation::MultiLinePlainText(value.clone())
- }
- }
-
- lsp::MarkupKind::Markdown => {
- let parsed = parse_markdown(value, Some(language_registry), language).await;
- CompletionDocumentation::MultiLineMarkdown(parsed)
- }
- },
- }
-}
-
-#[derive(Clone, Debug)]
-pub enum CompletionDocumentation {
- /// There is no documentation for this completion.
- Undocumented,
- /// A single line of documentation.
- SingleLine(String),
- /// Multiple lines of plain text documentation.
- MultiLinePlainText(String),
- /// Markdown documentation.
- MultiLineMarkdown(ParsedMarkdown),
-}
-
/// An operation used to synchronize this buffer with its other replicas.
#[derive(Clone, Debug, PartialEq)]
pub enum Operation {
@@ -20,7 +20,6 @@ test-support = [
[dependencies]
anyhow.workspace = true
-futures.workspace = true
gpui.workspace = true
language.workspace = true
linkify.workspace = true
@@ -34,7 +33,7 @@ util.workspace = true
assets.workspace = true
env_logger.workspace = true
gpui = { workspace = true, features = ["test-support"] }
-languages.workspace = true
+languages = { workspace = true, features = ["load-grammars"] }
node_runtime.workspace = true
settings = { workspace = true, features = ["test-support"] }
util = { workspace = true, features = ["test-support"] }
@@ -15,84 +15,12 @@ const MARKDOWN_EXAMPLE: &str = r#"
## Headings
Headings are created by adding one or more `#` symbols before your heading text. The number of `#` you use will determine the size of the heading.
-```rust
-gpui::window::ViewContext
-impl<'a, V> ViewContext<'a, V>
-pub fn on_blur(&mut self, handle: &FocusHandle, listener: impl FnMut(&mut V, &mut iewContext<V>) + 'static) -> Subscription
-where
- // Bounds from impl:
- V: 'static,
```
+function a(b: T) {
-## Emphasis
-Emphasis can be added with italics or bold. *This text will be italic*. _This will also be italic_
-
-## Lists
-
-### Unordered Lists
-Unordered lists use asterisks `*`, plus `+`, or minus `-` as list markers.
-
-* Item 1
-* Item 2
- * Item 2a
- * Item 2b
-
-### Ordered Lists
-Ordered lists use numbers followed by a period.
-
-1. Item 1
-2. Item 2
-3. Item 3
- 1. Item 3a
- 2. Item 3b
-
-## Links
-Links are created using the format [http://zed.dev](https://zed.dev).
-
-They can also be detected automatically, for example https://zed.dev/blog.
-
-They may contain dollar signs:
-
-[https://svelte.dev/docs/svelte/$state](https://svelte.dev/docs/svelte/$state)
-
-https://svelte.dev/docs/svelte/$state
-
-## Images
-Images are like links, but with an exclamation mark `!` in front.
-
-```markdown
-
-```
-
-## Code
-Inline `code` can be wrapped with backticks `` ` ``.
-
-```markdown
-Inline `code` has `back-ticks around` it.
-```
-
-Code blocks can be created by indenting lines by four spaces or with triple backticks ```.
-
-```javascript
-function test() {
- console.log("notice the blank line before this function?");
}
```
-## Blockquotes
-Blockquotes are created with `>`.
-
-> This is a blockquote.
-
-## Horizontal Rules
-Horizontal rules are created using three or more asterisks `***`, dashes `---`, or underscores `___`.
-
-## Line breaks
-This is a
-\
-line break!
-
----
Remember, markdown processors may have slight differences and extensions, so always refer to the specific documentation or guides relevant to your platform or editor for the best practices and additional features.
"#;
@@ -161,7 +89,7 @@ pub fn main() {
};
MarkdownExample::new(
- MARKDOWN_EXAMPLE.to_string(),
+ MARKDOWN_EXAMPLE.into(),
markdown_style,
language_registry,
window,
@@ -179,14 +107,22 @@ struct MarkdownExample {
impl MarkdownExample {
pub fn new(
- text: String,
+ text: SharedString,
style: MarkdownStyle,
language_registry: Arc<LanguageRegistry>,
window: &mut Window,
cx: &mut App,
) -> Self {
- let markdown =
- cx.new(|cx| Markdown::new(text, style, Some(language_registry), None, window, cx));
+ let markdown = cx.new(|cx| {
+ Markdown::new(
+ text,
+ style,
+ Some(language_registry),
+ Some("TypeScript".to_string()),
+ window,
+ cx,
+ )
+ });
Self { markdown }
}
}
@@ -1,7 +1,6 @@
pub mod parser;
use crate::parser::CodeBlockKind;
-use futures::FutureExt;
use gpui::{
actions, point, quad, AnyElement, App, Bounds, ClipboardItem, CursorStyle, DispatchPhase,
Edges, Entity, FocusHandle, Focusable, FontStyle, FontWeight, GlobalElementId, Hitbox, Hsla,
@@ -12,7 +11,7 @@ use gpui::{
use language::{Language, LanguageRegistry, Rope};
use parser::{parse_links_only, parse_markdown, MarkdownEvent, MarkdownTag, MarkdownTagEnd};
-use std::{iter, mem, ops::Range, rc::Rc, sync::Arc};
+use std::{collections::HashMap, iter, mem, ops::Range, rc::Rc, sync::Arc};
use theme::SyntaxTheme;
use ui::{prelude::*, Tooltip};
use util::{ResultExt, TryFutureExt};
@@ -49,7 +48,7 @@ impl Default for MarkdownStyle {
}
pub struct Markdown {
- source: String,
+ source: SharedString,
selection: Selection,
pressed_link: Option<RenderedLink>,
autoscroll_request: Option<usize>,
@@ -60,6 +59,7 @@ pub struct Markdown {
focus_handle: FocusHandle,
language_registry: Option<Arc<LanguageRegistry>>,
fallback_code_block_language: Option<String>,
+ open_url: Option<Box<dyn Fn(SharedString, &mut Window, &mut App)>>,
options: Options,
}
@@ -73,7 +73,7 @@ actions!(markdown, [Copy]);
impl Markdown {
pub fn new(
- source: String,
+ source: SharedString,
style: MarkdownStyle,
language_registry: Option<Arc<LanguageRegistry>>,
fallback_code_block_language: Option<String>,
@@ -97,13 +97,24 @@ impl Markdown {
parse_links_only: false,
copy_code_block_buttons: true,
},
+ open_url: None,
};
this.parse(window, cx);
this
}
+ pub fn open_url(
+ self,
+ open_url: impl Fn(SharedString, &mut Window, &mut App) + 'static,
+ ) -> Self {
+ Self {
+ open_url: Some(Box::new(open_url)),
+ ..self
+ }
+ }
+
pub fn new_text(
- source: String,
+ source: SharedString,
style: MarkdownStyle,
language_registry: Option<Arc<LanguageRegistry>>,
fallback_code_block_language: Option<String>,
@@ -127,6 +138,7 @@ impl Markdown {
parse_links_only: true,
copy_code_block_buttons: true,
},
+ open_url: None,
};
this.parse(window, cx);
this
@@ -137,11 +149,11 @@ impl Markdown {
}
pub fn append(&mut self, text: &str, window: &mut Window, cx: &mut Context<Self>) {
- self.source.push_str(text);
+ self.source = SharedString::new(self.source.to_string() + text);
self.parse(window, cx);
}
- pub fn reset(&mut self, source: String, window: &mut Window, cx: &mut Context<Self>) {
+ pub fn reset(&mut self, source: SharedString, window: &mut Window, cx: &mut Context<Self>) {
if source == self.source() {
return;
}
@@ -176,17 +188,38 @@ impl Markdown {
return;
}
- let text = self.source.clone();
+ let source = self.source.clone();
let parse_text_only = self.options.parse_links_only;
+ let language_registry = self.language_registry.clone();
+ let fallback = self.fallback_code_block_language.clone();
let parsed = cx.background_spawn(async move {
- let text = SharedString::from(text);
- let events = match parse_text_only {
- true => Arc::from(parse_links_only(text.as_ref())),
- false => Arc::from(parse_markdown(text.as_ref())),
- };
+ if parse_text_only {
+ return anyhow::Ok(ParsedMarkdown {
+ events: Arc::from(parse_links_only(source.as_ref())),
+ source,
+ languages: HashMap::default(),
+ });
+ }
+ let (events, language_names) = parse_markdown(&source);
+ let mut languages = HashMap::with_capacity(language_names.len());
+ for name in language_names {
+ if let Some(registry) = language_registry.as_ref() {
+ let language = if !name.is_empty() {
+ registry.language_for_name(&name)
+ } else if let Some(fallback) = &fallback {
+ registry.language_for_name(fallback)
+ } else {
+ continue;
+ };
+ if let Ok(language) = language.await {
+ languages.insert(name, language);
+ }
+ }
+ }
anyhow::Ok(ParsedMarkdown {
- source: text,
- events,
+ source,
+ events: Arc::from(events),
+ languages,
})
});
@@ -217,12 +250,7 @@ impl Markdown {
impl Render for Markdown {
fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
- MarkdownElement::new(
- cx.entity().clone(),
- self.style.clone(),
- self.language_registry.clone(),
- self.fallback_code_block_language.clone(),
- )
+ MarkdownElement::new(cx.entity().clone(), self.style.clone())
}
}
@@ -270,6 +298,7 @@ impl Selection {
pub struct ParsedMarkdown {
source: SharedString,
events: Arc<[(Range<usize>, MarkdownEvent)]>,
+ languages: HashMap<SharedString, Arc<Language>>,
}
impl ParsedMarkdown {
@@ -285,61 +314,11 @@ impl ParsedMarkdown {
pub struct MarkdownElement {
markdown: Entity<Markdown>,
style: MarkdownStyle,
- language_registry: Option<Arc<LanguageRegistry>>,
- fallback_code_block_language: Option<String>,
}
impl MarkdownElement {
- fn new(
- markdown: Entity<Markdown>,
- style: MarkdownStyle,
- language_registry: Option<Arc<LanguageRegistry>>,
- fallback_code_block_language: Option<String>,
- ) -> Self {
- Self {
- markdown,
- style,
- language_registry,
- fallback_code_block_language,
- }
- }
-
- fn load_language(
- &self,
- name: &str,
- window: &mut Window,
- cx: &mut App,
- ) -> Option<Arc<Language>> {
- let language_test = self.language_registry.as_ref()?.language_for_name(name);
-
- let language_name = match language_test.now_or_never() {
- Some(Ok(_)) => String::from(name),
- Some(Err(_)) if !name.is_empty() && self.fallback_code_block_language.is_some() => {
- self.fallback_code_block_language.clone().unwrap()
- }
- _ => String::new(),
- };
-
- let language = self
- .language_registry
- .as_ref()?
- .language_for_name(language_name.as_str())
- .map(|language| language.ok())
- .shared();
-
- match language.clone().now_or_never() {
- Some(language) => language,
- None => {
- let markdown = self.markdown.downgrade();
- window
- .spawn(cx, |mut cx| async move {
- language.await;
- markdown.update(&mut cx, |_, cx| cx.notify())
- })
- .detach_and_log_err(cx);
- None
- }
- }
+ fn new(markdown: Entity<Markdown>, style: MarkdownStyle) -> Self {
+ Self { markdown, style }
}
fn paint_selection(
@@ -452,7 +431,7 @@ impl MarkdownElement {
pending: true,
};
window.focus(&markdown.focus_handle);
- window.prevent_default()
+ window.prevent_default();
}
cx.notify();
@@ -492,11 +471,15 @@ impl MarkdownElement {
});
self.on_mouse_event(window, cx, {
let rendered_text = rendered_text.clone();
- move |markdown, event: &MouseUpEvent, phase, _, cx| {
+ move |markdown, event: &MouseUpEvent, phase, window, cx| {
if phase.bubble() {
if let Some(pressed_link) = markdown.pressed_link.take() {
if Some(&pressed_link) == rendered_text.link_for_position(event.position) {
- cx.open_url(&pressed_link.destination_url);
+ if let Some(open_url) = markdown.open_url.as_mut() {
+ open_url(pressed_link.destination_url, window, cx);
+ } else {
+ cx.open_url(&pressed_link.destination_url);
+ }
}
}
} else if markdown.selection.pending {
@@ -617,7 +600,7 @@ impl Element for MarkdownElement {
}
MarkdownTag::CodeBlock(kind) => {
let language = if let CodeBlockKind::Fenced(language) = kind {
- self.load_language(language.as_ref(), window, cx)
+ parsed_markdown.languages.get(language).cloned()
} else {
None
};
@@ -2,15 +2,16 @@ use gpui::SharedString;
use linkify::LinkFinder;
pub use pulldown_cmark::TagEnd as MarkdownTagEnd;
use pulldown_cmark::{Alignment, HeadingLevel, LinkType, MetadataBlockKind, Options, Parser};
-use std::ops::Range;
+use std::{collections::HashSet, ops::Range};
-pub fn parse_markdown(text: &str) -> Vec<(Range<usize>, MarkdownEvent)> {
+pub fn parse_markdown(text: &str) -> (Vec<(Range<usize>, MarkdownEvent)>, HashSet<SharedString>) {
let mut options = Options::all();
options.remove(pulldown_cmark::Options::ENABLE_DEFINITION_LIST);
options.remove(pulldown_cmark::Options::ENABLE_YAML_STYLE_METADATA_BLOCKS);
options.remove(pulldown_cmark::Options::ENABLE_MATH);
let mut events = Vec::new();
+ let mut languages = HashSet::new();
let mut within_link = false;
let mut within_metadata = false;
for (pulldown_event, mut range) in Parser::new_ext(text, options).into_offset_iter() {
@@ -27,6 +28,11 @@ pub fn parse_markdown(text: &str) -> Vec<(Range<usize>, MarkdownEvent)> {
match tag {
pulldown_cmark::Tag::Link { .. } => within_link = true,
pulldown_cmark::Tag::MetadataBlock { .. } => within_metadata = true,
+ pulldown_cmark::Tag::CodeBlock(pulldown_cmark::CodeBlockKind::Fenced(
+ ref language,
+ )) => {
+ languages.insert(SharedString::from(language.to_string()));
+ }
_ => {}
}
events.push((range, MarkdownEvent::Start(tag.into())))
@@ -102,7 +108,7 @@ pub fn parse_markdown(text: &str) -> Vec<(Range<usize>, MarkdownEvent)> {
pulldown_cmark::Event::InlineMath(_) | pulldown_cmark::Event::DisplayMath(_) => {}
}
}
- events
+ (events, languages)
}
pub fn parse_links_only(mut text: &str) -> Vec<(Range<usize>, MarkdownEvent)> {
@@ -26,7 +26,8 @@ use futures::{
};
use globset::{Glob, GlobBuilder, GlobMatcher, GlobSet, GlobSetBuilder};
use gpui::{
- App, AppContext as _, AsyncApp, Context, Entity, EventEmitter, PromptLevel, Task, WeakEntity,
+ App, AppContext as _, AsyncApp, Context, Entity, EventEmitter, PromptLevel, SharedString, Task,
+ WeakEntity,
};
use http_client::HttpClient;
use itertools::Itertools as _;
@@ -34,13 +35,12 @@ use language::{
language_settings::{
language_settings, FormatOnSave, Formatter, LanguageSettings, SelectedFormatter,
},
- markdown, point_to_lsp, prepare_completion_documentation,
+ point_to_lsp,
proto::{deserialize_anchor, deserialize_version, serialize_anchor, serialize_version},
range_from_lsp, range_to_lsp, Bias, Buffer, BufferSnapshot, CachedLspAdapter, CodeLabel,
- CompletionDocumentation, Diagnostic, DiagnosticEntry, DiagnosticSet, Diff, File as _, Language,
- LanguageRegistry, LanguageServerBinaryStatus, LanguageToolchainStore, LocalFile, LspAdapter,
- LspAdapterDelegate, Patch, PointUtf16, TextBufferSnapshot, ToOffset, ToPointUtf16, Transaction,
- Unclipped,
+ Diagnostic, DiagnosticEntry, DiagnosticSet, Diff, File as _, Language, LanguageRegistry,
+ LanguageServerBinaryStatus, LanguageToolchainStore, LocalFile, LspAdapter, LspAdapterDelegate,
+ Patch, PointUtf16, TextBufferSnapshot, ToOffset, ToPointUtf16, Transaction, Unclipped,
};
use lsp::{
notification::DidRenameFiles, CodeActionKind, CompletionContext, DiagnosticSeverity,
@@ -4204,14 +4204,8 @@ impl LspStore {
cx.foreground_executor().spawn(async move {
let completions = task.await?;
let mut result = Vec::new();
- populate_labels_for_completions(
- completions,
- &language_registry,
- language,
- lsp_adapter,
- &mut result,
- )
- .await;
+ populate_labels_for_completions(completions, language, lsp_adapter, &mut result)
+ .await;
Ok(result)
})
} else if let Some(local) = self.as_local() {
@@ -4260,7 +4254,6 @@ impl LspStore {
if let Ok(new_completions) = task.await {
populate_labels_for_completions(
new_completions,
- &language_registry,
language.clone(),
lsp_adapter,
&mut completions,
@@ -4284,7 +4277,6 @@ impl LspStore {
cx: &mut Context<Self>,
) -> Task<Result<bool>> {
let client = self.upstream_client();
- let language_registry = self.languages.clone();
let buffer_id = buffer.read(cx).remote_id();
let buffer_snapshot = buffer.read(cx).snapshot();
@@ -4302,7 +4294,6 @@ impl LspStore {
completions.clone(),
completion_index,
client.clone(),
- language_registry.clone(),
)
.await
.log_err()
@@ -4343,7 +4334,6 @@ impl LspStore {
&buffer_snapshot,
completions.clone(),
completion_index,
- language_registry.clone(),
)
.await
.log_err();
@@ -4419,22 +4409,14 @@ impl LspStore {
snapshot: &BufferSnapshot,
completions: Rc<RefCell<Box<[Completion]>>>,
completion_index: usize,
- language_registry: Arc<LanguageRegistry>,
) -> Result<()> {
let completion_item = completions.borrow()[completion_index]
.lsp_completion
.clone();
- if let Some(lsp_documentation) = completion_item.documentation.as_ref() {
- let documentation = language::prepare_completion_documentation(
- lsp_documentation,
- &language_registry,
- snapshot.language().cloned(),
- )
- .await;
-
+ if let Some(lsp_documentation) = completion_item.documentation.clone() {
let mut completions = completions.borrow_mut();
let completion = &mut completions[completion_index];
- completion.documentation = Some(documentation);
+ completion.documentation = Some(lsp_documentation.into());
} else {
let mut completions = completions.borrow_mut();
let completion = &mut completions[completion_index];
@@ -4487,7 +4469,6 @@ impl LspStore {
completions: Rc<RefCell<Box<[Completion]>>>,
completion_index: usize,
client: AnyProtoClient,
- language_registry: Arc<LanguageRegistry>,
) -> Result<()> {
let lsp_completion = {
let completion = &completions.borrow()[completion_index];
@@ -4514,14 +4495,11 @@ impl LspStore {
let documentation = if response.documentation.is_empty() {
CompletionDocumentation::Undocumented
} else if response.documentation_is_markdown {
- CompletionDocumentation::MultiLineMarkdown(
- markdown::parse_markdown(&response.documentation, Some(&language_registry), None)
- .await,
- )
+ CompletionDocumentation::MultiLineMarkdown(response.documentation.into())
} else if response.documentation.lines().count() <= 1 {
- CompletionDocumentation::SingleLine(response.documentation)
+ CompletionDocumentation::SingleLine(response.documentation.into())
} else {
- CompletionDocumentation::MultiLinePlainText(response.documentation)
+ CompletionDocumentation::MultiLinePlainText(response.documentation.into())
};
let mut completions = completions.borrow_mut();
@@ -8060,7 +8038,6 @@ fn remove_empty_hover_blocks(mut hover: Hover) -> Option<Hover> {
async fn populate_labels_for_completions(
mut new_completions: Vec<CoreCompletion>,
- language_registry: &Arc<LanguageRegistry>,
language: Option<Arc<Language>>,
lsp_adapter: Option<Arc<CachedLspAdapter>>,
completions: &mut Vec<Completion>,
@@ -8085,8 +8062,8 @@ async fn populate_labels_for_completions(
.zip(lsp_completions)
.zip(labels.into_iter().chain(iter::repeat(None)))
{
- let documentation = if let Some(docs) = &lsp_completion.documentation {
- Some(prepare_completion_documentation(docs, language_registry, language.clone()).await)
+ let documentation = if let Some(docs) = lsp_completion.documentation.clone() {
+ Some(docs.into())
} else {
None
};
@@ -8477,6 +8454,46 @@ impl DiagnosticSummary {
}
}
+#[derive(Clone, Debug)]
+pub enum CompletionDocumentation {
+ /// There is no documentation for this completion.
+ Undocumented,
+ /// A single line of documentation.
+ SingleLine(SharedString),
+ /// Multiple lines of plain text documentation.
+ MultiLinePlainText(SharedString),
+ /// Markdown documentation.
+ MultiLineMarkdown(SharedString),
+}
+
+impl From<lsp::Documentation> for CompletionDocumentation {
+ fn from(docs: lsp::Documentation) -> Self {
+ match docs {
+ lsp::Documentation::String(text) => {
+ if text.lines().count() <= 1 {
+ CompletionDocumentation::SingleLine(text.into())
+ } else {
+ CompletionDocumentation::MultiLinePlainText(text.into())
+ }
+ }
+
+ lsp::Documentation::MarkupContent(lsp::MarkupContent { kind, value }) => match kind {
+ lsp::MarkupKind::PlainText => {
+ if value.lines().count() <= 1 {
+ CompletionDocumentation::SingleLine(value.into())
+ } else {
+ CompletionDocumentation::MultiLinePlainText(value.into())
+ }
+ }
+
+ lsp::MarkupKind::Markdown => {
+ CompletionDocumentation::MultiLineMarkdown(value.into())
+ }
+ },
+ }
+ }
+}
+
fn glob_literal_prefix(glob: &Path) -> PathBuf {
glob.components()
.take_while(|component| match component {
@@ -58,15 +58,15 @@ use gpui::{
use itertools::Itertools;
use language::{
language_settings::InlayHintKind, proto::split_operations, Buffer, BufferEvent, Capability,
- CodeLabel, CompletionDocumentation, File as _, Language, LanguageName, LanguageRegistry,
- PointUtf16, ToOffset, ToPointUtf16, Toolchain, ToolchainList, Transaction, Unclipped,
+ CodeLabel, File as _, Language, LanguageName, LanguageRegistry, PointUtf16, ToOffset,
+ ToPointUtf16, Toolchain, ToolchainList, Transaction, Unclipped,
};
use lsp::{
CodeActionKind, CompletionContext, CompletionItemKind, DocumentHighlightKind, LanguageServerId,
LanguageServerName, MessageActionItem,
};
use lsp_command::*;
-use lsp_store::{LspFormatTarget, OpenLspBufferHandle};
+use lsp_store::{CompletionDocumentation, LspFormatTarget, OpenLspBufferHandle};
use node_runtime::NodeRuntime;
use parking_lot::Mutex;
pub use prettier_store::PrettierStore;
@@ -208,7 +208,7 @@ impl SshPrompt {
..Default::default()
};
let markdown =
- cx.new(|cx| Markdown::new_text(prompt, markdown_style, None, None, window, cx));
+ cx.new(|cx| Markdown::new_text(prompt.into(), markdown_style, None, None, window, cx));
self.prompt = Some((markdown, tx));
self.status_message.take();
window.focus(&self.editor.focus_handle(cx));
@@ -1,7 +1,7 @@
use gpui::{
div, App, AppContext as _, Context, Entity, EventEmitter, FocusHandle, Focusable, FontWeight,
InteractiveElement, IntoElement, ParentElement, PromptHandle, PromptLevel, PromptResponse,
- Refineable, Render, RenderablePromptHandle, Styled, TextStyleRefinement, Window,
+ Refineable, Render, RenderablePromptHandle, SharedString, Styled, TextStyleRefinement, Window,
};
use markdown::{Markdown, MarkdownStyle};
use settings::Settings;
@@ -48,7 +48,14 @@ pub fn fallback_prompt_renderer(
selection_background_color: { cx.theme().players().local().selection },
..Default::default()
};
- Markdown::new(text.to_string(), markdown_style, None, None, window, cx)
+ Markdown::new(
+ SharedString::new(text),
+ markdown_style,
+ None,
+ None,
+ window,
+ cx,
+ )
})
}),
}