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:
parent
ebbc6a9752
commit
1678e3cbf1
16 changed files with 353 additions and 352 deletions
1
Cargo.lock
generated
1
Cargo.lock
generated
|
@ -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,
|
||||
)
|
||||
})
|
||||
}),
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue