diff --git a/Cargo.lock b/Cargo.lock index 0ae7925341..b8bd4389d0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7652,7 +7652,6 @@ dependencies = [ "anyhow", "assets", "env_logger 0.11.6", - "futures 0.3.31", "gpui", "language", "languages", diff --git a/crates/assistant2/src/active_thread.rs b/crates/assistant2/src/active_thread.rs index f479f755a1..bf1a31158c 100644 --- a/crates/assistant2/src/active_thread.rs +++ b/crates/assistant2/src/active_thread.rs @@ -179,7 +179,7 @@ impl ActiveThread { let markdown = cx.new(|cx| { Markdown::new( - text, + text.into(), markdown_style, Some(self.language_registry.clone()), None, diff --git a/crates/assistant_context_editor/src/slash_command.rs b/crates/assistant_context_editor/src/slash_command.rs index d51234e4ca..691f39bc75 100644 --- a/crates/assistant_context_editor/src/slash_command.rs +++ b/crates/assistant_context_editor/src/slash_command.rs @@ -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), diff --git a/crates/editor/src/code_context_menus.rs b/crates/editor/src/code_context_menus.rs index a967750ccc..0176651f2f 100644 --- a/crates/editor/src/code_context_menus.rs +++ b/crates/editor/src/code_context_menus.rs @@ -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, - workspace: Option>, + window: &mut Window, cx: &mut Context, ) -> Option { 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) -> 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>>>, + markdown_element: Option>, } 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, - workspace: Option>, + window: &mut Window, cx: &mut Context, ) -> Option { 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, diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 0788d7d51d..457296ab7c 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -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, + window: &mut Window, cx: &mut Context, ) -> Option { - 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), diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index fd5663f297..96d2c3804f 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -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, - + window: &mut Window, cx: &mut App, ) -> Option { 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) }) } } diff --git a/crates/editor/src/hover_popover.rs b/crates/editor/src/hover_popover.rs index 128ee45341..bec413329a 100644 --- a/crates/editor/src/hover_popover.rs +++ b/crates/editor/src/hover_popover.rs @@ -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::().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::(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, diff --git a/crates/language/src/buffer.rs b/crates/language/src/buffer.rs index e51a1ce23e..022031a046 100644 --- a/crates/language/src/buffer.rs +++ b/crates/language/src/buffer.rs @@ -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, } -/// TODO - move this into the `project` crate and make it private. -pub async fn prepare_completion_documentation( - documentation: &lsp::Documentation, - language_registry: &Arc, - language: Option>, -) -> 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 { diff --git a/crates/markdown/Cargo.toml b/crates/markdown/Cargo.toml index 5fe64ece7b..0fffd5e091 100644 --- a/crates/markdown/Cargo.toml +++ b/crates/markdown/Cargo.toml @@ -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"] } diff --git a/crates/markdown/examples/markdown.rs b/crates/markdown/examples/markdown.rs index 0745020943..f4eafc5e1f 100644 --- a/crates/markdown/examples/markdown.rs +++ b/crates/markdown/examples/markdown.rs @@ -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) + '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 -![This is an image](/images/logo.png) -``` - -## 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, 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 } } } diff --git a/crates/markdown/src/markdown.rs b/crates/markdown/src/markdown.rs index fbf9983d1f..11cbda57ee 100644 --- a/crates/markdown/src/markdown.rs +++ b/crates/markdown/src/markdown.rs @@ -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, autoscroll_request: Option, @@ -60,6 +59,7 @@ pub struct Markdown { focus_handle: FocusHandle, language_registry: Option>, fallback_code_block_language: Option, + open_url: Option>, options: Options, } @@ -73,7 +73,7 @@ actions!(markdown, [Copy]); impl Markdown { pub fn new( - source: String, + source: SharedString, style: MarkdownStyle, language_registry: Option>, fallback_code_block_language: Option, @@ -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>, fallback_code_block_language: Option, @@ -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.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) { + pub fn reset(&mut self, source: SharedString, window: &mut Window, cx: &mut Context) { 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) -> 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, MarkdownEvent)]>, + languages: HashMap>, } impl ParsedMarkdown { @@ -285,61 +314,11 @@ impl ParsedMarkdown { pub struct MarkdownElement { markdown: Entity, style: MarkdownStyle, - language_registry: Option>, - fallback_code_block_language: Option, } impl MarkdownElement { - fn new( - markdown: Entity, - style: MarkdownStyle, - language_registry: Option>, - fallback_code_block_language: Option, - ) -> Self { - Self { - markdown, - style, - language_registry, - fallback_code_block_language, - } - } - - fn load_language( - &self, - name: &str, - window: &mut Window, - cx: &mut App, - ) -> Option> { - 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, 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 }; diff --git a/crates/markdown/src/parser.rs b/crates/markdown/src/parser.rs index 9e69f3192e..29fe24d262 100644 --- a/crates/markdown/src/parser.rs +++ b/crates/markdown/src/parser.rs @@ -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, MarkdownEvent)> { +pub fn parse_markdown(text: &str) -> (Vec<(Range, MarkdownEvent)>, HashSet) { 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, 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, MarkdownEvent)> { pulldown_cmark::Event::InlineMath(_) | pulldown_cmark::Event::DisplayMath(_) => {} } } - events + (events, languages) } pub fn parse_links_only(mut text: &str) -> Vec<(Range, MarkdownEvent)> { diff --git a/crates/project/src/lsp_store.rs b/crates/project/src/lsp_store.rs index 8520f0a616..69ef5228cd 100644 --- a/crates/project/src/lsp_store.rs +++ b/crates/project/src/lsp_store.rs @@ -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, ) -> Task> { 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>>, completion_index: usize, - language_registry: Arc, ) -> 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>>, completion_index: usize, client: AnyProtoClient, - language_registry: Arc, ) -> 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 { async fn populate_labels_for_completions( mut new_completions: Vec, - language_registry: &Arc, language: Option>, lsp_adapter: Option>, completions: &mut Vec, @@ -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 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 { diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index ee96072890..7621643d99 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -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; diff --git a/crates/recent_projects/src/ssh_connections.rs b/crates/recent_projects/src/ssh_connections.rs index 249f9db116..97f7106d1b 100644 --- a/crates/recent_projects/src/ssh_connections.rs +++ b/crates/recent_projects/src/ssh_connections.rs @@ -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)); diff --git a/crates/zed/src/zed/linux_prompts.rs b/crates/zed/src/zed/linux_prompts.rs index 01b1e53aa8..e838c8b029 100644 --- a/crates/zed/src/zed/linux_prompts.rs +++ b/crates/zed/src/zed/linux_prompts.rs @@ -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, + ) }) }), }