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,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>,
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue