Fix clicking on file links in editor (#25117)

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
This commit is contained in:
Conrad Irwin 2025-02-18 22:54:35 -07:00 committed by GitHub
parent ebbc6a9752
commit 1678e3cbf1
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
16 changed files with 353 additions and 352 deletions

1
Cargo.lock generated
View file

@ -7652,7 +7652,6 @@ dependencies = [
"anyhow", "anyhow",
"assets", "assets",
"env_logger 0.11.6", "env_logger 0.11.6",
"futures 0.3.31",
"gpui", "gpui",
"language", "language",
"languages", "languages",

View file

@ -179,7 +179,7 @@ impl ActiveThread {
let markdown = cx.new(|cx| { let markdown = cx.new(|cx| {
Markdown::new( Markdown::new(
text, text.into(),
markdown_style, markdown_style,
Some(self.language_registry.clone()), Some(self.language_registry.clone()),
None, None,

View file

@ -5,9 +5,9 @@ use assistant_slash_command::{AfterCompletion, SlashCommandLine, SlashCommandWor
use editor::{CompletionProvider, Editor}; use editor::{CompletionProvider, Editor};
use fuzzy::{match_strings, StringMatchCandidate}; use fuzzy::{match_strings, StringMatchCandidate};
use gpui::{App, AppContext as _, Context, Entity, Task, WeakEntity, Window}; 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 parking_lot::Mutex;
use project::CompletionIntent; use project::{lsp_store::CompletionDocumentation, CompletionIntent};
use rope::Point; use rope::Point;
use std::{ use std::{
cell::RefCell, cell::RefCell,
@ -121,7 +121,7 @@ impl SlashCommandCompletionProvider {
Some(project::Completion { Some(project::Completion {
old_range: name_range.clone(), old_range: name_range.clone(),
documentation: Some(CompletionDocumentation::SingleLine( documentation: Some(CompletionDocumentation::SingleLine(
command.description(), command.description().into(),
)), )),
new_text, new_text,
label: command.label(cx), label: command.label(cx),

View file

@ -1,14 +1,16 @@
use fuzzy::{StringMatch, StringMatchCandidate}; use fuzzy::{StringMatch, StringMatchCandidate};
use gpui::{ 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, ListSizingBehavior, ScrollStrategy, SharedString, Size, StrikethroughStyle, StyledText,
UniformListScrollHandle, WeakEntity, UniformListScrollHandle,
}; };
use language::Buffer; use language::Buffer;
use language::{CodeLabel, CompletionDocumentation}; use language::CodeLabel;
use lsp::LanguageServerId; use lsp::LanguageServerId;
use markdown::Markdown;
use multi_buffer::{Anchor, ExcerptId}; use multi_buffer::{Anchor, ExcerptId};
use ordered_float::OrderedFloat; use ordered_float::OrderedFloat;
use project::lsp_store::CompletionDocumentation;
use project::{CodeAction, Completion, TaskSourceKind}; use project::{CodeAction, Completion, TaskSourceKind};
use std::{ use std::{
@ -21,12 +23,12 @@ use std::{
use task::ResolvedTask; use task::ResolvedTask;
use ui::{prelude::*, Color, IntoElement, ListItem, Pixels, Popover, Styled}; use ui::{prelude::*, Color, IntoElement, ListItem, Pixels, Popover, Styled};
use util::ResultExt; use util::ResultExt;
use workspace::Workspace;
use crate::hover_popover::{hover_markdown_style, open_markdown_url};
use crate::{ use crate::{
actions::{ConfirmCodeAction, ConfirmCompletion}, actions::{ConfirmCodeAction, ConfirmCompletion},
render_parsed_markdown, split_words, styled_runs_for_code_label, CodeActionProvider, split_words, styled_runs_for_code_label, CodeActionProvider, CompletionId, CompletionProvider,
CompletionId, CompletionProvider, DisplayRow, Editor, EditorStyle, ResolvedTasks, DisplayRow, Editor, EditorStyle, ResolvedTasks,
}; };
pub const MENU_GAP: Pixels = px(4.); pub const MENU_GAP: Pixels = px(4.);
@ -137,17 +139,27 @@ impl CodeContextMenu {
} }
pub fn render_aside( pub fn render_aside(
&self, &mut self,
style: &EditorStyle, editor: &Editor,
max_size: Size<Pixels>, max_size: Size<Pixels>,
workspace: Option<WeakEntity<Workspace>>, window: &mut Window,
cx: &mut Context<Editor>, cx: &mut Context<Editor>,
) -> Option<AnyElement> { ) -> Option<AnyElement> {
match self { 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, 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 { pub enum ContextMenuOrigin {
@ -169,6 +181,7 @@ pub struct CompletionsMenu {
resolve_completions: bool, resolve_completions: bool,
show_completion_documentation: bool, show_completion_documentation: bool,
last_rendered_range: Rc<RefCell<Option<Range<usize>>>>, last_rendered_range: Rc<RefCell<Option<Range<usize>>>>,
markdown_element: Option<Entity<Markdown>>,
} }
impl CompletionsMenu { impl CompletionsMenu {
@ -199,6 +212,7 @@ impl CompletionsMenu {
scroll_handle: UniformListScrollHandle::new(), scroll_handle: UniformListScrollHandle::new(),
resolve_completions: true, resolve_completions: true,
last_rendered_range: RefCell::new(None).into(), last_rendered_range: RefCell::new(None).into(),
markdown_element: None,
} }
} }
@ -255,6 +269,7 @@ impl CompletionsMenu {
resolve_completions: false, resolve_completions: false,
show_completion_documentation: false, show_completion_documentation: false,
last_rendered_range: RefCell::new(None).into(), last_rendered_range: RefCell::new(None).into(),
markdown_element: None,
} }
} }
@ -556,10 +571,10 @@ impl CompletionsMenu {
} }
fn render_aside( fn render_aside(
&self, &mut self,
style: &EditorStyle, editor: &Editor,
max_size: Size<Pixels>, max_size: Size<Pixels>,
workspace: Option<WeakEntity<Workspace>>, window: &mut Window,
cx: &mut Context<Editor>, cx: &mut Context<Editor>,
) -> Option<AnyElement> { ) -> Option<AnyElement> {
if !self.show_completion_documentation { if !self.show_completion_documentation {
@ -571,17 +586,35 @@ impl CompletionsMenu {
.documentation .documentation
.as_ref()? .as_ref()?
{ {
CompletionDocumentation::MultiLinePlainText(text) => { CompletionDocumentation::MultiLinePlainText(text) => div().child(text.clone()),
div().child(SharedString::from(text.clone())) CompletionDocumentation::MultiLineMarkdown(parsed) if !parsed.is_empty() => {
} let markdown = self.markdown_element.get_or_insert_with(|| {
CompletionDocumentation::MultiLineMarkdown(parsed) if !parsed.text.is_empty() => div() cx.new(|cx| {
.child(render_parsed_markdown( let languages = editor
"completions_markdown", .workspace
parsed, .as_ref()
&style, .and_then(|(workspace, _)| workspace.upgrade())
workspace, .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, 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(_) => return None, CompletionDocumentation::MultiLineMarkdown(_) => return None,
CompletionDocumentation::SingleLine(_) => return None, CompletionDocumentation::SingleLine(_) => return None,
CompletionDocumentation::Undocumented => return None, CompletionDocumentation::Undocumented => return None,

View file

@ -99,9 +99,9 @@ use itertools::Itertools;
use language::{ use language::{
language_settings::{self, all_language_settings, language_settings, InlayHintSettings}, language_settings::{self, all_language_settings, language_settings, InlayHintSettings},
markdown, point_from_lsp, AutoindentMode, BracketPair, Buffer, Capability, CharKind, CodeLabel, markdown, point_from_lsp, AutoindentMode, BracketPair, Buffer, Capability, CharKind, CodeLabel,
CompletionDocumentation, CursorShape, Diagnostic, DiskState, EditPredictionsMode, EditPreview, CursorShape, Diagnostic, DiskState, EditPredictionsMode, EditPreview, HighlightedText,
HighlightedText, IndentKind, IndentSize, Language, OffsetRangeExt, Point, Selection, IndentKind, IndentSize, Language, OffsetRangeExt, Point, Selection, SelectionGoal, TextObject,
SelectionGoal, TextObject, TransactionId, TreeSitterOptions, TransactionId, TreeSitterOptions,
}; };
use language::{point_to_lsp, BufferRow, CharClassifier, Runnable, RunnableRange}; use language::{point_to_lsp, BufferRow, CharClassifier, Runnable, RunnableRange};
use linked_editing_ranges::refresh_linked_ranges; use linked_editing_ranges::refresh_linked_ranges;
@ -132,7 +132,7 @@ use multi_buffer::{
ToOffsetUtf16, ToOffsetUtf16,
}; };
use project::{ use project::{
lsp_store::{FormatTrigger, LspFormatTarget, OpenLspBufferHandle}, lsp_store::{CompletionDocumentation, FormatTrigger, LspFormatTarget, OpenLspBufferHandle},
project_settings::{GitGutterSetting, ProjectSettings}, project_settings::{GitGutterSetting, ProjectSettings},
CodeAction, Completion, CompletionIntent, DocumentHighlight, InlayHint, Location, LocationLink, CodeAction, Completion, CompletionIntent, DocumentHighlight, InlayHint, Location, LocationLink,
PrepareRenameResponse, Project, ProjectItem, ProjectTransaction, TaskSourceKind, PrepareRenameResponse, Project, ProjectItem, ProjectTransaction, TaskSourceKind,
@ -6221,19 +6221,14 @@ impl Editor {
} }
fn render_context_menu_aside( fn render_context_menu_aside(
&self, &mut self,
style: &EditorStyle,
max_size: Size<Pixels>, max_size: Size<Pixels>,
window: &mut Window,
cx: &mut Context<Editor>, cx: &mut Context<Editor>,
) -> Option<AnyElement> { ) -> Option<AnyElement> {
self.context_menu.borrow().as_ref().and_then(|menu| { self.context_menu.borrow_mut().as_mut().and_then(|menu| {
if menu.visible() { if menu.visible() {
menu.render_aside( menu.render_aside(self, max_size, window, cx)
style,
max_size,
self.workspace.as_ref().map(|(w, _)| w.clone()),
cx,
)
} else { } else {
None None
} }
@ -14926,8 +14921,14 @@ impl Editor {
if !self.hover_state.focused(window, cx) { if !self.hover_state.focused(window, cx) {
hide_hover(self, cx); hide_hover(self, 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.hide_context_menu(window, cx);
}
self.discard_inline_completion(false, cx); self.discard_inline_completion(false, cx);
cx.emit(EditorEvent::Blurred); cx.emit(EditorEvent::Blurred);
cx.notify(); cx.notify();
@ -15674,7 +15675,7 @@ fn snippet_completions(
documentation: snippet documentation: snippet
.description .description
.clone() .clone()
.map(CompletionDocumentation::SingleLine), .map(|description| CompletionDocumentation::SingleLine(description.into())),
lsp_completion: lsp::CompletionItem { lsp_completion: lsp::CompletionItem {
label: snippet.prefix.first().unwrap().clone(), label: snippet.prefix.first().unwrap().clone(),
kind: Some(CompletionItemKind::SNIPPET), kind: Some(CompletionItemKind::SNIPPET),

View file

@ -3426,9 +3426,11 @@ impl EditorElement {
available_within_viewport.right - px(1.), available_within_viewport.right - px(1.),
MENU_ASIDE_MAX_WIDTH, MENU_ASIDE_MAX_WIDTH,
); );
let Some(mut aside) = let Some(mut aside) = self.render_context_menu_aside(
self.render_context_menu_aside(size(max_width, max_height - POPOVER_Y_PADDING), cx) size(max_width, max_height - POPOVER_Y_PADDING),
else { window,
cx,
) else {
return; return;
}; };
aside.layout_as_root(AvailableSpace::min_size(), window, cx); aside.layout_as_root(AvailableSpace::min_size(), window, cx);
@ -3450,7 +3452,7 @@ impl EditorElement {
), ),
) - POPOVER_Y_PADDING, ) - 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; return;
}; };
let actual_size = aside.layout_as_root(AvailableSpace::min_size(), window, cx); 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. // Skip drawing if it doesn't fit anywhere.
if let Some((aside, position)) = positioned_aside { 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( fn render_context_menu_aside(
&self, &self,
max_size: Size<Pixels>, max_size: Size<Pixels>,
window: &mut Window,
cx: &mut App, cx: &mut App,
) -> Option<AnyElement> { ) -> Option<AnyElement> {
if max_size.width < px(100.) || max_size.height < px(12.) { if max_size.width < px(100.) || max_size.height < px(12.) {
None None
} else { } else {
self.editor.update(cx, |editor, cx| { 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)
}) })
} }
} }

View file

@ -1,7 +1,7 @@
use crate::{ use crate::{
display_map::{invisibles::is_invisible, InlayOffset, ToDisplayPoint}, display_map::{invisibles::is_invisible, InlayOffset, ToDisplayPoint},
hover_links::{InlayHighlight, RangeInEditor}, hover_links::{InlayHighlight, RangeInEditor},
scroll::ScrollAmount, scroll::{Autoscroll, ScrollAmount},
Anchor, AnchorRangeExt, DisplayPoint, DisplayRow, Editor, EditorSettings, EditorSnapshot, Anchor, AnchorRangeExt, DisplayPoint, DisplayRow, Editor, EditorSettings, EditorSnapshot,
Hover, Hover,
}; };
@ -18,12 +18,14 @@ use markdown::{Markdown, MarkdownStyle};
use multi_buffer::ToOffset; use multi_buffer::ToOffset;
use project::{HoverBlock, HoverBlockKind, InlayHintLabelPart}; use project::{HoverBlock, HoverBlockKind, InlayHintLabelPart};
use settings::Settings; use settings::Settings;
use std::rc::Rc;
use std::{borrow::Cow, cell::RefCell}; use std::{borrow::Cow, cell::RefCell};
use std::{ops::Range, sync::Arc, time::Duration}; use std::{ops::Range, sync::Arc, time::Duration};
use std::{path::PathBuf, rc::Rc};
use theme::ThemeSettings; use theme::ThemeSettings;
use ui::{prelude::*, theme_is_transparent, Scrollbar, ScrollbarState}; use ui::{prelude::*, theme_is_transparent, Scrollbar, ScrollbarState};
use url::Url;
use util::TryFutureExt; use util::TryFutureExt;
use workspace::Workspace;
pub const HOVER_REQUEST_DELAY_MILLIS: u64 = 200; pub const HOVER_REQUEST_DELAY_MILLIS: u64 = 200;
pub const MIN_POPOVER_CHARACTER_WIDTH: f32 = 20.; pub const MIN_POPOVER_CHARACTER_WIDTH: f32 = 20.;
@ -356,7 +358,15 @@ fn show_hover(
}, },
..Default::default() ..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(); .ok();
@ -558,6 +568,23 @@ async fn parse_blocks(
let rendered_block = cx let rendered_block = cx
.new_window_entity(|window, cx| { .new_window_entity(|window, cx| {
Markdown::new(
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 settings = ThemeSettings::get_global(cx);
let ui_font_family = settings.ui_font.family.clone(); let ui_font_family = settings.ui_font.family.clone();
let ui_font_fallbacks = settings.ui_font.fallbacks.clone(); let ui_font_fallbacks = settings.ui_font.fallbacks.clone();
@ -571,8 +598,7 @@ async fn parse_blocks(
color: Some(cx.theme().colors().editor_foreground), color: Some(cx.theme().colors().editor_foreground),
..Default::default() ..Default::default()
}); });
MarkdownStyle {
let markdown_style = MarkdownStyle {
base_text_style, base_text_style,
code_block: StyleRefinement::default().my(rems(1.)).font_buffer(cx), code_block: StyleRefinement::default().my(rems(1.)).font_buffer(cx),
inline_code: TextStyleRefinement { inline_code: TextStyleRefinement {
@ -604,21 +630,58 @@ async fn parse_blocks(
.text_base() .text_base()
.mt(rems(1.)) .mt(rems(1.))
.mb_0(), .mb_0(),
}; }
}
Markdown::new( pub fn open_markdown_url(link: SharedString, window: &mut Window, cx: &mut App) {
combined_text, if let Ok(uri) = Url::parse(&link) {
markdown_style.clone(), if uri.scheme() == "file" {
Some(language_registry.clone()), if let Some(workspace) = window.root::<Workspace>().flatten() {
fallback_language_name, 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, window,
cx, cx,
) |selections| {
.copy_code_block_buttons(false) selections.select_ranges([text::Point::new(accum - 1, 0)
..text::Point::new(accum - 1, 0)]);
},
);
}) })
.ok(); })
.detach_and_log_err(cx);
rendered_block });
return;
}
}
}
cx.open_url(&link);
} }
#[derive(Default, Debug)] #[derive(Default, Debug)]

View file

@ -7,7 +7,6 @@ pub use crate::{
use crate::{ use crate::{
diagnostic_set::{DiagnosticEntry, DiagnosticGroup}, diagnostic_set::{DiagnosticEntry, DiagnosticGroup},
language_settings::{language_settings, LanguageSettings}, language_settings::{language_settings, LanguageSettings},
markdown::parse_markdown,
outline::OutlineItem, outline::OutlineItem,
syntax_map::{ syntax_map::{
SyntaxLayer, SyntaxMap, SyntaxMapCapture, SyntaxMapCaptures, SyntaxMapMatch, SyntaxLayer, SyntaxMap, SyntaxMapCapture, SyntaxMapCaptures, SyntaxMapMatch,
@ -231,50 +230,6 @@ pub struct Diagnostic {
pub data: Option<Value>, 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. /// An operation used to synchronize this buffer with its other replicas.
#[derive(Clone, Debug, PartialEq)] #[derive(Clone, Debug, PartialEq)]
pub enum Operation { pub enum Operation {

View file

@ -20,7 +20,6 @@ test-support = [
[dependencies] [dependencies]
anyhow.workspace = true anyhow.workspace = true
futures.workspace = true
gpui.workspace = true gpui.workspace = true
language.workspace = true language.workspace = true
linkify.workspace = true linkify.workspace = true
@ -34,7 +33,7 @@ util.workspace = true
assets.workspace = true assets.workspace = true
env_logger.workspace = true env_logger.workspace = true
gpui = { workspace = true, features = ["test-support"] } gpui = { workspace = true, features = ["test-support"] }
languages.workspace = true languages = { workspace = true, features = ["load-grammars"] }
node_runtime.workspace = true node_runtime.workspace = true
settings = { workspace = true, features = ["test-support"] } settings = { workspace = true, features = ["test-support"] }
util = { workspace = true, features = ["test-support"] } util = { workspace = true, features = ["test-support"] }

View file

@ -15,84 +15,12 @@ const MARKDOWN_EXAMPLE: &str = r#"
## Headings ## 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. 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
![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. 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( MarkdownExample::new(
MARKDOWN_EXAMPLE.to_string(), MARKDOWN_EXAMPLE.into(),
markdown_style, markdown_style,
language_registry, language_registry,
window, window,
@ -179,14 +107,22 @@ struct MarkdownExample {
impl MarkdownExample { impl MarkdownExample {
pub fn new( pub fn new(
text: String, text: SharedString,
style: MarkdownStyle, style: MarkdownStyle,
language_registry: Arc<LanguageRegistry>, language_registry: Arc<LanguageRegistry>,
window: &mut Window, window: &mut Window,
cx: &mut App, cx: &mut App,
) -> Self { ) -> Self {
let markdown = let markdown = cx.new(|cx| {
cx.new(|cx| Markdown::new(text, style, Some(language_registry), None, window, cx)); Markdown::new(
text,
style,
Some(language_registry),
Some("TypeScript".to_string()),
window,
cx,
)
});
Self { markdown } Self { markdown }
} }
} }

View file

@ -1,7 +1,6 @@
pub mod parser; pub mod parser;
use crate::parser::CodeBlockKind; use crate::parser::CodeBlockKind;
use futures::FutureExt;
use gpui::{ use gpui::{
actions, point, quad, AnyElement, App, Bounds, ClipboardItem, CursorStyle, DispatchPhase, actions, point, quad, AnyElement, App, Bounds, ClipboardItem, CursorStyle, DispatchPhase,
Edges, Entity, FocusHandle, Focusable, FontStyle, FontWeight, GlobalElementId, Hitbox, Hsla, Edges, Entity, FocusHandle, Focusable, FontStyle, FontWeight, GlobalElementId, Hitbox, Hsla,
@ -12,7 +11,7 @@ use gpui::{
use language::{Language, LanguageRegistry, Rope}; use language::{Language, LanguageRegistry, Rope};
use parser::{parse_links_only, parse_markdown, MarkdownEvent, MarkdownTag, MarkdownTagEnd}; 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 theme::SyntaxTheme;
use ui::{prelude::*, Tooltip}; use ui::{prelude::*, Tooltip};
use util::{ResultExt, TryFutureExt}; use util::{ResultExt, TryFutureExt};
@ -49,7 +48,7 @@ impl Default for MarkdownStyle {
} }
pub struct Markdown { pub struct Markdown {
source: String, source: SharedString,
selection: Selection, selection: Selection,
pressed_link: Option<RenderedLink>, pressed_link: Option<RenderedLink>,
autoscroll_request: Option<usize>, autoscroll_request: Option<usize>,
@ -60,6 +59,7 @@ pub struct Markdown {
focus_handle: FocusHandle, focus_handle: FocusHandle,
language_registry: Option<Arc<LanguageRegistry>>, language_registry: Option<Arc<LanguageRegistry>>,
fallback_code_block_language: Option<String>, fallback_code_block_language: Option<String>,
open_url: Option<Box<dyn Fn(SharedString, &mut Window, &mut App)>>,
options: Options, options: Options,
} }
@ -73,7 +73,7 @@ actions!(markdown, [Copy]);
impl Markdown { impl Markdown {
pub fn new( pub fn new(
source: String, source: SharedString,
style: MarkdownStyle, style: MarkdownStyle,
language_registry: Option<Arc<LanguageRegistry>>, language_registry: Option<Arc<LanguageRegistry>>,
fallback_code_block_language: Option<String>, fallback_code_block_language: Option<String>,
@ -97,13 +97,24 @@ impl Markdown {
parse_links_only: false, parse_links_only: false,
copy_code_block_buttons: true, copy_code_block_buttons: true,
}, },
open_url: None,
}; };
this.parse(window, cx); this.parse(window, cx);
this 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( pub fn new_text(
source: String, source: SharedString,
style: MarkdownStyle, style: MarkdownStyle,
language_registry: Option<Arc<LanguageRegistry>>, language_registry: Option<Arc<LanguageRegistry>>,
fallback_code_block_language: Option<String>, fallback_code_block_language: Option<String>,
@ -127,6 +138,7 @@ impl Markdown {
parse_links_only: true, parse_links_only: true,
copy_code_block_buttons: true, copy_code_block_buttons: true,
}, },
open_url: None,
}; };
this.parse(window, cx); this.parse(window, cx);
this this
@ -137,11 +149,11 @@ impl Markdown {
} }
pub fn append(&mut self, text: &str, window: &mut Window, cx: &mut Context<Self>) { 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); 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() { if source == self.source() {
return; return;
} }
@ -176,17 +188,38 @@ impl Markdown {
return; return;
} }
let text = self.source.clone(); let source = self.source.clone();
let parse_text_only = self.options.parse_links_only; 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 parsed = cx.background_spawn(async move {
let text = SharedString::from(text); if parse_text_only {
let events = match parse_text_only { return anyhow::Ok(ParsedMarkdown {
true => Arc::from(parse_links_only(text.as_ref())), events: Arc::from(parse_links_only(source.as_ref())),
false => Arc::from(parse_markdown(text.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 { anyhow::Ok(ParsedMarkdown {
source: text, source,
events, events: Arc::from(events),
languages,
}) })
}); });
@ -217,12 +250,7 @@ impl Markdown {
impl Render for Markdown { impl Render for Markdown {
fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement { fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
MarkdownElement::new( MarkdownElement::new(cx.entity().clone(), self.style.clone())
cx.entity().clone(),
self.style.clone(),
self.language_registry.clone(),
self.fallback_code_block_language.clone(),
)
} }
} }
@ -270,6 +298,7 @@ impl Selection {
pub struct ParsedMarkdown { pub struct ParsedMarkdown {
source: SharedString, source: SharedString,
events: Arc<[(Range<usize>, MarkdownEvent)]>, events: Arc<[(Range<usize>, MarkdownEvent)]>,
languages: HashMap<SharedString, Arc<Language>>,
} }
impl ParsedMarkdown { impl ParsedMarkdown {
@ -285,61 +314,11 @@ impl ParsedMarkdown {
pub struct MarkdownElement { pub struct MarkdownElement {
markdown: Entity<Markdown>, markdown: Entity<Markdown>,
style: MarkdownStyle, style: MarkdownStyle,
language_registry: Option<Arc<LanguageRegistry>>,
fallback_code_block_language: Option<String>,
} }
impl MarkdownElement { impl MarkdownElement {
fn new( fn new(markdown: Entity<Markdown>, style: MarkdownStyle) -> Self {
markdown: Entity<Markdown>, Self { markdown, style }
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 paint_selection( fn paint_selection(
@ -452,7 +431,7 @@ impl MarkdownElement {
pending: true, pending: true,
}; };
window.focus(&markdown.focus_handle); window.focus(&markdown.focus_handle);
window.prevent_default() window.prevent_default();
} }
cx.notify(); cx.notify();
@ -492,13 +471,17 @@ impl MarkdownElement {
}); });
self.on_mouse_event(window, cx, { self.on_mouse_event(window, cx, {
let rendered_text = rendered_text.clone(); let rendered_text = rendered_text.clone();
move |markdown, event: &MouseUpEvent, phase, _, cx| { move |markdown, event: &MouseUpEvent, phase, window, cx| {
if phase.bubble() { if phase.bubble() {
if let Some(pressed_link) = markdown.pressed_link.take() { if let Some(pressed_link) = markdown.pressed_link.take() {
if Some(&pressed_link) == rendered_text.link_for_position(event.position) { if Some(&pressed_link) == rendered_text.link_for_position(event.position) {
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); cx.open_url(&pressed_link.destination_url);
} }
} }
}
} else if markdown.selection.pending { } else if markdown.selection.pending {
markdown.selection.pending = false; markdown.selection.pending = false;
#[cfg(any(target_os = "linux", target_os = "freebsd"))] #[cfg(any(target_os = "linux", target_os = "freebsd"))]
@ -617,7 +600,7 @@ impl Element for MarkdownElement {
} }
MarkdownTag::CodeBlock(kind) => { MarkdownTag::CodeBlock(kind) => {
let language = if let CodeBlockKind::Fenced(language) = kind { let language = if let CodeBlockKind::Fenced(language) = kind {
self.load_language(language.as_ref(), window, cx) parsed_markdown.languages.get(language).cloned()
} else { } else {
None None
}; };

View file

@ -2,15 +2,16 @@ use gpui::SharedString;
use linkify::LinkFinder; use linkify::LinkFinder;
pub use pulldown_cmark::TagEnd as MarkdownTagEnd; pub use pulldown_cmark::TagEnd as MarkdownTagEnd;
use pulldown_cmark::{Alignment, HeadingLevel, LinkType, MetadataBlockKind, Options, Parser}; 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(); let mut options = Options::all();
options.remove(pulldown_cmark::Options::ENABLE_DEFINITION_LIST); options.remove(pulldown_cmark::Options::ENABLE_DEFINITION_LIST);
options.remove(pulldown_cmark::Options::ENABLE_YAML_STYLE_METADATA_BLOCKS); options.remove(pulldown_cmark::Options::ENABLE_YAML_STYLE_METADATA_BLOCKS);
options.remove(pulldown_cmark::Options::ENABLE_MATH); options.remove(pulldown_cmark::Options::ENABLE_MATH);
let mut events = Vec::new(); let mut events = Vec::new();
let mut languages = HashSet::new();
let mut within_link = false; let mut within_link = false;
let mut within_metadata = false; let mut within_metadata = false;
for (pulldown_event, mut range) in Parser::new_ext(text, options).into_offset_iter() { 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 { match tag {
pulldown_cmark::Tag::Link { .. } => within_link = true, pulldown_cmark::Tag::Link { .. } => within_link = true,
pulldown_cmark::Tag::MetadataBlock { .. } => within_metadata = 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()))) 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(_) => {} pulldown_cmark::Event::InlineMath(_) | pulldown_cmark::Event::DisplayMath(_) => {}
} }
} }
events (events, languages)
} }
pub fn parse_links_only(mut text: &str) -> Vec<(Range<usize>, MarkdownEvent)> { pub fn parse_links_only(mut text: &str) -> Vec<(Range<usize>, MarkdownEvent)> {

View file

@ -26,7 +26,8 @@ use futures::{
}; };
use globset::{Glob, GlobBuilder, GlobMatcher, GlobSet, GlobSetBuilder}; use globset::{Glob, GlobBuilder, GlobMatcher, GlobSet, GlobSetBuilder};
use gpui::{ 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 http_client::HttpClient;
use itertools::Itertools as _; use itertools::Itertools as _;
@ -34,13 +35,12 @@ use language::{
language_settings::{ language_settings::{
language_settings, FormatOnSave, Formatter, LanguageSettings, SelectedFormatter, 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}, proto::{deserialize_anchor, deserialize_version, serialize_anchor, serialize_version},
range_from_lsp, range_to_lsp, Bias, Buffer, BufferSnapshot, CachedLspAdapter, CodeLabel, range_from_lsp, range_to_lsp, Bias, Buffer, BufferSnapshot, CachedLspAdapter, CodeLabel,
CompletionDocumentation, Diagnostic, DiagnosticEntry, DiagnosticSet, Diff, File as _, Language, Diagnostic, DiagnosticEntry, DiagnosticSet, Diff, File as _, Language, LanguageRegistry,
LanguageRegistry, LanguageServerBinaryStatus, LanguageToolchainStore, LocalFile, LspAdapter, LanguageServerBinaryStatus, LanguageToolchainStore, LocalFile, LspAdapter, LspAdapterDelegate,
LspAdapterDelegate, Patch, PointUtf16, TextBufferSnapshot, ToOffset, ToPointUtf16, Transaction, Patch, PointUtf16, TextBufferSnapshot, ToOffset, ToPointUtf16, Transaction, Unclipped,
Unclipped,
}; };
use lsp::{ use lsp::{
notification::DidRenameFiles, CodeActionKind, CompletionContext, DiagnosticSeverity, notification::DidRenameFiles, CodeActionKind, CompletionContext, DiagnosticSeverity,
@ -4204,13 +4204,7 @@ impl LspStore {
cx.foreground_executor().spawn(async move { cx.foreground_executor().spawn(async move {
let completions = task.await?; let completions = task.await?;
let mut result = Vec::new(); let mut result = Vec::new();
populate_labels_for_completions( populate_labels_for_completions(completions, language, lsp_adapter, &mut result)
completions,
&language_registry,
language,
lsp_adapter,
&mut result,
)
.await; .await;
Ok(result) Ok(result)
}) })
@ -4260,7 +4254,6 @@ impl LspStore {
if let Ok(new_completions) = task.await { if let Ok(new_completions) = task.await {
populate_labels_for_completions( populate_labels_for_completions(
new_completions, new_completions,
&language_registry,
language.clone(), language.clone(),
lsp_adapter, lsp_adapter,
&mut completions, &mut completions,
@ -4284,7 +4277,6 @@ impl LspStore {
cx: &mut Context<Self>, cx: &mut Context<Self>,
) -> Task<Result<bool>> { ) -> Task<Result<bool>> {
let client = self.upstream_client(); let client = self.upstream_client();
let language_registry = self.languages.clone();
let buffer_id = buffer.read(cx).remote_id(); let buffer_id = buffer.read(cx).remote_id();
let buffer_snapshot = buffer.read(cx).snapshot(); let buffer_snapshot = buffer.read(cx).snapshot();
@ -4302,7 +4294,6 @@ impl LspStore {
completions.clone(), completions.clone(),
completion_index, completion_index,
client.clone(), client.clone(),
language_registry.clone(),
) )
.await .await
.log_err() .log_err()
@ -4343,7 +4334,6 @@ impl LspStore {
&buffer_snapshot, &buffer_snapshot,
completions.clone(), completions.clone(),
completion_index, completion_index,
language_registry.clone(),
) )
.await .await
.log_err(); .log_err();
@ -4419,22 +4409,14 @@ impl LspStore {
snapshot: &BufferSnapshot, snapshot: &BufferSnapshot,
completions: Rc<RefCell<Box<[Completion]>>>, completions: Rc<RefCell<Box<[Completion]>>>,
completion_index: usize, completion_index: usize,
language_registry: Arc<LanguageRegistry>,
) -> Result<()> { ) -> Result<()> {
let completion_item = completions.borrow()[completion_index] let completion_item = completions.borrow()[completion_index]
.lsp_completion .lsp_completion
.clone(); .clone();
if let Some(lsp_documentation) = completion_item.documentation.as_ref() { if let Some(lsp_documentation) = completion_item.documentation.clone() {
let documentation = language::prepare_completion_documentation(
lsp_documentation,
&language_registry,
snapshot.language().cloned(),
)
.await;
let mut completions = completions.borrow_mut(); let mut completions = completions.borrow_mut();
let completion = &mut completions[completion_index]; let completion = &mut completions[completion_index];
completion.documentation = Some(documentation); completion.documentation = Some(lsp_documentation.into());
} else { } else {
let mut completions = completions.borrow_mut(); let mut completions = completions.borrow_mut();
let completion = &mut completions[completion_index]; let completion = &mut completions[completion_index];
@ -4487,7 +4469,6 @@ impl LspStore {
completions: Rc<RefCell<Box<[Completion]>>>, completions: Rc<RefCell<Box<[Completion]>>>,
completion_index: usize, completion_index: usize,
client: AnyProtoClient, client: AnyProtoClient,
language_registry: Arc<LanguageRegistry>,
) -> Result<()> { ) -> Result<()> {
let lsp_completion = { let lsp_completion = {
let completion = &completions.borrow()[completion_index]; let completion = &completions.borrow()[completion_index];
@ -4514,14 +4495,11 @@ impl LspStore {
let documentation = if response.documentation.is_empty() { let documentation = if response.documentation.is_empty() {
CompletionDocumentation::Undocumented CompletionDocumentation::Undocumented
} else if response.documentation_is_markdown { } else if response.documentation_is_markdown {
CompletionDocumentation::MultiLineMarkdown( CompletionDocumentation::MultiLineMarkdown(response.documentation.into())
markdown::parse_markdown(&response.documentation, Some(&language_registry), None)
.await,
)
} else if response.documentation.lines().count() <= 1 { } else if response.documentation.lines().count() <= 1 {
CompletionDocumentation::SingleLine(response.documentation) CompletionDocumentation::SingleLine(response.documentation.into())
} else { } else {
CompletionDocumentation::MultiLinePlainText(response.documentation) CompletionDocumentation::MultiLinePlainText(response.documentation.into())
}; };
let mut completions = completions.borrow_mut(); 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( async fn populate_labels_for_completions(
mut new_completions: Vec<CoreCompletion>, mut new_completions: Vec<CoreCompletion>,
language_registry: &Arc<LanguageRegistry>,
language: Option<Arc<Language>>, language: Option<Arc<Language>>,
lsp_adapter: Option<Arc<CachedLspAdapter>>, lsp_adapter: Option<Arc<CachedLspAdapter>>,
completions: &mut Vec<Completion>, completions: &mut Vec<Completion>,
@ -8085,8 +8062,8 @@ async fn populate_labels_for_completions(
.zip(lsp_completions) .zip(lsp_completions)
.zip(labels.into_iter().chain(iter::repeat(None))) .zip(labels.into_iter().chain(iter::repeat(None)))
{ {
let documentation = if let Some(docs) = &lsp_completion.documentation { let documentation = if let Some(docs) = lsp_completion.documentation.clone() {
Some(prepare_completion_documentation(docs, language_registry, language.clone()).await) Some(docs.into())
} else { } else {
None 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 { fn glob_literal_prefix(glob: &Path) -> PathBuf {
glob.components() glob.components()
.take_while(|component| match component { .take_while(|component| match component {

View file

@ -58,15 +58,15 @@ use gpui::{
use itertools::Itertools; use itertools::Itertools;
use language::{ use language::{
language_settings::InlayHintKind, proto::split_operations, Buffer, BufferEvent, Capability, language_settings::InlayHintKind, proto::split_operations, Buffer, BufferEvent, Capability,
CodeLabel, CompletionDocumentation, File as _, Language, LanguageName, LanguageRegistry, CodeLabel, File as _, Language, LanguageName, LanguageRegistry, PointUtf16, ToOffset,
PointUtf16, ToOffset, ToPointUtf16, Toolchain, ToolchainList, Transaction, Unclipped, ToPointUtf16, Toolchain, ToolchainList, Transaction, Unclipped,
}; };
use lsp::{ use lsp::{
CodeActionKind, CompletionContext, CompletionItemKind, DocumentHighlightKind, LanguageServerId, CodeActionKind, CompletionContext, CompletionItemKind, DocumentHighlightKind, LanguageServerId,
LanguageServerName, MessageActionItem, LanguageServerName, MessageActionItem,
}; };
use lsp_command::*; use lsp_command::*;
use lsp_store::{LspFormatTarget, OpenLspBufferHandle}; use lsp_store::{CompletionDocumentation, LspFormatTarget, OpenLspBufferHandle};
use node_runtime::NodeRuntime; use node_runtime::NodeRuntime;
use parking_lot::Mutex; use parking_lot::Mutex;
pub use prettier_store::PrettierStore; pub use prettier_store::PrettierStore;

View file

@ -208,7 +208,7 @@ impl SshPrompt {
..Default::default() ..Default::default()
}; };
let markdown = 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.prompt = Some((markdown, tx));
self.status_message.take(); self.status_message.take();
window.focus(&self.editor.focus_handle(cx)); window.focus(&self.editor.focus_handle(cx));

View file

@ -1,7 +1,7 @@
use gpui::{ use gpui::{
div, App, AppContext as _, Context, Entity, EventEmitter, FocusHandle, Focusable, FontWeight, div, App, AppContext as _, Context, Entity, EventEmitter, FocusHandle, Focusable, FontWeight,
InteractiveElement, IntoElement, ParentElement, PromptHandle, PromptLevel, PromptResponse, InteractiveElement, IntoElement, ParentElement, PromptHandle, PromptLevel, PromptResponse,
Refineable, Render, RenderablePromptHandle, Styled, TextStyleRefinement, Window, Refineable, Render, RenderablePromptHandle, SharedString, Styled, TextStyleRefinement, Window,
}; };
use markdown::{Markdown, MarkdownStyle}; use markdown::{Markdown, MarkdownStyle};
use settings::Settings; use settings::Settings;
@ -48,7 +48,14 @@ pub fn fallback_prompt_renderer(
selection_background_color: { cx.theme().players().local().selection }, selection_background_color: { cx.theme().players().local().selection },
..Default::default() ..Default::default()
}; };
Markdown::new(text.to_string(), markdown_style, None, None, window, cx) Markdown::new(
SharedString::new(text),
markdown_style,
None,
None,
window,
cx,
)
}) })
}), }),
} }