From e802c072f7e3e32503355e44cbaa32b5e382e14d Mon Sep 17 00:00:00 2001 From: Julia Date: Tue, 5 Sep 2023 22:16:12 -0400 Subject: [PATCH 01/30] Start hacking in autocomplete docs --- crates/editor/src/editor.rs | 91 ++++--- crates/editor/src/hover_popover.rs | 392 ++++++++++++++++++++++++----- crates/theme/src/theme.rs | 8 +- styles/src/style_tree/editor.ts | 8 +- 4 files changed, 382 insertions(+), 117 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 24ffa64a6a..b1c5e35703 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -859,7 +859,6 @@ struct CompletionsMenu { id: CompletionId, initial_position: Anchor, buffer: ModelHandle, - project: Option>, completions: Arc<[Completion]>, match_candidates: Vec, matches: Arc<[StringMatch]>, @@ -903,42 +902,17 @@ impl CompletionsMenu { fn render(&self, style: EditorStyle, cx: &mut ViewContext) -> AnyElement { enum CompletionTag {} - let language_servers = self.project.as_ref().map(|project| { - project - .read(cx) - .language_servers_for_buffer(self.buffer.read(cx), cx) - .filter(|(_, server)| server.capabilities().completion_provider.is_some()) - .map(|(adapter, server)| (server.server_id(), adapter.short_name)) - .collect::>() - }); - let needs_server_name = language_servers - .as_ref() - .map_or(false, |servers| servers.len() > 1); - - let get_server_name = - move |lookup_server_id: lsp::LanguageServerId| -> Option<&'static str> { - language_servers - .iter() - .flatten() - .find_map(|(server_id, server_name)| { - if *server_id == lookup_server_id { - Some(*server_name) - } else { - None - } - }) - }; - let widest_completion_ix = self .matches .iter() .enumerate() .max_by_key(|(_, mat)| { let completion = &self.completions[mat.candidate_id]; - let mut len = completion.label.text.chars().count(); + let documentation = &completion.lsp_completion.documentation; - if let Some(server_name) = get_server_name(completion.server_id) { - len += server_name.chars().count(); + let mut len = completion.label.text.chars().count(); + if let Some(lsp::Documentation::String(text)) = documentation { + len += text.chars().count(); } len @@ -948,8 +922,16 @@ impl CompletionsMenu { let completions = self.completions.clone(); let matches = self.matches.clone(); let selected_item = self.selected_item; - let container_style = style.autocomplete.container; - UniformList::new( + + let alongside_docs_text_style = TextStyle { + soft_wrap: true, + ..style.text.clone() + }; + let alongside_docs_width = style.autocomplete.alongside_docs_width; + let alongside_docs_container_style = style.autocomplete.alongside_docs_container; + let outer_container_style = style.autocomplete.container; + + let list = UniformList::new( self.list.clone(), matches.len(), cx, @@ -957,7 +939,9 @@ impl CompletionsMenu { let start_ix = range.start; for (ix, mat) in matches[range].iter().enumerate() { let completion = &completions[mat.candidate_id]; + let documentation = &completion.lsp_completion.documentation; let item_ix = start_ix + ix; + items.push( MouseEventHandler::new::( mat.candidate_id, @@ -986,22 +970,18 @@ impl CompletionsMenu { ), ); - if let Some(server_name) = get_server_name(completion.server_id) { + if let Some(lsp::Documentation::String(text)) = documentation { Flex::row() .with_child(completion_label) .with_children((|| { - if !needs_server_name { - return None; - } - let text_style = TextStyle { - color: style.autocomplete.server_name_color, + color: style.autocomplete.inline_docs_color, font_size: style.text.font_size - * style.autocomplete.server_name_size_percent, + * style.autocomplete.inline_docs_size_percent, ..style.text.clone() }; - let label = Text::new(server_name, text_style) + let label = Text::new(text.clone(), text_style) .aligned() .constrained() .dynamically(move |constraint, _, _| { @@ -1021,7 +1001,7 @@ impl CompletionsMenu { .with_style( style .autocomplete - .server_name_container, + .inline_docs_container, ) .into_any(), ) @@ -1065,10 +1045,29 @@ impl CompletionsMenu { } }, ) - .with_width_from_item(widest_completion_ix) - .contained() - .with_style(container_style) - .into_any() + .with_width_from_item(widest_completion_ix); + + Flex::row() + .with_child(list) + .with_children({ + let completion = &self.completions[selected_item]; + let documentation = &completion.lsp_completion.documentation; + + if let Some(lsp::Documentation::MarkupContent(content)) = documentation { + Some( + Text::new(content.value.clone(), alongside_docs_text_style) + .constrained() + .with_width(alongside_docs_width) + .contained() + .with_style(alongside_docs_container_style), + ) + } else { + None + } + }) + .contained() + .with_style(outer_container_style) + .into_any() } pub async fn filter(&mut self, query: Option<&str>, executor: Arc) { @@ -3150,7 +3149,6 @@ impl Editor { }); let id = post_inc(&mut self.next_completion_id); - let project = self.project.clone(); let task = cx.spawn(|this, mut cx| { async move { let menu = if let Some(completions) = completions.await.log_err() { @@ -3169,7 +3167,6 @@ impl Editor { }) .collect(), buffer, - project, completions: completions.into(), matches: Vec::new().into(), selected_item: 0, diff --git a/crates/editor/src/hover_popover.rs b/crates/editor/src/hover_popover.rs index 553cb321c3..69b5562c34 100644 --- a/crates/editor/src/hover_popover.rs +++ b/crates/editor/src/hover_popover.rs @@ -1,6 +1,6 @@ use crate::{ display_map::{InlayOffset, ToDisplayPoint}, - link_go_to_definition::{InlayHighlight, RangeInEditor}, + link_go_to_definition::{DocumentRange, InlayRange}, Anchor, AnchorRangeExt, DisplayPoint, Editor, EditorSettings, EditorSnapshot, EditorStyle, ExcerptId, RangeToAnchorExt, }; @@ -8,12 +8,12 @@ use futures::FutureExt; use gpui::{ actions, elements::{Flex, MouseEventHandler, Padding, ParentElement, Text}, + fonts::{HighlightStyle, Underline, Weight}, platform::{CursorStyle, MouseButton}, - AnyElement, AppContext, Element, ModelHandle, Task, ViewContext, + AnyElement, AppContext, CursorRegion, Element, ModelHandle, MouseRegion, Task, ViewContext, }; use language::{Bias, DiagnosticEntry, DiagnosticSeverity, Language, LanguageRegistry}; use project::{HoverBlock, HoverBlockKind, InlayHintLabelPart, Project}; -use rich_text::{new_paragraph, render_code, render_markdown_mut, RichText}; use std::{ops::Range, sync::Arc, time::Duration}; use util::TryFutureExt; @@ -50,18 +50,19 @@ pub fn hover_at(editor: &mut Editor, point: Option, cx: &mut ViewC pub struct InlayHover { pub excerpt: ExcerptId, - pub range: InlayHighlight, + pub triggered_from: InlayOffset, + pub range: InlayRange, pub tooltip: HoverBlock, } pub fn find_hovered_hint_part( label_parts: Vec, - hint_start: InlayOffset, + hint_range: Range, hovered_offset: InlayOffset, ) -> Option<(InlayHintLabelPart, Range)> { - if hovered_offset >= hint_start { - let mut hovered_character = (hovered_offset - hint_start).0; - let mut part_start = hint_start; + if hovered_offset >= hint_range.start && hovered_offset <= hint_range.end { + let mut hovered_character = (hovered_offset - hint_range.start).0; + let mut part_start = hint_range.start; for part in label_parts { let part_len = part.value.chars().count(); if hovered_character > part_len { @@ -87,8 +88,10 @@ pub fn hover_at_inlay(editor: &mut Editor, inlay_hover: InlayHover, cx: &mut Vie }; if let Some(InfoPopover { symbol_range, .. }) = &editor.hover_state.info_popover { - if let RangeInEditor::Inlay(range) = symbol_range { - if range == &inlay_hover.range { + if let DocumentRange::Inlay(range) = symbol_range { + if (range.highlight_start..range.highlight_end) + .contains(&inlay_hover.triggered_from) + { // Hover triggered from same location as last time. Don't show again. return; } @@ -96,6 +99,18 @@ pub fn hover_at_inlay(editor: &mut Editor, inlay_hover: InlayHover, cx: &mut Vie hide_hover(editor, cx); } + let snapshot = editor.snapshot(cx); + // Don't request again if the location is the same as the previous request + if let Some(triggered_from) = editor.hover_state.triggered_from { + if inlay_hover.triggered_from + == snapshot + .display_snapshot + .anchor_to_inlay_offset(triggered_from) + { + return; + } + } + let task = cx.spawn(|this, mut cx| { async move { cx.background() @@ -107,7 +122,7 @@ pub fn hover_at_inlay(editor: &mut Editor, inlay_hover: InlayHover, cx: &mut Vie let hover_popover = InfoPopover { project: project.clone(), - symbol_range: RangeInEditor::Inlay(inlay_hover.range.clone()), + symbol_range: DocumentRange::Inlay(inlay_hover.range), blocks: vec![inlay_hover.tooltip], language: None, rendered_content: None, @@ -311,7 +326,7 @@ fn show_hover( Some(InfoPopover { project: project.clone(), - symbol_range: RangeInEditor::Text(range), + symbol_range: DocumentRange::Text(range), blocks: hover_result.contents, language: hover_result.language, rendered_content: None, @@ -346,43 +361,237 @@ fn show_hover( } fn render_blocks( + theme_id: usize, blocks: &[HoverBlock], language_registry: &Arc, language: Option<&Arc>, -) -> RichText { - let mut data = RichText { - text: Default::default(), - highlights: Default::default(), - region_ranges: Default::default(), - regions: Default::default(), - }; + style: &EditorStyle, +) -> RenderedInfo { + let mut text = String::new(); + let mut highlights = Vec::new(); + let mut region_ranges = Vec::new(); + let mut regions = Vec::new(); for block in blocks { match &block.kind { HoverBlockKind::PlainText => { - new_paragraph(&mut data.text, &mut Vec::new()); - data.text.push_str(&block.text); + new_paragraph(&mut text, &mut Vec::new()); + text.push_str(&block.text); } + HoverBlockKind::Markdown => { - render_markdown_mut(&block.text, language_registry, language, &mut data) + use pulldown_cmark::{CodeBlockKind, Event, Options, Parser, Tag}; + + let mut bold_depth = 0; + let mut italic_depth = 0; + let mut link_url = None; + let mut current_language = None; + let mut list_stack = Vec::new(); + + for event in Parser::new_ext(&block.text, Options::all()) { + let prev_len = text.len(); + match event { + Event::Text(t) => { + if let Some(language) = ¤t_language { + render_code( + &mut text, + &mut highlights, + t.as_ref(), + language, + style, + ); + } else { + text.push_str(t.as_ref()); + + let mut style = HighlightStyle::default(); + if bold_depth > 0 { + style.weight = Some(Weight::BOLD); + } + if italic_depth > 0 { + style.italic = Some(true); + } + if let Some(link_url) = link_url.clone() { + region_ranges.push(prev_len..text.len()); + regions.push(RenderedRegion { + link_url: Some(link_url), + code: false, + }); + style.underline = Some(Underline { + thickness: 1.0.into(), + ..Default::default() + }); + } + + if style != HighlightStyle::default() { + let mut new_highlight = true; + if let Some((last_range, last_style)) = highlights.last_mut() { + if last_range.end == prev_len && last_style == &style { + last_range.end = text.len(); + new_highlight = false; + } + } + if new_highlight { + highlights.push((prev_len..text.len(), style)); + } + } + } + } + + Event::Code(t) => { + text.push_str(t.as_ref()); + region_ranges.push(prev_len..text.len()); + if link_url.is_some() { + highlights.push(( + prev_len..text.len(), + HighlightStyle { + underline: Some(Underline { + thickness: 1.0.into(), + ..Default::default() + }), + ..Default::default() + }, + )); + } + regions.push(RenderedRegion { + code: true, + link_url: link_url.clone(), + }); + } + + Event::Start(tag) => match tag { + Tag::Paragraph => new_paragraph(&mut text, &mut list_stack), + + Tag::Heading(_, _, _) => { + new_paragraph(&mut text, &mut list_stack); + bold_depth += 1; + } + + Tag::CodeBlock(kind) => { + new_paragraph(&mut text, &mut list_stack); + current_language = if let CodeBlockKind::Fenced(language) = kind { + language_registry + .language_for_name(language.as_ref()) + .now_or_never() + .and_then(Result::ok) + } else { + language.cloned() + } + } + + Tag::Emphasis => italic_depth += 1, + + Tag::Strong => bold_depth += 1, + + Tag::Link(_, url, _) => link_url = Some(url.to_string()), + + Tag::List(number) => { + list_stack.push((number, false)); + } + + Tag::Item => { + let len = list_stack.len(); + if let Some((list_number, has_content)) = list_stack.last_mut() { + *has_content = false; + if !text.is_empty() && !text.ends_with('\n') { + text.push('\n'); + } + for _ in 0..len - 1 { + text.push_str(" "); + } + if let Some(number) = list_number { + text.push_str(&format!("{}. ", number)); + *number += 1; + *has_content = false; + } else { + text.push_str("- "); + } + } + } + + _ => {} + }, + + Event::End(tag) => match tag { + Tag::Heading(_, _, _) => bold_depth -= 1, + Tag::CodeBlock(_) => current_language = None, + Tag::Emphasis => italic_depth -= 1, + Tag::Strong => bold_depth -= 1, + Tag::Link(_, _, _) => link_url = None, + Tag::List(_) => drop(list_stack.pop()), + _ => {} + }, + + Event::HardBreak => text.push('\n'), + + Event::SoftBreak => text.push(' '), + + _ => {} + } + } } + HoverBlockKind::Code { language } => { if let Some(language) = language_registry .language_for_name(language) .now_or_never() .and_then(Result::ok) { - render_code(&mut data.text, &mut data.highlights, &block.text, &language); + render_code(&mut text, &mut highlights, &block.text, &language, style); } else { - data.text.push_str(&block.text); + text.push_str(&block.text); } } } } - data.text = data.text.trim().to_string(); + RenderedInfo { + theme_id, + text: text.trim().to_string(), + highlights, + region_ranges, + regions, + } +} - data +fn render_code( + text: &mut String, + highlights: &mut Vec<(Range, HighlightStyle)>, + content: &str, + language: &Arc, + style: &EditorStyle, +) { + let prev_len = text.len(); + text.push_str(content); + for (range, highlight_id) in language.highlight_text(&content.into(), 0..content.len()) { + if let Some(style) = highlight_id.style(&style.syntax) { + highlights.push((prev_len + range.start..prev_len + range.end, style)); + } + } +} + +fn new_paragraph(text: &mut String, list_stack: &mut Vec<(Option, bool)>) { + let mut is_subsequent_paragraph_of_list = false; + if let Some((_, has_content)) = list_stack.last_mut() { + if *has_content { + is_subsequent_paragraph_of_list = true; + } else { + *has_content = true; + return; + } + } + + if !text.is_empty() { + if !text.ends_with('\n') { + text.push('\n'); + } + text.push('\n'); + } + for _ in 0..list_stack.len().saturating_sub(1) { + text.push_str(" "); + } + if is_subsequent_paragraph_of_list { + text.push_str(" "); + } } #[derive(Default)] @@ -415,8 +624,8 @@ impl HoverState { self.info_popover .as_ref() .map(|info_popover| match &info_popover.symbol_range { - RangeInEditor::Text(range) => &range.start, - RangeInEditor::Inlay(range) => &range.inlay_position, + DocumentRange::Text(range) => &range.start, + DocumentRange::Inlay(range) => &range.inlay_position, }) })?; let point = anchor.to_display_point(&snapshot.display_snapshot); @@ -442,10 +651,25 @@ impl HoverState { #[derive(Debug, Clone)] pub struct InfoPopover { pub project: ModelHandle, - symbol_range: RangeInEditor, + symbol_range: DocumentRange, pub blocks: Vec, language: Option>, - rendered_content: Option, + rendered_content: Option, +} + +#[derive(Debug, Clone)] +struct RenderedInfo { + theme_id: usize, + text: String, + highlights: Vec<(Range, HighlightStyle)>, + region_ranges: Vec>, + regions: Vec, +} + +#[derive(Debug, Clone)] +struct RenderedRegion { + code: bool, + link_url: Option, } impl InfoPopover { @@ -454,24 +678,63 @@ impl InfoPopover { style: &EditorStyle, cx: &mut ViewContext, ) -> AnyElement { + if let Some(rendered) = &self.rendered_content { + if rendered.theme_id != style.theme_id { + self.rendered_content = None; + } + } + let rendered_content = self.rendered_content.get_or_insert_with(|| { render_blocks( + style.theme_id, &self.blocks, self.project.read(cx).languages(), self.language.as_ref(), + style, ) }); - MouseEventHandler::new::(0, cx, move |_, cx| { + MouseEventHandler::new::(0, cx, |_, cx| { + let mut region_id = 0; + let view_id = cx.view_id(); + let code_span_background_color = style.document_highlight_read_background; + let regions = rendered_content.regions.clone(); Flex::column() .scrollable::(1, None, cx) - .with_child(rendered_content.element( - style.syntax.clone(), - style.text.clone(), - code_span_background_color, - cx, - )) + .with_child( + Text::new(rendered_content.text.clone(), style.text.clone()) + .with_highlights(rendered_content.highlights.clone()) + .with_custom_runs( + rendered_content.region_ranges.clone(), + move |ix, bounds, scene, _| { + region_id += 1; + let region = regions[ix].clone(); + if let Some(url) = region.link_url { + scene.push_cursor_region(CursorRegion { + bounds, + style: CursorStyle::PointingHand, + }); + scene.push_mouse_region( + MouseRegion::new::(view_id, region_id, bounds) + .on_click::( + MouseButton::Left, + move |_, _, cx| cx.platform().open_url(&url), + ), + ); + } + if region.code { + scene.push_quad(gpui::Quad { + bounds, + background: Some(code_span_background_color), + border: Default::default(), + corner_radii: (2.0).into(), + }); + } + }, + ) + .with_soft_wrap(true), + ) .contained() .with_style(style.hover_popover.container) }) @@ -564,15 +827,13 @@ mod tests { inlay_hint_cache::tests::{cached_hint_labels, visible_hint_labels}, link_go_to_definition::update_inlay_link_and_hover_points, test::editor_lsp_test_context::EditorLspTestContext, - InlayId, }; use collections::BTreeSet; - use gpui::fonts::{HighlightStyle, Underline, Weight}; + use gpui::fonts::Weight; use indoc::indoc; use language::{language_settings::InlayHintSettings, Diagnostic, DiagnosticSet}; use lsp::LanguageServerId; use project::{HoverBlock, HoverBlockKind}; - use rich_text::Highlight; use smol::stream::StreamExt; use unindent::Unindent; use util::test::marked_text_ranges; @@ -783,7 +1044,7 @@ mod tests { .await; cx.condition(|editor, _| editor.hover_state.visible()).await; - cx.editor(|editor, _| { + cx.editor(|editor, cx| { let blocks = editor.hover_state.info_popover.clone().unwrap().blocks; assert_eq!( blocks, @@ -793,7 +1054,8 @@ mod tests { }], ); - let rendered = render_blocks(&blocks, &Default::default(), None); + let style = editor.style(cx); + let rendered = render_blocks(0, &blocks, &Default::default(), None, &style); assert_eq!( rendered.text, code_str.trim(), @@ -985,7 +1247,7 @@ mod tests { expected_styles, } in &rows[0..] { - let rendered = render_blocks(&blocks, &Default::default(), None); + let rendered = render_blocks(0, &blocks, &Default::default(), None, &style); let (expected_text, ranges) = marked_text_ranges(expected_marked_text, false); let expected_highlights = ranges @@ -996,21 +1258,8 @@ mod tests { rendered.text, expected_text, "wrong text for input {blocks:?}" ); - - let rendered_highlights: Vec<_> = rendered - .highlights - .iter() - .filter_map(|(range, highlight)| { - let style = match highlight { - Highlight::Id(id) => id.style(&style.syntax)?, - Highlight::Highlight(style) => style.clone(), - }; - Some((range.clone(), style)) - }) - .collect(); - assert_eq!( - rendered_highlights, expected_highlights, + rendered.highlights, expected_highlights, "wrong highlights for input {blocks:?}" ); } @@ -1244,16 +1493,25 @@ mod tests { .advance_clock(Duration::from_millis(HOVER_DELAY_MILLIS + 100)); cx.foreground().run_until_parked(); cx.update_editor(|editor, cx| { + let snapshot = editor.snapshot(cx); let hover_state = &editor.hover_state; assert!(hover_state.diagnostic_popover.is_none() && hover_state.info_popover.is_some()); let popover = hover_state.info_popover.as_ref().unwrap(); let buffer_snapshot = editor.buffer().update(cx, |buffer, cx| buffer.snapshot(cx)); + let entire_inlay_start = snapshot.display_point_to_inlay_offset( + inlay_range.start.to_display_point(&snapshot), + Bias::Left, + ); + + let expected_new_type_label_start = InlayOffset(entire_inlay_start.0 + ": ".len()); assert_eq!( popover.symbol_range, - RangeInEditor::Inlay(InlayHighlight { - inlay: InlayId::Hint(0), + DocumentRange::Inlay(InlayRange { inlay_position: buffer_snapshot.anchor_at(inlay_range.start, Bias::Right), - range: ": ".len()..": ".len() + new_type_label.len(), + highlight_start: expected_new_type_label_start, + highlight_end: InlayOffset( + expected_new_type_label_start.0 + new_type_label.len() + ), }), "Popover range should match the new type label part" ); @@ -1301,17 +1559,23 @@ mod tests { .advance_clock(Duration::from_millis(HOVER_DELAY_MILLIS + 100)); cx.foreground().run_until_parked(); cx.update_editor(|editor, cx| { + let snapshot = editor.snapshot(cx); let hover_state = &editor.hover_state; assert!(hover_state.diagnostic_popover.is_none() && hover_state.info_popover.is_some()); let popover = hover_state.info_popover.as_ref().unwrap(); let buffer_snapshot = editor.buffer().update(cx, |buffer, cx| buffer.snapshot(cx)); + let entire_inlay_start = snapshot.display_point_to_inlay_offset( + inlay_range.start.to_display_point(&snapshot), + Bias::Left, + ); + let expected_struct_label_start = + InlayOffset(entire_inlay_start.0 + ": ".len() + new_type_label.len() + "<".len()); assert_eq!( popover.symbol_range, - RangeInEditor::Inlay(InlayHighlight { - inlay: InlayId::Hint(0), + DocumentRange::Inlay(InlayRange { inlay_position: buffer_snapshot.anchor_at(inlay_range.start, Bias::Right), - range: ": ".len() + new_type_label.len() + "<".len() - ..": ".len() + new_type_label.len() + "<".len() + struct_label.len(), + highlight_start: expected_struct_label_start, + highlight_end: InlayOffset(expected_struct_label_start.0 + struct_label.len()), }), "Popover range should match the struct label part" ); diff --git a/crates/theme/src/theme.rs b/crates/theme/src/theme.rs index e534ba4260..9f7530ec18 100644 --- a/crates/theme/src/theme.rs +++ b/crates/theme/src/theme.rs @@ -867,9 +867,11 @@ pub struct AutocompleteStyle { pub selected_item: ContainerStyle, pub hovered_item: ContainerStyle, pub match_highlight: HighlightStyle, - pub server_name_container: ContainerStyle, - pub server_name_color: Color, - pub server_name_size_percent: f32, + pub inline_docs_container: ContainerStyle, + pub inline_docs_color: Color, + pub inline_docs_size_percent: f32, + pub alongside_docs_width: f32, + pub alongside_docs_container: ContainerStyle, } #[derive(Clone, Copy, Default, Deserialize, JsonSchema)] diff --git a/styles/src/style_tree/editor.ts b/styles/src/style_tree/editor.ts index e55a73c365..37d6c4ea1e 100644 --- a/styles/src/style_tree/editor.ts +++ b/styles/src/style_tree/editor.ts @@ -206,9 +206,11 @@ export default function editor(): any { match_highlight: foreground(theme.middle, "accent", "active"), background: background(theme.middle, "active"), }, - server_name_container: { padding: { left: 40 } }, - server_name_color: text(theme.middle, "sans", "disabled", {}).color, - server_name_size_percent: 0.75, + inline_docs_container: { padding: { left: 40 } }, + inline_docs_color: text(theme.middle, "sans", "disabled", {}).color, + inline_docs_size_percent: 0.75, + alongside_docs_width: 400, + alongside_docs_container: { padding: autocomplete_item.padding } }, diagnostic_header: { background: background(theme.middle), From 1584dae9c211c65120825314e4a6b5fbda39cd1d Mon Sep 17 00:00:00 2001 From: Julia Date: Tue, 5 Sep 2023 22:23:16 -0400 Subject: [PATCH 02/30] Actually display the correct completion's doc --- crates/editor/src/editor.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index b1c5e35703..ba2b50c1c1 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -1050,7 +1050,8 @@ impl CompletionsMenu { Flex::row() .with_child(list) .with_children({ - let completion = &self.completions[selected_item]; + let mat = &self.matches[selected_item]; + let completion = &self.completions[mat.candidate_id]; let documentation = &completion.lsp_completion.documentation; if let Some(lsp::Documentation::MarkupContent(content)) = documentation { From 370a3cafd0381b088ca1ac2122ecea968c7f9839 Mon Sep 17 00:00:00 2001 From: Julia Date: Thu, 14 Sep 2023 15:24:46 -0400 Subject: [PATCH 03/30] Add markdown rendering to alongside completion docs --- crates/editor/src/editor.rs | 51 ++++--- crates/editor/src/markdown.rs | 246 ++++++++++++++++++++++++++++++++ styles/src/style_tree/editor.ts | 2 +- 3 files changed, 280 insertions(+), 19 deletions(-) create mode 100644 crates/editor/src/markdown.rs diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index ba2b50c1c1..2035bd35f0 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -9,6 +9,7 @@ mod highlight_matching_bracket; mod hover_popover; pub mod items; mod link_go_to_definition; +mod markdown; mod mouse_context_menu; pub mod movement; pub mod multi_buffer; @@ -845,11 +846,12 @@ impl ContextMenu { fn render( &self, cursor_position: DisplayPoint, + editor: &Editor, style: EditorStyle, cx: &mut ViewContext, ) -> (DisplayPoint, AnyElement) { match self { - ContextMenu::Completions(menu) => (cursor_position, menu.render(style, cx)), + ContextMenu::Completions(menu) => (cursor_position, menu.render(editor, style, cx)), ContextMenu::CodeActions(menu) => menu.render(cursor_position, style, cx), } } @@ -899,7 +901,12 @@ impl CompletionsMenu { !self.matches.is_empty() } - fn render(&self, style: EditorStyle, cx: &mut ViewContext) -> AnyElement { + fn render( + &self, + editor: &Editor, + style: EditorStyle, + cx: &mut ViewContext, + ) -> AnyElement { enum CompletionTag {} let widest_completion_ix = self @@ -923,18 +930,12 @@ impl CompletionsMenu { let matches = self.matches.clone(); let selected_item = self.selected_item; - let alongside_docs_text_style = TextStyle { - soft_wrap: true, - ..style.text.clone() - }; let alongside_docs_width = style.autocomplete.alongside_docs_width; let alongside_docs_container_style = style.autocomplete.alongside_docs_container; let outer_container_style = style.autocomplete.container; - let list = UniformList::new( - self.list.clone(), - matches.len(), - cx, + let list = UniformList::new(self.list.clone(), matches.len(), cx, { + let style = style.clone(); move |_, range, items, cx| { let start_ix = range.start; for (ix, mat) in matches[range].iter().enumerate() { @@ -1043,8 +1044,8 @@ impl CompletionsMenu { .into_any(), ); } - }, - ) + } + }) .with_width_from_item(widest_completion_ix); Flex::row() @@ -1055,12 +1056,26 @@ impl CompletionsMenu { let documentation = &completion.lsp_completion.documentation; if let Some(lsp::Documentation::MarkupContent(content)) = documentation { + let registry = editor + .project + .as_ref() + .unwrap() + .read(cx) + .languages() + .clone(); + let language = self.buffer.read(cx).language().map(Arc::clone); Some( - Text::new(content.value.clone(), alongside_docs_text_style) - .constrained() - .with_width(alongside_docs_width) - .contained() - .with_style(alongside_docs_container_style), + crate::markdown::render_markdown( + &content.value, + ®istry, + &language, + &style, + cx, + ) + .constrained() + .with_width(alongside_docs_width) + .contained() + .with_style(alongside_docs_container_style), ) } else { None @@ -3985,7 +4000,7 @@ impl Editor { ) -> Option<(DisplayPoint, AnyElement)> { self.context_menu .as_ref() - .map(|menu| menu.render(cursor_position, style, cx)) + .map(|menu| menu.render(cursor_position, self, style, cx)) } fn show_context_menu(&mut self, menu: ContextMenu, cx: &mut ViewContext) { diff --git a/crates/editor/src/markdown.rs b/crates/editor/src/markdown.rs new file mode 100644 index 0000000000..3ea8db34b3 --- /dev/null +++ b/crates/editor/src/markdown.rs @@ -0,0 +1,246 @@ +use std::ops::Range; +use std::sync::Arc; + +use futures::FutureExt; +use gpui::{ + elements::Text, + fonts::{HighlightStyle, Underline, Weight}, + platform::{CursorStyle, MouseButton}, + CursorRegion, MouseRegion, ViewContext, +}; +use language::{Language, LanguageRegistry}; +use pulldown_cmark::{CodeBlockKind, Event, Options, Parser, Tag}; + +use crate::{Editor, EditorStyle}; + +#[derive(Debug, Clone)] +struct RenderedRegion { + code: bool, + link_url: Option, +} + +pub fn render_markdown( + markdown: &str, + language_registry: &Arc, + language: &Option>, + style: &EditorStyle, + cx: &mut ViewContext, +) -> Text { + let mut text = String::new(); + let mut highlights = Vec::new(); + let mut region_ranges = Vec::new(); + let mut regions = Vec::new(); + + let mut bold_depth = 0; + let mut italic_depth = 0; + let mut link_url = None; + let mut current_language = None; + let mut list_stack = Vec::new(); + + for event in Parser::new_ext(&markdown, Options::all()) { + let prev_len = text.len(); + match event { + Event::Text(t) => { + if let Some(language) = ¤t_language { + render_code(&mut text, &mut highlights, t.as_ref(), language, style); + } else { + text.push_str(t.as_ref()); + + let mut style = HighlightStyle::default(); + if bold_depth > 0 { + style.weight = Some(Weight::BOLD); + } + if italic_depth > 0 { + style.italic = Some(true); + } + if let Some(link_url) = link_url.clone() { + region_ranges.push(prev_len..text.len()); + regions.push(RenderedRegion { + link_url: Some(link_url), + code: false, + }); + style.underline = Some(Underline { + thickness: 1.0.into(), + ..Default::default() + }); + } + + if style != HighlightStyle::default() { + let mut new_highlight = true; + if let Some((last_range, last_style)) = highlights.last_mut() { + if last_range.end == prev_len && last_style == &style { + last_range.end = text.len(); + new_highlight = false; + } + } + if new_highlight { + highlights.push((prev_len..text.len(), style)); + } + } + } + } + + Event::Code(t) => { + text.push_str(t.as_ref()); + region_ranges.push(prev_len..text.len()); + if link_url.is_some() { + highlights.push(( + prev_len..text.len(), + HighlightStyle { + underline: Some(Underline { + thickness: 1.0.into(), + ..Default::default() + }), + ..Default::default() + }, + )); + } + regions.push(RenderedRegion { + code: true, + link_url: link_url.clone(), + }); + } + + Event::Start(tag) => match tag { + Tag::Paragraph => new_paragraph(&mut text, &mut list_stack), + + Tag::Heading(_, _, _) => { + new_paragraph(&mut text, &mut list_stack); + bold_depth += 1; + } + + Tag::CodeBlock(kind) => { + new_paragraph(&mut text, &mut list_stack); + current_language = if let CodeBlockKind::Fenced(language) = kind { + language_registry + .language_for_name(language.as_ref()) + .now_or_never() + .and_then(Result::ok) + } else { + language.clone() + } + } + + Tag::Emphasis => italic_depth += 1, + + Tag::Strong => bold_depth += 1, + + Tag::Link(_, url, _) => link_url = Some(url.to_string()), + + Tag::List(number) => { + list_stack.push((number, false)); + } + + Tag::Item => { + let len = list_stack.len(); + if let Some((list_number, has_content)) = list_stack.last_mut() { + *has_content = false; + if !text.is_empty() && !text.ends_with('\n') { + text.push('\n'); + } + for _ in 0..len - 1 { + text.push_str(" "); + } + if let Some(number) = list_number { + text.push_str(&format!("{}. ", number)); + *number += 1; + *has_content = false; + } else { + text.push_str("- "); + } + } + } + + _ => {} + }, + + Event::End(tag) => match tag { + Tag::Heading(_, _, _) => bold_depth -= 1, + Tag::CodeBlock(_) => current_language = None, + Tag::Emphasis => italic_depth -= 1, + Tag::Strong => bold_depth -= 1, + Tag::Link(_, _, _) => link_url = None, + Tag::List(_) => drop(list_stack.pop()), + _ => {} + }, + + Event::HardBreak => text.push('\n'), + + Event::SoftBreak => text.push(' '), + + _ => {} + } + } + + let code_span_background_color = style.document_highlight_read_background; + let view_id = cx.view_id(); + let mut region_id = 0; + Text::new(text, style.text.clone()) + .with_highlights(highlights) + .with_custom_runs(region_ranges, move |ix, bounds, scene, _| { + region_id += 1; + let region = regions[ix].clone(); + if let Some(url) = region.link_url { + scene.push_cursor_region(CursorRegion { + bounds, + style: CursorStyle::PointingHand, + }); + scene.push_mouse_region( + MouseRegion::new::(view_id, region_id, bounds) + .on_click::(MouseButton::Left, move |_, _, cx| { + cx.platform().open_url(&url) + }), + ); + } + if region.code { + scene.push_quad(gpui::Quad { + bounds, + background: Some(code_span_background_color), + border: Default::default(), + corner_radii: (2.0).into(), + }); + } + }) + .with_soft_wrap(true) +} + +fn render_code( + text: &mut String, + highlights: &mut Vec<(Range, HighlightStyle)>, + content: &str, + language: &Arc, + style: &EditorStyle, +) { + let prev_len = text.len(); + text.push_str(content); + for (range, highlight_id) in language.highlight_text(&content.into(), 0..content.len()) { + if let Some(style) = highlight_id.style(&style.syntax) { + highlights.push((prev_len + range.start..prev_len + range.end, style)); + } + } +} + +fn new_paragraph(text: &mut String, list_stack: &mut Vec<(Option, bool)>) { + let mut is_subsequent_paragraph_of_list = false; + if let Some((_, has_content)) = list_stack.last_mut() { + if *has_content { + is_subsequent_paragraph_of_list = true; + } else { + *has_content = true; + return; + } + } + + if !text.is_empty() { + if !text.ends_with('\n') { + text.push('\n'); + } + text.push('\n'); + } + for _ in 0..list_stack.len().saturating_sub(1) { + text.push_str(" "); + } + if is_subsequent_paragraph_of_list { + text.push_str(" "); + } +} diff --git a/styles/src/style_tree/editor.ts b/styles/src/style_tree/editor.ts index 37d6c4ea1e..e7717583a8 100644 --- a/styles/src/style_tree/editor.ts +++ b/styles/src/style_tree/editor.ts @@ -209,7 +209,7 @@ export default function editor(): any { inline_docs_container: { padding: { left: 40 } }, inline_docs_color: text(theme.middle, "sans", "disabled", {}).color, inline_docs_size_percent: 0.75, - alongside_docs_width: 400, + alongside_docs_width: 700, alongside_docs_container: { padding: autocomplete_item.padding } }, diagnostic_header: { From e8be14e5d64d6b574ea626ff50e59e84875ebf39 Mon Sep 17 00:00:00 2001 From: Julia Date: Fri, 15 Sep 2023 11:51:57 -0400 Subject: [PATCH 04/30] Merge info popover's and autocomplete docs' markdown rendering --- crates/editor/src/hover_popover.rs | 230 ++++------------------------- crates/editor/src/markdown.rs | 102 ++++++++----- 2 files changed, 87 insertions(+), 245 deletions(-) diff --git a/crates/editor/src/hover_popover.rs b/crates/editor/src/hover_popover.rs index 69b5562c34..16ecb2dc01 100644 --- a/crates/editor/src/hover_popover.rs +++ b/crates/editor/src/hover_popover.rs @@ -1,6 +1,7 @@ use crate::{ display_map::{InlayOffset, ToDisplayPoint}, link_go_to_definition::{DocumentRange, InlayRange}, + markdown::{self, RenderedRegion}, Anchor, AnchorRangeExt, DisplayPoint, Editor, EditorSettings, EditorSnapshot, EditorStyle, ExcerptId, RangeToAnchorExt, }; @@ -8,7 +9,7 @@ use futures::FutureExt; use gpui::{ actions, elements::{Flex, MouseEventHandler, Padding, ParentElement, Text}, - fonts::{HighlightStyle, Underline, Weight}, + fonts::HighlightStyle, platform::{CursorStyle, MouseButton}, AnyElement, AppContext, CursorRegion, Element, ModelHandle, MouseRegion, Task, ViewContext, }; @@ -364,7 +365,7 @@ fn render_blocks( theme_id: usize, blocks: &[HoverBlock], language_registry: &Arc, - language: Option<&Arc>, + language: &Option>, style: &EditorStyle, ) -> RenderedInfo { let mut text = String::new(); @@ -375,160 +376,20 @@ fn render_blocks( for block in blocks { match &block.kind { HoverBlockKind::PlainText => { - new_paragraph(&mut text, &mut Vec::new()); + markdown::new_paragraph(&mut text, &mut Vec::new()); text.push_str(&block.text); } - HoverBlockKind::Markdown => { - use pulldown_cmark::{CodeBlockKind, Event, Options, Parser, Tag}; - - let mut bold_depth = 0; - let mut italic_depth = 0; - let mut link_url = None; - let mut current_language = None; - let mut list_stack = Vec::new(); - - for event in Parser::new_ext(&block.text, Options::all()) { - let prev_len = text.len(); - match event { - Event::Text(t) => { - if let Some(language) = ¤t_language { - render_code( - &mut text, - &mut highlights, - t.as_ref(), - language, - style, - ); - } else { - text.push_str(t.as_ref()); - - let mut style = HighlightStyle::default(); - if bold_depth > 0 { - style.weight = Some(Weight::BOLD); - } - if italic_depth > 0 { - style.italic = Some(true); - } - if let Some(link_url) = link_url.clone() { - region_ranges.push(prev_len..text.len()); - regions.push(RenderedRegion { - link_url: Some(link_url), - code: false, - }); - style.underline = Some(Underline { - thickness: 1.0.into(), - ..Default::default() - }); - } - - if style != HighlightStyle::default() { - let mut new_highlight = true; - if let Some((last_range, last_style)) = highlights.last_mut() { - if last_range.end == prev_len && last_style == &style { - last_range.end = text.len(); - new_highlight = false; - } - } - if new_highlight { - highlights.push((prev_len..text.len(), style)); - } - } - } - } - - Event::Code(t) => { - text.push_str(t.as_ref()); - region_ranges.push(prev_len..text.len()); - if link_url.is_some() { - highlights.push(( - prev_len..text.len(), - HighlightStyle { - underline: Some(Underline { - thickness: 1.0.into(), - ..Default::default() - }), - ..Default::default() - }, - )); - } - regions.push(RenderedRegion { - code: true, - link_url: link_url.clone(), - }); - } - - Event::Start(tag) => match tag { - Tag::Paragraph => new_paragraph(&mut text, &mut list_stack), - - Tag::Heading(_, _, _) => { - new_paragraph(&mut text, &mut list_stack); - bold_depth += 1; - } - - Tag::CodeBlock(kind) => { - new_paragraph(&mut text, &mut list_stack); - current_language = if let CodeBlockKind::Fenced(language) = kind { - language_registry - .language_for_name(language.as_ref()) - .now_or_never() - .and_then(Result::ok) - } else { - language.cloned() - } - } - - Tag::Emphasis => italic_depth += 1, - - Tag::Strong => bold_depth += 1, - - Tag::Link(_, url, _) => link_url = Some(url.to_string()), - - Tag::List(number) => { - list_stack.push((number, false)); - } - - Tag::Item => { - let len = list_stack.len(); - if let Some((list_number, has_content)) = list_stack.last_mut() { - *has_content = false; - if !text.is_empty() && !text.ends_with('\n') { - text.push('\n'); - } - for _ in 0..len - 1 { - text.push_str(" "); - } - if let Some(number) = list_number { - text.push_str(&format!("{}. ", number)); - *number += 1; - *has_content = false; - } else { - text.push_str("- "); - } - } - } - - _ => {} - }, - - Event::End(tag) => match tag { - Tag::Heading(_, _, _) => bold_depth -= 1, - Tag::CodeBlock(_) => current_language = None, - Tag::Emphasis => italic_depth -= 1, - Tag::Strong => bold_depth -= 1, - Tag::Link(_, _, _) => link_url = None, - Tag::List(_) => drop(list_stack.pop()), - _ => {} - }, - - Event::HardBreak => text.push('\n'), - - Event::SoftBreak => text.push(' '), - - _ => {} - } - } - } + HoverBlockKind::Markdown => markdown::render_markdown_block( + &block.text, + language_registry, + language, + style, + &mut text, + &mut highlights, + &mut region_ranges, + &mut regions, + ), HoverBlockKind::Code { language } => { if let Some(language) = language_registry @@ -536,7 +397,13 @@ fn render_blocks( .now_or_never() .and_then(Result::ok) { - render_code(&mut text, &mut highlights, &block.text, &language, style); + markdown::render_code( + &mut text, + &mut highlights, + &block.text, + &language, + style, + ); } else { text.push_str(&block.text); } @@ -553,47 +420,6 @@ fn render_blocks( } } -fn render_code( - text: &mut String, - highlights: &mut Vec<(Range, HighlightStyle)>, - content: &str, - language: &Arc, - style: &EditorStyle, -) { - let prev_len = text.len(); - text.push_str(content); - for (range, highlight_id) in language.highlight_text(&content.into(), 0..content.len()) { - if let Some(style) = highlight_id.style(&style.syntax) { - highlights.push((prev_len + range.start..prev_len + range.end, style)); - } - } -} - -fn new_paragraph(text: &mut String, list_stack: &mut Vec<(Option, bool)>) { - let mut is_subsequent_paragraph_of_list = false; - if let Some((_, has_content)) = list_stack.last_mut() { - if *has_content { - is_subsequent_paragraph_of_list = true; - } else { - *has_content = true; - return; - } - } - - if !text.is_empty() { - if !text.ends_with('\n') { - text.push('\n'); - } - text.push('\n'); - } - for _ in 0..list_stack.len().saturating_sub(1) { - text.push_str(" "); - } - if is_subsequent_paragraph_of_list { - text.push_str(" "); - } -} - #[derive(Default)] pub struct HoverState { pub info_popover: Option, @@ -666,12 +492,6 @@ struct RenderedInfo { regions: Vec, } -#[derive(Debug, Clone)] -struct RenderedRegion { - code: bool, - link_url: Option, -} - impl InfoPopover { pub fn render( &mut self, @@ -689,7 +509,7 @@ impl InfoPopover { style.theme_id, &self.blocks, self.project.read(cx).languages(), - self.language.as_ref(), + &self.language, style, ) }); @@ -829,7 +649,7 @@ mod tests { test::editor_lsp_test_context::EditorLspTestContext, }; use collections::BTreeSet; - use gpui::fonts::Weight; + use gpui::fonts::{Underline, Weight}; use indoc::indoc; use language::{language_settings::InlayHintSettings, Diagnostic, DiagnosticSet}; use lsp::LanguageServerId; @@ -1055,7 +875,7 @@ mod tests { ); let style = editor.style(cx); - let rendered = render_blocks(0, &blocks, &Default::default(), None, &style); + let rendered = render_blocks(0, &blocks, &Default::default(), &None, &style); assert_eq!( rendered.text, code_str.trim(), @@ -1247,7 +1067,7 @@ mod tests { expected_styles, } in &rows[0..] { - let rendered = render_blocks(0, &blocks, &Default::default(), None, &style); + let rendered = render_blocks(0, &blocks, &Default::default(), &None, &style); let (expected_text, ranges) = marked_text_ranges(expected_marked_text, false); let expected_highlights = ranges diff --git a/crates/editor/src/markdown.rs b/crates/editor/src/markdown.rs index 3ea8db34b3..df5041c0db 100644 --- a/crates/editor/src/markdown.rs +++ b/crates/editor/src/markdown.rs @@ -14,9 +14,9 @@ use pulldown_cmark::{CodeBlockKind, Event, Options, Parser, Tag}; use crate::{Editor, EditorStyle}; #[derive(Debug, Clone)] -struct RenderedRegion { - code: bool, - link_url: Option, +pub struct RenderedRegion { + pub code: bool, + pub link_url: Option, } pub fn render_markdown( @@ -31,6 +31,59 @@ pub fn render_markdown( let mut region_ranges = Vec::new(); let mut regions = Vec::new(); + render_markdown_block( + markdown, + language_registry, + language, + style, + &mut text, + &mut highlights, + &mut region_ranges, + &mut regions, + ); + + let code_span_background_color = style.document_highlight_read_background; + let view_id = cx.view_id(); + let mut region_id = 0; + Text::new(text, style.text.clone()) + .with_highlights(highlights) + .with_custom_runs(region_ranges, move |ix, bounds, scene, _| { + region_id += 1; + let region = regions[ix].clone(); + if let Some(url) = region.link_url { + scene.push_cursor_region(CursorRegion { + bounds, + style: CursorStyle::PointingHand, + }); + scene.push_mouse_region( + MouseRegion::new::(view_id, region_id, bounds) + .on_click::(MouseButton::Left, move |_, _, cx| { + cx.platform().open_url(&url) + }), + ); + } + if region.code { + scene.push_quad(gpui::Quad { + bounds, + background: Some(code_span_background_color), + border: Default::default(), + corner_radii: (2.0).into(), + }); + } + }) + .with_soft_wrap(true) +} + +pub fn render_markdown_block( + markdown: &str, + language_registry: &Arc, + language: &Option>, + style: &EditorStyle, + text: &mut String, + highlights: &mut Vec<(Range, HighlightStyle)>, + region_ranges: &mut Vec>, + regions: &mut Vec, +) { let mut bold_depth = 0; let mut italic_depth = 0; let mut link_url = None; @@ -42,7 +95,7 @@ pub fn render_markdown( match event { Event::Text(t) => { if let Some(language) = ¤t_language { - render_code(&mut text, &mut highlights, t.as_ref(), language, style); + render_code(text, highlights, t.as_ref(), language, style); } else { text.push_str(t.as_ref()); @@ -102,15 +155,15 @@ pub fn render_markdown( } Event::Start(tag) => match tag { - Tag::Paragraph => new_paragraph(&mut text, &mut list_stack), + Tag::Paragraph => new_paragraph(text, &mut list_stack), Tag::Heading(_, _, _) => { - new_paragraph(&mut text, &mut list_stack); + new_paragraph(text, &mut list_stack); bold_depth += 1; } Tag::CodeBlock(kind) => { - new_paragraph(&mut text, &mut list_stack); + new_paragraph(text, &mut list_stack); current_language = if let CodeBlockKind::Fenced(language) = kind { language_registry .language_for_name(language.as_ref()) @@ -171,40 +224,9 @@ pub fn render_markdown( _ => {} } } - - let code_span_background_color = style.document_highlight_read_background; - let view_id = cx.view_id(); - let mut region_id = 0; - Text::new(text, style.text.clone()) - .with_highlights(highlights) - .with_custom_runs(region_ranges, move |ix, bounds, scene, _| { - region_id += 1; - let region = regions[ix].clone(); - if let Some(url) = region.link_url { - scene.push_cursor_region(CursorRegion { - bounds, - style: CursorStyle::PointingHand, - }); - scene.push_mouse_region( - MouseRegion::new::(view_id, region_id, bounds) - .on_click::(MouseButton::Left, move |_, _, cx| { - cx.platform().open_url(&url) - }), - ); - } - if region.code { - scene.push_quad(gpui::Quad { - bounds, - background: Some(code_span_background_color), - border: Default::default(), - corner_radii: (2.0).into(), - }); - } - }) - .with_soft_wrap(true) } -fn render_code( +pub fn render_code( text: &mut String, highlights: &mut Vec<(Range, HighlightStyle)>, content: &str, @@ -220,7 +242,7 @@ fn render_code( } } -fn new_paragraph(text: &mut String, list_stack: &mut Vec<(Option, bool)>) { +pub fn new_paragraph(text: &mut String, list_stack: &mut Vec<(Option, bool)>) { let mut is_subsequent_paragraph_of_list = false; if let Some((_, has_content)) = list_stack.last_mut() { if *has_content { From ca88717f0c3145b441f3efeb81d53d578b1a9c7e Mon Sep 17 00:00:00 2001 From: Julia Date: Fri, 15 Sep 2023 15:12:04 -0400 Subject: [PATCH 05/30] Make completion docs scrollable --- crates/editor/src/editor.rs | 26 +++++++++++++++----------- 1 file changed, 15 insertions(+), 11 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 2035bd35f0..2f02ac59b0 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -1064,18 +1064,22 @@ impl CompletionsMenu { .languages() .clone(); let language = self.buffer.read(cx).language().map(Arc::clone); + + enum CompletionDocsMarkdown {} Some( - crate::markdown::render_markdown( - &content.value, - ®istry, - &language, - &style, - cx, - ) - .constrained() - .with_width(alongside_docs_width) - .contained() - .with_style(alongside_docs_container_style), + Flex::column() + .scrollable::(0, None, cx) + .with_child(crate::markdown::render_markdown( + &content.value, + ®istry, + &language, + &style, + cx, + )) + .constrained() + .with_width(alongside_docs_width) + .contained() + .with_style(alongside_docs_container_style), ) } else { None From 77ba25328cbeee3bfec5c3acef71fa8ff4394870 Mon Sep 17 00:00:00 2001 From: Julia Date: Fri, 22 Sep 2023 15:10:48 -0400 Subject: [PATCH 06/30] Most of getting completion documentation resolved & cached MD parsing --- Cargo.lock | 2 +- crates/editor/Cargo.toml | 1 - crates/editor/src/editor.rs | 170 ++++++++++++++++---- crates/editor/src/hover_popover.rs | 6 +- crates/language/Cargo.toml | 1 + crates/language/src/buffer.rs | 2 + crates/language/src/language.rs | 1 + crates/{editor => language}/src/markdown.rs | 90 ++++++----- crates/language/src/proto.rs | 1 + crates/lsp/src/lsp.rs | 14 +- crates/project/src/lsp_command.rs | 1 + crates/zed/src/languages.rs | 4 +- 12 files changed, 217 insertions(+), 76 deletions(-) rename crates/{editor => language}/src/markdown.rs (79%) diff --git a/Cargo.lock b/Cargo.lock index c971846a5d..147760ab14 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2404,7 +2404,6 @@ dependencies = [ "parking_lot 0.11.2", "postage", "project", - "pulldown-cmark", "rand 0.8.5", "rich_text", "rpc", @@ -3990,6 +3989,7 @@ dependencies = [ "lsp", "parking_lot 0.11.2", "postage", + "pulldown-cmark", "rand 0.8.5", "regex", "rpc", diff --git a/crates/editor/Cargo.toml b/crates/editor/Cargo.toml index 2c3d6227a9..d03e1c1106 100644 --- a/crates/editor/Cargo.toml +++ b/crates/editor/Cargo.toml @@ -57,7 +57,6 @@ log.workspace = true ordered-float.workspace = true parking_lot.workspace = true postage.workspace = true -pulldown-cmark = { version = "0.9.2", default-features = false } rand.workspace = true schemars.workspace = true serde.workspace = true diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 2f02ac59b0..c0d2b4ee0b 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -9,7 +9,6 @@ mod highlight_matching_bracket; mod hover_popover; pub mod items; mod link_go_to_definition; -mod markdown; mod mouse_context_menu; pub mod movement; pub mod multi_buffer; @@ -78,6 +77,7 @@ pub use multi_buffer::{ ToPoint, }; use ordered_float::OrderedFloat; +use parking_lot::RwLock; use project::{FormatTrigger, Location, Project, ProjectPath, ProjectTransaction}; use rand::{seq::SliceRandom, thread_rng}; use rpc::proto::PeerId; @@ -788,10 +788,14 @@ enum ContextMenu { } impl ContextMenu { - fn select_first(&mut self, cx: &mut ViewContext) -> bool { + fn select_first( + &mut self, + project: Option<&ModelHandle>, + cx: &mut ViewContext, + ) -> bool { if self.visible() { match self { - ContextMenu::Completions(menu) => menu.select_first(cx), + ContextMenu::Completions(menu) => menu.select_first(project, cx), ContextMenu::CodeActions(menu) => menu.select_first(cx), } true @@ -800,10 +804,14 @@ impl ContextMenu { } } - fn select_prev(&mut self, cx: &mut ViewContext) -> bool { + fn select_prev( + &mut self, + project: Option<&ModelHandle>, + cx: &mut ViewContext, + ) -> bool { if self.visible() { match self { - ContextMenu::Completions(menu) => menu.select_prev(cx), + ContextMenu::Completions(menu) => menu.select_prev(project, cx), ContextMenu::CodeActions(menu) => menu.select_prev(cx), } true @@ -812,10 +820,14 @@ impl ContextMenu { } } - fn select_next(&mut self, cx: &mut ViewContext) -> bool { + fn select_next( + &mut self, + project: Option<&ModelHandle>, + cx: &mut ViewContext, + ) -> bool { if self.visible() { match self { - ContextMenu::Completions(menu) => menu.select_next(cx), + ContextMenu::Completions(menu) => menu.select_next(project, cx), ContextMenu::CodeActions(menu) => menu.select_next(cx), } true @@ -824,10 +836,14 @@ impl ContextMenu { } } - fn select_last(&mut self, cx: &mut ViewContext) -> bool { + fn select_last( + &mut self, + project: Option<&ModelHandle>, + cx: &mut ViewContext, + ) -> bool { if self.visible() { match self { - ContextMenu::Completions(menu) => menu.select_last(cx), + ContextMenu::Completions(menu) => menu.select_last(project, cx), ContextMenu::CodeActions(menu) => menu.select_last(cx), } true @@ -861,7 +877,7 @@ struct CompletionsMenu { id: CompletionId, initial_position: Anchor, buffer: ModelHandle, - completions: Arc<[Completion]>, + completions: Arc>>, match_candidates: Vec, matches: Arc<[StringMatch]>, selected_item: usize, @@ -869,34 +885,115 @@ struct CompletionsMenu { } impl CompletionsMenu { - fn select_first(&mut self, cx: &mut ViewContext) { + fn select_first( + &mut self, + project: Option<&ModelHandle>, + cx: &mut ViewContext, + ) { self.selected_item = 0; self.list.scroll_to(ScrollTarget::Show(self.selected_item)); + self.attempt_resolve_selected_completion(project, cx); cx.notify(); } - fn select_prev(&mut self, cx: &mut ViewContext) { + fn select_prev( + &mut self, + project: Option<&ModelHandle>, + cx: &mut ViewContext, + ) { if self.selected_item > 0 { self.selected_item -= 1; self.list.scroll_to(ScrollTarget::Show(self.selected_item)); } + self.attempt_resolve_selected_completion(project, cx); cx.notify(); } - fn select_next(&mut self, cx: &mut ViewContext) { + fn select_next( + &mut self, + project: Option<&ModelHandle>, + cx: &mut ViewContext, + ) { if self.selected_item + 1 < self.matches.len() { self.selected_item += 1; self.list.scroll_to(ScrollTarget::Show(self.selected_item)); } + self.attempt_resolve_selected_completion(project, cx); cx.notify(); } - fn select_last(&mut self, cx: &mut ViewContext) { + fn select_last( + &mut self, + project: Option<&ModelHandle>, + cx: &mut ViewContext, + ) { self.selected_item = self.matches.len() - 1; self.list.scroll_to(ScrollTarget::Show(self.selected_item)); + self.attempt_resolve_selected_completion(project, cx); cx.notify(); } + fn attempt_resolve_selected_completion( + &mut self, + project: Option<&ModelHandle>, + cx: &mut ViewContext, + ) { + println!("attempt_resolve_selected_completion"); + let index = self.matches[dbg!(self.selected_item)].candidate_id; + dbg!(index); + let Some(project) = project else { + println!("no project"); + return; + }; + + let completions = self.completions.clone(); + let completions_guard = completions.read(); + let completion = &completions_guard[index]; + if completion.lsp_completion.documentation.is_some() { + println!("has existing documentation"); + return; + } + + let server_id = completion.server_id; + let completion = completion.lsp_completion.clone(); + drop(completions_guard); + + let Some(server) = project.read(cx).language_server_for_id(server_id) else { + println!("no server"); + return; + }; + + let can_resolve = server + .capabilities() + .completion_provider + .as_ref() + .and_then(|options| options.resolve_provider) + .unwrap_or(false); + if !dbg!(can_resolve) { + return; + } + + cx.spawn(|this, mut cx| async move { + println!("in spawn"); + let request = server.request::(completion); + let Some(completion_item) = request.await.log_err() else { + println!("errored"); + return; + }; + + if completion_item.documentation.is_some() { + println!("got new documentation"); + let mut completions = completions.write(); + completions[index].lsp_completion.documentation = completion_item.documentation; + println!("notifying"); + _ = this.update(&mut cx, |_, cx| cx.notify()); + } else { + println!("did not get anything"); + } + }) + .detach(); + } + fn visible(&self) -> bool { !self.matches.is_empty() } @@ -914,7 +1011,8 @@ impl CompletionsMenu { .iter() .enumerate() .max_by_key(|(_, mat)| { - let completion = &self.completions[mat.candidate_id]; + let completions = self.completions.read(); + let completion = &completions[mat.candidate_id]; let documentation = &completion.lsp_completion.documentation; let mut len = completion.label.text.chars().count(); @@ -938,6 +1036,7 @@ impl CompletionsMenu { let style = style.clone(); move |_, range, items, cx| { let start_ix = range.start; + let completions = completions.read(); for (ix, mat) in matches[range].iter().enumerate() { let completion = &completions[mat.candidate_id]; let documentation = &completion.lsp_completion.documentation; @@ -1052,7 +1151,8 @@ impl CompletionsMenu { .with_child(list) .with_children({ let mat = &self.matches[selected_item]; - let completion = &self.completions[mat.candidate_id]; + let completions = self.completions.read(); + let completion = &completions[mat.candidate_id]; let documentation = &completion.lsp_completion.documentation; if let Some(lsp::Documentation::MarkupContent(content)) = documentation { @@ -1069,13 +1169,12 @@ impl CompletionsMenu { Some( Flex::column() .scrollable::(0, None, cx) - .with_child(crate::markdown::render_markdown( - &content.value, - ®istry, - &language, - &style, - cx, - )) + // .with_child(language::markdown::render_markdown( + // &content.value, + // ®istry, + // &language, + // &style, + // )) .constrained() .with_width(alongside_docs_width) .contained() @@ -1130,17 +1229,20 @@ impl CompletionsMenu { } } + let completions = self.completions.read(); matches.sort_unstable_by_key(|mat| { - let completion = &self.completions[mat.candidate_id]; + let completion = &completions[mat.candidate_id]; ( completion.lsp_completion.sort_text.as_ref(), Reverse(OrderedFloat(mat.score)), completion.sort_key(), ) }); + drop(completions); for mat in &mut matches { - let filter_start = self.completions[mat.candidate_id].label.filter_range.start; + let completions = self.completions.read(); + let filter_start = completions[mat.candidate_id].label.filter_range.start; for position in &mut mat.positions { *position += filter_start; } @@ -3187,7 +3289,7 @@ impl Editor { }) .collect(), buffer, - completions: completions.into(), + completions: Arc::new(RwLock::new(completions.into())), matches: Vec::new().into(), selected_item: 0, list: Default::default(), @@ -3196,6 +3298,9 @@ impl Editor { if menu.matches.is_empty() { None } else { + _ = this.update(&mut cx, |editor, cx| { + menu.attempt_resolve_selected_completion(editor.project.as_ref(), cx); + }); Some(menu) } } else { @@ -3252,7 +3357,8 @@ impl Editor { .matches .get(action.item_ix.unwrap_or(completions_menu.selected_item))?; let buffer_handle = completions_menu.buffer; - let completion = completions_menu.completions.get(mat.candidate_id)?; + let completions = completions_menu.completions.read(); + let completion = completions.get(mat.candidate_id)?; let snippet; let text; @@ -5372,7 +5478,7 @@ impl Editor { if self .context_menu .as_mut() - .map(|menu| menu.select_last(cx)) + .map(|menu| menu.select_last(self.project.as_ref(), cx)) .unwrap_or(false) { return; @@ -5416,25 +5522,25 @@ impl Editor { pub fn context_menu_first(&mut self, _: &ContextMenuFirst, cx: &mut ViewContext) { if let Some(context_menu) = self.context_menu.as_mut() { - context_menu.select_first(cx); + context_menu.select_first(self.project.as_ref(), cx); } } pub fn context_menu_prev(&mut self, _: &ContextMenuPrev, cx: &mut ViewContext) { if let Some(context_menu) = self.context_menu.as_mut() { - context_menu.select_prev(cx); + context_menu.select_prev(self.project.as_ref(), cx); } } pub fn context_menu_next(&mut self, _: &ContextMenuNext, cx: &mut ViewContext) { if let Some(context_menu) = self.context_menu.as_mut() { - context_menu.select_next(cx); + context_menu.select_next(self.project.as_ref(), cx); } } pub fn context_menu_last(&mut self, _: &ContextMenuLast, cx: &mut ViewContext) { if let Some(context_menu) = self.context_menu.as_mut() { - context_menu.select_last(cx); + context_menu.select_last(self.project.as_ref(), cx); } } diff --git a/crates/editor/src/hover_popover.rs b/crates/editor/src/hover_popover.rs index 16ecb2dc01..ea6eac3a66 100644 --- a/crates/editor/src/hover_popover.rs +++ b/crates/editor/src/hover_popover.rs @@ -1,7 +1,6 @@ use crate::{ display_map::{InlayOffset, ToDisplayPoint}, link_go_to_definition::{DocumentRange, InlayRange}, - markdown::{self, RenderedRegion}, Anchor, AnchorRangeExt, DisplayPoint, Editor, EditorSettings, EditorSnapshot, EditorStyle, ExcerptId, RangeToAnchorExt, }; @@ -13,7 +12,10 @@ use gpui::{ platform::{CursorStyle, MouseButton}, AnyElement, AppContext, CursorRegion, Element, ModelHandle, MouseRegion, Task, ViewContext, }; -use language::{Bias, DiagnosticEntry, DiagnosticSeverity, Language, LanguageRegistry}; +use language::{ + markdown::{self, RenderedRegion}, + Bias, DiagnosticEntry, DiagnosticSeverity, Language, LanguageRegistry, +}; use project::{HoverBlock, HoverBlockKind, InlayHintLabelPart, Project}; use std::{ops::Range, sync::Arc, time::Duration}; use util::TryFutureExt; diff --git a/crates/language/Cargo.toml b/crates/language/Cargo.toml index 4771fc7083..d5d5bdd1af 100644 --- a/crates/language/Cargo.toml +++ b/crates/language/Cargo.toml @@ -46,6 +46,7 @@ lazy_static.workspace = true log.workspace = true parking_lot.workspace = true postage.workspace = true +pulldown-cmark = { version = "0.9.2", default-features = false } regex.workspace = true schemars.workspace = true serde.workspace = true diff --git a/crates/language/src/buffer.rs b/crates/language/src/buffer.rs index 207c41e7cd..10585633ae 100644 --- a/crates/language/src/buffer.rs +++ b/crates/language/src/buffer.rs @@ -1,6 +1,7 @@ pub use crate::{ diagnostic_set::DiagnosticSet, highlight_map::{HighlightId, HighlightMap}, + markdown::RenderedMarkdown, proto, BracketPair, Grammar, Language, LanguageConfig, LanguageRegistry, PLAIN_TEXT, }; use crate::{ @@ -148,6 +149,7 @@ pub struct Completion { pub old_range: Range, pub new_text: String, pub label: CodeLabel, + pub alongside_documentation: Option, pub server_id: LanguageServerId, pub lsp_completion: lsp::CompletionItem, } diff --git a/crates/language/src/language.rs b/crates/language/src/language.rs index 7d113a88af..12f76f1df3 100644 --- a/crates/language/src/language.rs +++ b/crates/language/src/language.rs @@ -2,6 +2,7 @@ mod buffer; mod diagnostic_set; mod highlight_map; pub mod language_settings; +pub mod markdown; mod outline; pub mod proto; mod syntax_map; diff --git a/crates/editor/src/markdown.rs b/crates/language/src/markdown.rs similarity index 79% rename from crates/editor/src/markdown.rs rename to crates/language/src/markdown.rs index df5041c0db..e033820a21 100644 --- a/crates/editor/src/markdown.rs +++ b/crates/language/src/markdown.rs @@ -1,6 +1,7 @@ use std::ops::Range; use std::sync::Arc; +use crate::{Language, LanguageRegistry}; use futures::FutureExt; use gpui::{ elements::Text, @@ -8,10 +9,50 @@ use gpui::{ platform::{CursorStyle, MouseButton}, CursorRegion, MouseRegion, ViewContext, }; -use language::{Language, LanguageRegistry}; use pulldown_cmark::{CodeBlockKind, Event, Options, Parser, Tag}; -use crate::{Editor, EditorStyle}; +#[derive(Debug, Clone)] +pub struct RenderedMarkdown { + text: String, + highlights: Vec<(Range, HighlightStyle)>, + region_ranges: Vec>, + regions: Vec, +} + +// impl RenderedMarkdown { +// pub fn render(&self, style: &theme::Editor, cx: &mut ViewContext) -> Text { +// let code_span_background_color = style.document_highlight_read_background; +// let view_id = cx.view_id(); +// let mut region_id = 0; +// Text::new(text, style.text.clone()) +// .with_highlights(highlights) +// .with_custom_runs(region_ranges, move |ix, bounds, scene, _| { +// region_id += 1; +// let region = regions[ix].clone(); +// if let Some(url) = region.link_url { +// scene.push_cursor_region(CursorRegion { +// bounds, +// style: CursorStyle::PointingHand, +// }); +// scene.push_mouse_region( +// MouseRegion::new::(view_id, region_id, bounds) +// .on_click::(MouseButton::Left, move |_, _, cx| { +// cx.platform().open_url(&url) +// }), +// ); +// } +// if region.code { +// scene.push_quad(gpui::Quad { +// bounds, +// background: Some(code_span_background_color), +// border: Default::default(), +// corner_radii: (2.0).into(), +// }); +// } +// }) +// .with_soft_wrap(true) +// } +// } #[derive(Debug, Clone)] pub struct RenderedRegion { @@ -23,9 +64,8 @@ pub fn render_markdown( markdown: &str, language_registry: &Arc, language: &Option>, - style: &EditorStyle, - cx: &mut ViewContext, -) -> Text { + style: &theme::Editor, +) -> RenderedMarkdown { let mut text = String::new(); let mut highlights = Vec::new(); let mut region_ranges = Vec::new(); @@ -42,43 +82,19 @@ pub fn render_markdown( &mut regions, ); - let code_span_background_color = style.document_highlight_read_background; - let view_id = cx.view_id(); - let mut region_id = 0; - Text::new(text, style.text.clone()) - .with_highlights(highlights) - .with_custom_runs(region_ranges, move |ix, bounds, scene, _| { - region_id += 1; - let region = regions[ix].clone(); - if let Some(url) = region.link_url { - scene.push_cursor_region(CursorRegion { - bounds, - style: CursorStyle::PointingHand, - }); - scene.push_mouse_region( - MouseRegion::new::(view_id, region_id, bounds) - .on_click::(MouseButton::Left, move |_, _, cx| { - cx.platform().open_url(&url) - }), - ); - } - if region.code { - scene.push_quad(gpui::Quad { - bounds, - background: Some(code_span_background_color), - border: Default::default(), - corner_radii: (2.0).into(), - }); - } - }) - .with_soft_wrap(true) + RenderedMarkdown { + text, + highlights, + region_ranges, + regions, + } } pub fn render_markdown_block( markdown: &str, language_registry: &Arc, language: &Option>, - style: &EditorStyle, + style: &theme::Editor, text: &mut String, highlights: &mut Vec<(Range, HighlightStyle)>, region_ranges: &mut Vec>, @@ -231,7 +247,7 @@ pub fn render_code( highlights: &mut Vec<(Range, HighlightStyle)>, content: &str, language: &Arc, - style: &EditorStyle, + style: &theme::Editor, ) { let prev_len = text.len(); text.push_str(content); diff --git a/crates/language/src/proto.rs b/crates/language/src/proto.rs index c4abe39d47..49b332b4fb 100644 --- a/crates/language/src/proto.rs +++ b/crates/language/src/proto.rs @@ -482,6 +482,7 @@ pub async fn deserialize_completion( lsp_completion.filter_text.as_deref(), ) }), + alongside_documentation: None, server_id: LanguageServerId(completion.server_id as usize), lsp_completion, }) diff --git a/crates/lsp/src/lsp.rs b/crates/lsp/src/lsp.rs index 33581721ae..b4099e2f6e 100644 --- a/crates/lsp/src/lsp.rs +++ b/crates/lsp/src/lsp.rs @@ -466,7 +466,10 @@ impl LanguageServer { completion_item: Some(CompletionItemCapability { snippet_support: Some(true), resolve_support: Some(CompletionItemCapabilityResolveSupport { - properties: vec!["additionalTextEdits".to_string()], + properties: vec![ + "documentation".to_string(), + "additionalTextEdits".to_string(), + ], }), ..Default::default() }), @@ -748,6 +751,15 @@ impl LanguageServer { ) } + // some child of string literal (be it "" or ``) which is the child of an attribute + + // + // + // + // + // const classes = "awesome "; + // + fn request_internal( next_id: &AtomicUsize, response_handlers: &Mutex>>, diff --git a/crates/project/src/lsp_command.rs b/crates/project/src/lsp_command.rs index 8beaea5031..000fd3928c 100644 --- a/crates/project/src/lsp_command.rs +++ b/crates/project/src/lsp_command.rs @@ -1462,6 +1462,7 @@ impl LspCommand for GetCompletions { lsp_completion.filter_text.as_deref(), ) }), + alongside_documentation: None, server_id, lsp_completion, } diff --git a/crates/zed/src/languages.rs b/crates/zed/src/languages.rs index 04e5292a7d..3b65255a3d 100644 --- a/crates/zed/src/languages.rs +++ b/crates/zed/src/languages.rs @@ -128,8 +128,8 @@ pub fn init( "tsx", tree_sitter_typescript::language_tsx(), vec![ - Arc::new(typescript::TypeScriptLspAdapter::new(node_runtime.clone())), - Arc::new(typescript::EsLintLspAdapter::new(node_runtime.clone())), + // Arc::new(typescript::TypeScriptLspAdapter::new(node_runtime.clone())), + // Arc::new(typescript::EsLintLspAdapter::new(node_runtime.clone())), Arc::new(tailwind::TailwindLspAdapter::new(node_runtime.clone())), ], ); From fcaf48eb4965349bedc7daab6561060686c8c757 Mon Sep 17 00:00:00 2001 From: Julia Date: Fri, 22 Sep 2023 17:03:40 -0400 Subject: [PATCH 07/30] Use completion item default `data` when provided --- crates/project/src/lsp_command.rs | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/crates/project/src/lsp_command.rs b/crates/project/src/lsp_command.rs index 000fd3928c..16ebb7467b 100644 --- a/crates/project/src/lsp_command.rs +++ b/crates/project/src/lsp_command.rs @@ -1358,7 +1358,7 @@ impl LspCommand for GetCompletions { } } } else { - Default::default() + Vec::new() }; let completions = buffer.read_with(&cx, |buffer, _| { @@ -1370,6 +1370,14 @@ impl LspCommand for GetCompletions { completions .into_iter() .filter_map(move |mut lsp_completion| { + if let Some(response_list) = &response_list { + if let Some(item_defaults) = &response_list.item_defaults { + if let Some(data) = &item_defaults.data { + lsp_completion.data = Some(data.clone()); + } + } + } + let (old_range, mut new_text) = match lsp_completion.text_edit.as_ref() { // If the language server provides a range to overwrite, then // check that the range is valid. From fe62423344fedb81c17a5f6aa81b56805fc8d2bc Mon Sep 17 00:00:00 2001 From: Julia Date: Thu, 28 Sep 2023 14:16:30 -0400 Subject: [PATCH 08/30] Asynchronously request completion documentation if not present --- crates/editor/src/editor.rs | 78 +++++++++++++++++++++++---------- crates/language/src/markdown.rs | 50 +++------------------ 2 files changed, 61 insertions(+), 67 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index c0d2b4ee0b..d35a9dd30e 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -48,9 +48,9 @@ use gpui::{ impl_actions, keymap_matcher::KeymapContext, platform::{CursorStyle, MouseButton}, - serde_json, AnyElement, AnyViewHandle, AppContext, AsyncAppContext, ClipboardItem, Element, - Entity, ModelHandle, Subscription, Task, View, ViewContext, ViewHandle, WeakViewHandle, - WindowContext, + serde_json, AnyElement, AnyViewHandle, AppContext, AsyncAppContext, ClipboardItem, + CursorRegion, Element, Entity, ModelHandle, MouseRegion, Subscription, Task, View, ViewContext, + ViewHandle, WeakViewHandle, WindowContext, }; use highlight_matching_bracket::refresh_matching_bracket_highlights; use hover_popover::{hide_hover, HoverState}; @@ -119,6 +119,46 @@ pub const DOCUMENT_HIGHLIGHTS_DEBOUNCE_TIMEOUT: Duration = Duration::from_millis pub const FORMAT_TIMEOUT: Duration = Duration::from_secs(2); +pub fn render_rendered_markdown( + md: &language::RenderedMarkdown, + style: &EditorStyle, + cx: &mut ViewContext, +) -> Text { + enum RenderedRenderedMarkdown {} + + let md = md.clone(); + let code_span_background_color = style.document_highlight_read_background; + let view_id = cx.view_id(); + let mut region_id = 0; + Text::new(md.text, style.text.clone()) + .with_highlights(md.highlights) + .with_custom_runs(md.region_ranges, move |ix, bounds, scene, _| { + region_id += 1; + let region = md.regions[ix].clone(); + if let Some(url) = region.link_url { + scene.push_cursor_region(CursorRegion { + bounds, + style: CursorStyle::PointingHand, + }); + scene.push_mouse_region( + MouseRegion::new::(view_id, region_id, bounds) + .on_click::(MouseButton::Left, move |_, _, cx| { + cx.platform().open_url(&url) + }), + ); + } + if region.code { + scene.push_quad(gpui::Quad { + bounds, + background: Some(code_span_background_color), + border: Default::default(), + corner_radii: (2.0).into(), + }); + } + }) + .with_soft_wrap(true) +} + #[derive(Clone, Deserialize, PartialEq, Default)] pub struct SelectNext { #[serde(default)] @@ -938,11 +978,8 @@ impl CompletionsMenu { project: Option<&ModelHandle>, cx: &mut ViewContext, ) { - println!("attempt_resolve_selected_completion"); - let index = self.matches[dbg!(self.selected_item)].candidate_id; - dbg!(index); + let index = self.matches[self.selected_item].candidate_id; let Some(project) = project else { - println!("no project"); return; }; @@ -950,7 +987,6 @@ impl CompletionsMenu { let completions_guard = completions.read(); let completion = &completions_guard[index]; if completion.lsp_completion.documentation.is_some() { - println!("has existing documentation"); return; } @@ -959,7 +995,6 @@ impl CompletionsMenu { drop(completions_guard); let Some(server) = project.read(cx).language_server_for_id(server_id) else { - println!("no server"); return; }; @@ -969,26 +1004,21 @@ impl CompletionsMenu { .as_ref() .and_then(|options| options.resolve_provider) .unwrap_or(false); - if !dbg!(can_resolve) { + if !can_resolve { return; } cx.spawn(|this, mut cx| async move { - println!("in spawn"); let request = server.request::(completion); let Some(completion_item) = request.await.log_err() else { - println!("errored"); return; }; if completion_item.documentation.is_some() { - println!("got new documentation"); let mut completions = completions.write(); completions[index].lsp_completion.documentation = completion_item.documentation; - println!("notifying"); + drop(completions); _ = this.update(&mut cx, |_, cx| cx.notify()); - } else { - println!("did not get anything"); } }) .detach(); @@ -1169,12 +1199,16 @@ impl CompletionsMenu { Some( Flex::column() .scrollable::(0, None, cx) - // .with_child(language::markdown::render_markdown( - // &content.value, - // ®istry, - // &language, - // &style, - // )) + .with_child(render_rendered_markdown( + &language::markdown::render_markdown( + &content.value, + ®istry, + &language, + &style.theme, + ), + &style, + cx, + )) .constrained() .with_width(alongside_docs_width) .contained() diff --git a/crates/language/src/markdown.rs b/crates/language/src/markdown.rs index e033820a21..4ccd2955b6 100644 --- a/crates/language/src/markdown.rs +++ b/crates/language/src/markdown.rs @@ -3,57 +3,17 @@ use std::sync::Arc; use crate::{Language, LanguageRegistry}; use futures::FutureExt; -use gpui::{ - elements::Text, - fonts::{HighlightStyle, Underline, Weight}, - platform::{CursorStyle, MouseButton}, - CursorRegion, MouseRegion, ViewContext, -}; +use gpui::fonts::{HighlightStyle, Underline, Weight}; use pulldown_cmark::{CodeBlockKind, Event, Options, Parser, Tag}; #[derive(Debug, Clone)] pub struct RenderedMarkdown { - text: String, - highlights: Vec<(Range, HighlightStyle)>, - region_ranges: Vec>, - regions: Vec, + pub text: String, + pub highlights: Vec<(Range, HighlightStyle)>, + pub region_ranges: Vec>, + pub regions: Vec, } -// impl RenderedMarkdown { -// pub fn render(&self, style: &theme::Editor, cx: &mut ViewContext) -> Text { -// let code_span_background_color = style.document_highlight_read_background; -// let view_id = cx.view_id(); -// let mut region_id = 0; -// Text::new(text, style.text.clone()) -// .with_highlights(highlights) -// .with_custom_runs(region_ranges, move |ix, bounds, scene, _| { -// region_id += 1; -// let region = regions[ix].clone(); -// if let Some(url) = region.link_url { -// scene.push_cursor_region(CursorRegion { -// bounds, -// style: CursorStyle::PointingHand, -// }); -// scene.push_mouse_region( -// MouseRegion::new::(view_id, region_id, bounds) -// .on_click::(MouseButton::Left, move |_, _, cx| { -// cx.platform().open_url(&url) -// }), -// ); -// } -// if region.code { -// scene.push_quad(gpui::Quad { -// bounds, -// background: Some(code_span_background_color), -// border: Default::default(), -// corner_radii: (2.0).into(), -// }); -// } -// }) -// .with_soft_wrap(true) -// } -// } - #[derive(Debug, Clone)] pub struct RenderedRegion { pub code: bool, From b8876f2b17307917bf24b20ffcdd090f9dc5126e Mon Sep 17 00:00:00 2001 From: Julia Date: Tue, 3 Oct 2023 10:58:08 -0400 Subject: [PATCH 09/30] Preparse documentation markdown when resolving completion --- crates/editor/src/editor.rs | 131 ++++++++++++++++------------- crates/editor/src/hover_popover.rs | 26 +++--- crates/language/src/buffer.rs | 44 +++++++++- crates/language/src/markdown.rs | 30 +++---- crates/language/src/proto.rs | 2 +- crates/project/src/lsp_command.rs | 2 +- 6 files changed, 144 insertions(+), 91 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index d35a9dd30e..0f117cde1b 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -119,12 +119,12 @@ pub const DOCUMENT_HIGHLIGHTS_DEBOUNCE_TIMEOUT: Duration = Duration::from_millis pub const FORMAT_TIMEOUT: Duration = Duration::from_secs(2); -pub fn render_rendered_markdown( - md: &language::RenderedMarkdown, +pub fn render_parsed_markdown( + md: &language::ParsedMarkdown, style: &EditorStyle, cx: &mut ViewContext, ) -> Text { - enum RenderedRenderedMarkdown {} + enum RenderedMarkdown {} let md = md.clone(); let code_span_background_color = style.document_highlight_read_background; @@ -141,7 +141,7 @@ pub fn render_rendered_markdown( style: CursorStyle::PointingHand, }); scene.push_mouse_region( - MouseRegion::new::(view_id, region_id, bounds) + MouseRegion::new::(view_id, region_id, bounds) .on_click::(MouseButton::Left, move |_, _, cx| { cx.platform().open_url(&url) }), @@ -831,11 +831,12 @@ impl ContextMenu { fn select_first( &mut self, project: Option<&ModelHandle>, + style: theme::Editor, cx: &mut ViewContext, ) -> bool { if self.visible() { match self { - ContextMenu::Completions(menu) => menu.select_first(project, cx), + ContextMenu::Completions(menu) => menu.select_first(project, style, cx), ContextMenu::CodeActions(menu) => menu.select_first(cx), } true @@ -847,11 +848,12 @@ impl ContextMenu { fn select_prev( &mut self, project: Option<&ModelHandle>, + style: theme::Editor, cx: &mut ViewContext, ) -> bool { if self.visible() { match self { - ContextMenu::Completions(menu) => menu.select_prev(project, cx), + ContextMenu::Completions(menu) => menu.select_prev(project, style, cx), ContextMenu::CodeActions(menu) => menu.select_prev(cx), } true @@ -863,11 +865,12 @@ impl ContextMenu { fn select_next( &mut self, project: Option<&ModelHandle>, + style: theme::Editor, cx: &mut ViewContext, ) -> bool { if self.visible() { match self { - ContextMenu::Completions(menu) => menu.select_next(project, cx), + ContextMenu::Completions(menu) => menu.select_next(project, style, cx), ContextMenu::CodeActions(menu) => menu.select_next(cx), } true @@ -879,11 +882,12 @@ impl ContextMenu { fn select_last( &mut self, project: Option<&ModelHandle>, + style: theme::Editor, cx: &mut ViewContext, ) -> bool { if self.visible() { match self { - ContextMenu::Completions(menu) => menu.select_last(project, cx), + ContextMenu::Completions(menu) => menu.select_last(project, style, cx), ContextMenu::CodeActions(menu) => menu.select_last(cx), } true @@ -928,60 +932,66 @@ impl CompletionsMenu { fn select_first( &mut self, project: Option<&ModelHandle>, + style: theme::Editor, cx: &mut ViewContext, ) { self.selected_item = 0; self.list.scroll_to(ScrollTarget::Show(self.selected_item)); - self.attempt_resolve_selected_completion(project, cx); + self.attempt_resolve_selected_completion(project, style, cx); cx.notify(); } fn select_prev( &mut self, project: Option<&ModelHandle>, + style: theme::Editor, cx: &mut ViewContext, ) { if self.selected_item > 0 { self.selected_item -= 1; self.list.scroll_to(ScrollTarget::Show(self.selected_item)); } - self.attempt_resolve_selected_completion(project, cx); + self.attempt_resolve_selected_completion(project, style, cx); cx.notify(); } fn select_next( &mut self, project: Option<&ModelHandle>, + style: theme::Editor, cx: &mut ViewContext, ) { if self.selected_item + 1 < self.matches.len() { self.selected_item += 1; self.list.scroll_to(ScrollTarget::Show(self.selected_item)); } - self.attempt_resolve_selected_completion(project, cx); + self.attempt_resolve_selected_completion(project, style, cx); cx.notify(); } fn select_last( &mut self, project: Option<&ModelHandle>, + style: theme::Editor, cx: &mut ViewContext, ) { self.selected_item = self.matches.len() - 1; self.list.scroll_to(ScrollTarget::Show(self.selected_item)); - self.attempt_resolve_selected_completion(project, cx); + self.attempt_resolve_selected_completion(project, style, cx); cx.notify(); } fn attempt_resolve_selected_completion( &mut self, project: Option<&ModelHandle>, + style: theme::Editor, cx: &mut ViewContext, ) { let index = self.matches[self.selected_item].candidate_id; let Some(project) = project else { return; }; + let language_registry = project.read(cx).languages().clone(); let completions = self.completions.clone(); let completions_guard = completions.read(); @@ -1008,16 +1018,27 @@ impl CompletionsMenu { return; } + // TODO: Do on background cx.spawn(|this, mut cx| async move { let request = server.request::(completion); let Some(completion_item) = request.await.log_err() else { return; }; - if completion_item.documentation.is_some() { + if let Some(lsp_documentation) = completion_item.documentation { + let documentation = language::prepare_completion_documentation( + &lsp_documentation, + &language_registry, + None, // TODO: Try to reasonably work out which language the completion is for + &style, + ); + let mut completions = completions.write(); - completions[index].lsp_completion.documentation = completion_item.documentation; + let completion = &mut completions[index]; + completion.documentation = documentation; + completion.lsp_completion.documentation = Some(lsp_documentation); drop(completions); + _ = this.update(&mut cx, |_, cx| cx.notify()); } }) @@ -1069,7 +1090,7 @@ impl CompletionsMenu { let completions = completions.read(); for (ix, mat) in matches[range].iter().enumerate() { let completion = &completions[mat.candidate_id]; - let documentation = &completion.lsp_completion.documentation; + let documentation = &completion.documentation; let item_ix = start_ix + ix; items.push( @@ -1100,7 +1121,9 @@ impl CompletionsMenu { ), ); - if let Some(lsp::Documentation::String(text)) = documentation { + if let Some(language::Documentation::SingleLine(text)) = + documentation + { Flex::row() .with_child(completion_label) .with_children((|| { @@ -1183,39 +1206,18 @@ impl CompletionsMenu { let mat = &self.matches[selected_item]; let completions = self.completions.read(); let completion = &completions[mat.candidate_id]; - let documentation = &completion.lsp_completion.documentation; + let documentation = &completion.documentation; - if let Some(lsp::Documentation::MarkupContent(content)) = documentation { - let registry = editor - .project - .as_ref() - .unwrap() - .read(cx) - .languages() - .clone(); - let language = self.buffer.read(cx).language().map(Arc::clone); + match documentation { + Some(language::Documentation::MultiLinePlainText(text)) => { + Some(Text::new(text.clone(), style.text.clone())) + } - enum CompletionDocsMarkdown {} - Some( - Flex::column() - .scrollable::(0, None, cx) - .with_child(render_rendered_markdown( - &language::markdown::render_markdown( - &content.value, - ®istry, - &language, - &style.theme, - ), - &style, - cx, - )) - .constrained() - .with_width(alongside_docs_width) - .contained() - .with_style(alongside_docs_container_style), - ) - } else { - None + Some(language::Documentation::MultiLineMarkdown(parsed)) => { + Some(render_parsed_markdown(parsed, &style, cx)) + } + + _ => None, } }) .contained() @@ -3333,7 +3335,11 @@ impl Editor { None } else { _ = this.update(&mut cx, |editor, cx| { - menu.attempt_resolve_selected_completion(editor.project.as_ref(), cx); + menu.attempt_resolve_selected_completion( + editor.project.as_ref(), + editor.style(cx).theme, + cx, + ); }); Some(menu) } @@ -5509,13 +5515,16 @@ impl Editor { return; } - if self - .context_menu - .as_mut() - .map(|menu| menu.select_last(self.project.as_ref(), cx)) - .unwrap_or(false) - { - return; + if self.context_menu.is_some() { + let style = self.style(cx).theme; + if self + .context_menu + .as_mut() + .map(|menu| menu.select_last(self.project.as_ref(), style, cx)) + .unwrap_or(false) + { + return; + } } if matches!(self.mode, EditorMode::SingleLine) { @@ -5555,26 +5564,30 @@ impl Editor { } pub fn context_menu_first(&mut self, _: &ContextMenuFirst, cx: &mut ViewContext) { + let style = self.style(cx).theme; if let Some(context_menu) = self.context_menu.as_mut() { - context_menu.select_first(self.project.as_ref(), cx); + context_menu.select_first(self.project.as_ref(), style, cx); } } pub fn context_menu_prev(&mut self, _: &ContextMenuPrev, cx: &mut ViewContext) { + let style = self.style(cx).theme; if let Some(context_menu) = self.context_menu.as_mut() { - context_menu.select_prev(self.project.as_ref(), cx); + context_menu.select_prev(self.project.as_ref(), style, cx); } } pub fn context_menu_next(&mut self, _: &ContextMenuNext, cx: &mut ViewContext) { + let style = self.style(cx).theme; if let Some(context_menu) = self.context_menu.as_mut() { - context_menu.select_next(self.project.as_ref(), cx); + context_menu.select_next(self.project.as_ref(), style, cx); } } pub fn context_menu_last(&mut self, _: &ContextMenuLast, cx: &mut ViewContext) { + let style = self.style(cx).theme; if let Some(context_menu) = self.context_menu.as_mut() { - context_menu.select_last(self.project.as_ref(), cx); + context_menu.select_last(self.project.as_ref(), style, cx); } } diff --git a/crates/editor/src/hover_popover.rs b/crates/editor/src/hover_popover.rs index ea6eac3a66..9341fa2da3 100644 --- a/crates/editor/src/hover_popover.rs +++ b/crates/editor/src/hover_popover.rs @@ -13,7 +13,7 @@ use gpui::{ AnyElement, AppContext, CursorRegion, Element, ModelHandle, MouseRegion, Task, ViewContext, }; use language::{ - markdown::{self, RenderedRegion}, + markdown::{self, ParsedRegion}, Bias, DiagnosticEntry, DiagnosticSeverity, Language, LanguageRegistry, }; use project::{HoverBlock, HoverBlockKind, InlayHintLabelPart, Project}; @@ -367,9 +367,9 @@ fn render_blocks( theme_id: usize, blocks: &[HoverBlock], language_registry: &Arc, - language: &Option>, + language: Option>, style: &EditorStyle, -) -> RenderedInfo { +) -> ParsedInfo { let mut text = String::new(); let mut highlights = Vec::new(); let mut region_ranges = Vec::new(); @@ -382,10 +382,10 @@ fn render_blocks( text.push_str(&block.text); } - HoverBlockKind::Markdown => markdown::render_markdown_block( + HoverBlockKind::Markdown => markdown::parse_markdown_block( &block.text, language_registry, - language, + language.clone(), style, &mut text, &mut highlights, @@ -399,7 +399,7 @@ fn render_blocks( .now_or_never() .and_then(Result::ok) { - markdown::render_code( + markdown::highlight_code( &mut text, &mut highlights, &block.text, @@ -413,7 +413,7 @@ fn render_blocks( } } - RenderedInfo { + ParsedInfo { theme_id, text: text.trim().to_string(), highlights, @@ -482,16 +482,16 @@ pub struct InfoPopover { symbol_range: DocumentRange, pub blocks: Vec, language: Option>, - rendered_content: Option, + rendered_content: Option, } #[derive(Debug, Clone)] -struct RenderedInfo { +struct ParsedInfo { theme_id: usize, text: String, highlights: Vec<(Range, HighlightStyle)>, region_ranges: Vec>, - regions: Vec, + regions: Vec, } impl InfoPopover { @@ -511,7 +511,7 @@ impl InfoPopover { style.theme_id, &self.blocks, self.project.read(cx).languages(), - &self.language, + self.language.clone(), style, ) }); @@ -877,7 +877,7 @@ mod tests { ); let style = editor.style(cx); - let rendered = render_blocks(0, &blocks, &Default::default(), &None, &style); + let rendered = render_blocks(0, &blocks, &Default::default(), None, &style); assert_eq!( rendered.text, code_str.trim(), @@ -1069,7 +1069,7 @@ mod tests { expected_styles, } in &rows[0..] { - let rendered = render_blocks(0, &blocks, &Default::default(), &None, &style); + let rendered = render_blocks(0, &blocks, &Default::default(), None, &style); let (expected_text, ranges) = marked_text_ranges(expected_marked_text, false); let expected_highlights = ranges diff --git a/crates/language/src/buffer.rs b/crates/language/src/buffer.rs index 10585633ae..344b470aa9 100644 --- a/crates/language/src/buffer.rs +++ b/crates/language/src/buffer.rs @@ -1,12 +1,13 @@ pub use crate::{ diagnostic_set::DiagnosticSet, highlight_map::{HighlightId, HighlightMap}, - markdown::RenderedMarkdown, + markdown::ParsedMarkdown, proto, BracketPair, Grammar, Language, LanguageConfig, LanguageRegistry, PLAIN_TEXT, }; use crate::{ diagnostic_set::{DiagnosticEntry, DiagnosticGroup}, language_settings::{language_settings, LanguageSettings}, + markdown, outline::OutlineItem, syntax_map::{ SyntaxLayerInfo, SyntaxMap, SyntaxMapCapture, SyntaxMapCaptures, SyntaxMapMatches, @@ -144,12 +145,51 @@ pub struct Diagnostic { pub is_unnecessary: bool, } +pub fn prepare_completion_documentation( + documentation: &lsp::Documentation, + language_registry: &Arc, + language: Option>, + style: &theme::Editor, +) -> Option { + match documentation { + lsp::Documentation::String(text) => { + if text.lines().count() <= 1 { + Some(Documentation::SingleLine(text.clone())) + } else { + Some(Documentation::MultiLinePlainText(text.clone())) + } + } + + lsp::Documentation::MarkupContent(lsp::MarkupContent { kind, value }) => match kind { + lsp::MarkupKind::PlainText => { + if value.lines().count() <= 1 { + Some(Documentation::SingleLine(value.clone())) + } else { + Some(Documentation::MultiLinePlainText(value.clone())) + } + } + + lsp::MarkupKind::Markdown => { + let parsed = markdown::parse_markdown(value, language_registry, language, style); + Some(Documentation::MultiLineMarkdown(parsed)) + } + }, + } +} + +#[derive(Clone, Debug)] +pub enum Documentation { + SingleLine(String), + MultiLinePlainText(String), + MultiLineMarkdown(ParsedMarkdown), +} + #[derive(Clone, Debug)] pub struct Completion { pub old_range: Range, pub new_text: String, pub label: CodeLabel, - pub alongside_documentation: Option, + pub documentation: Option, pub server_id: LanguageServerId, pub lsp_completion: lsp::CompletionItem, } diff --git a/crates/language/src/markdown.rs b/crates/language/src/markdown.rs index 4ccd2955b6..c56a676378 100644 --- a/crates/language/src/markdown.rs +++ b/crates/language/src/markdown.rs @@ -7,31 +7,31 @@ use gpui::fonts::{HighlightStyle, Underline, Weight}; use pulldown_cmark::{CodeBlockKind, Event, Options, Parser, Tag}; #[derive(Debug, Clone)] -pub struct RenderedMarkdown { +pub struct ParsedMarkdown { pub text: String, pub highlights: Vec<(Range, HighlightStyle)>, pub region_ranges: Vec>, - pub regions: Vec, + pub regions: Vec, } #[derive(Debug, Clone)] -pub struct RenderedRegion { +pub struct ParsedRegion { pub code: bool, pub link_url: Option, } -pub fn render_markdown( +pub fn parse_markdown( markdown: &str, language_registry: &Arc, - language: &Option>, + language: Option>, style: &theme::Editor, -) -> RenderedMarkdown { +) -> ParsedMarkdown { let mut text = String::new(); let mut highlights = Vec::new(); let mut region_ranges = Vec::new(); let mut regions = Vec::new(); - render_markdown_block( + parse_markdown_block( markdown, language_registry, language, @@ -42,7 +42,7 @@ pub fn render_markdown( &mut regions, ); - RenderedMarkdown { + ParsedMarkdown { text, highlights, region_ranges, @@ -50,15 +50,15 @@ pub fn render_markdown( } } -pub fn render_markdown_block( +pub fn parse_markdown_block( markdown: &str, language_registry: &Arc, - language: &Option>, + language: Option>, style: &theme::Editor, text: &mut String, highlights: &mut Vec<(Range, HighlightStyle)>, region_ranges: &mut Vec>, - regions: &mut Vec, + regions: &mut Vec, ) { let mut bold_depth = 0; let mut italic_depth = 0; @@ -71,7 +71,7 @@ pub fn render_markdown_block( match event { Event::Text(t) => { if let Some(language) = ¤t_language { - render_code(text, highlights, t.as_ref(), language, style); + highlight_code(text, highlights, t.as_ref(), language, style); } else { text.push_str(t.as_ref()); @@ -84,7 +84,7 @@ pub fn render_markdown_block( } if let Some(link_url) = link_url.clone() { region_ranges.push(prev_len..text.len()); - regions.push(RenderedRegion { + regions.push(ParsedRegion { link_url: Some(link_url), code: false, }); @@ -124,7 +124,7 @@ pub fn render_markdown_block( }, )); } - regions.push(RenderedRegion { + regions.push(ParsedRegion { code: true, link_url: link_url.clone(), }); @@ -202,7 +202,7 @@ pub fn render_markdown_block( } } -pub fn render_code( +pub fn highlight_code( text: &mut String, highlights: &mut Vec<(Range, HighlightStyle)>, content: &str, diff --git a/crates/language/src/proto.rs b/crates/language/src/proto.rs index 49b332b4fb..957f4ee7fb 100644 --- a/crates/language/src/proto.rs +++ b/crates/language/src/proto.rs @@ -482,7 +482,7 @@ pub async fn deserialize_completion( lsp_completion.filter_text.as_deref(), ) }), - alongside_documentation: None, + documentation: None, server_id: LanguageServerId(completion.server_id as usize), lsp_completion, }) diff --git a/crates/project/src/lsp_command.rs b/crates/project/src/lsp_command.rs index 16ebb7467b..400dbe2abf 100644 --- a/crates/project/src/lsp_command.rs +++ b/crates/project/src/lsp_command.rs @@ -1470,7 +1470,7 @@ impl LspCommand for GetCompletions { lsp_completion.filter_text.as_deref(), ) }), - alongside_documentation: None, + documentation: None, server_id, lsp_completion, } From ea6f366d2348135897fdb4a803097d4ffdfdab24 Mon Sep 17 00:00:00 2001 From: Julia Date: Tue, 3 Oct 2023 11:58:49 -0400 Subject: [PATCH 10/30] If documentation exists and hasn't been parsed, do so at render and keep --- crates/editor/src/editor.rs | 46 ++++++++++++++++++++++++------------- 1 file changed, 30 insertions(+), 16 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 0f117cde1b..cabf73b581 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -60,10 +60,10 @@ use itertools::Itertools; pub use language::{char_kind, CharKind}; use language::{ language_settings::{self, all_language_settings, InlayHintSettings}, - point_from_lsp, AutoindentMode, BracketPair, Buffer, CodeAction, CodeLabel, Completion, - CursorShape, Diagnostic, DiagnosticSeverity, File, IndentKind, IndentSize, Language, - LanguageServerName, OffsetRangeExt, OffsetUtf16, Point, Selection, SelectionGoal, - TransactionId, + point_from_lsp, prepare_completion_documentation, AutoindentMode, BracketPair, Buffer, + CodeAction, CodeLabel, Completion, CursorShape, Diagnostic, DiagnosticSeverity, Documentation, + File, IndentKind, IndentSize, Language, LanguageServerName, OffsetRangeExt, OffsetUtf16, Point, + Selection, SelectionGoal, TransactionId, }; use link_go_to_definition::{ hide_link_definition, show_link_definition, GoToDefinitionLink, InlayHighlight, @@ -1075,21 +1075,37 @@ impl CompletionsMenu { }) .map(|(ix, _)| ix); + let project = editor.project.clone(); let completions = self.completions.clone(); let matches = self.matches.clone(); let selected_item = self.selected_item; - let alongside_docs_width = style.autocomplete.alongside_docs_width; - let alongside_docs_container_style = style.autocomplete.alongside_docs_container; - let outer_container_style = style.autocomplete.container; - let list = UniformList::new(self.list.clone(), matches.len(), cx, { let style = style.clone(); move |_, range, items, cx| { let start_ix = range.start; - let completions = completions.read(); + let mut completions = completions.write(); + for (ix, mat) in matches[range].iter().enumerate() { - let completion = &completions[mat.candidate_id]; + let completion = &mut completions[mat.candidate_id]; + + if completion.documentation.is_none() { + if let Some(lsp_docs) = &completion.lsp_completion.documentation { + let project = project + .as_ref() + .expect("It is impossible have LSP servers without a project"); + + let language_registry = project.read(cx).languages(); + + completion.documentation = prepare_completion_documentation( + lsp_docs, + language_registry, + None, + &style.theme, + ); + } + } + let documentation = &completion.documentation; let item_ix = start_ix + ix; @@ -1121,9 +1137,7 @@ impl CompletionsMenu { ), ); - if let Some(language::Documentation::SingleLine(text)) = - documentation - { + if let Some(Documentation::SingleLine(text)) = documentation { Flex::row() .with_child(completion_label) .with_children((|| { @@ -1209,11 +1223,11 @@ impl CompletionsMenu { let documentation = &completion.documentation; match documentation { - Some(language::Documentation::MultiLinePlainText(text)) => { + Some(Documentation::MultiLinePlainText(text)) => { Some(Text::new(text.clone(), style.text.clone())) } - Some(language::Documentation::MultiLineMarkdown(parsed)) => { + Some(Documentation::MultiLineMarkdown(parsed)) => { Some(render_parsed_markdown(parsed, &style, cx)) } @@ -1221,7 +1235,7 @@ impl CompletionsMenu { } }) .contained() - .with_style(outer_container_style) + .with_style(style.autocomplete.container) .into_any() } From a881b1f5fb4b2c28a48581a90e16dba1201afe7e Mon Sep 17 00:00:00 2001 From: Julia Date: Wed, 4 Oct 2023 17:36:51 -0400 Subject: [PATCH 11/30] Wait for language to load when parsing markdown --- crates/editor/src/editor.rs | 42 ++++--- crates/editor/src/hover_popover.rs | 169 +++++++++++++++++------------ crates/language/src/buffer.rs | 6 +- crates/language/src/markdown.rs | 12 +- 4 files changed, 137 insertions(+), 92 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index cabf73b581..257abad41b 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -1018,7 +1018,6 @@ impl CompletionsMenu { return; } - // TODO: Do on background cx.spawn(|this, mut cx| async move { let request = server.request::(completion); let Some(completion_item) = request.await.log_err() else { @@ -1031,7 +1030,8 @@ impl CompletionsMenu { &language_registry, None, // TODO: Try to reasonably work out which language the completion is for &style, - ); + ) + .await; let mut completions = completions.write(); let completion = &mut completions[index]; @@ -1084,30 +1084,46 @@ impl CompletionsMenu { let style = style.clone(); move |_, range, items, cx| { let start_ix = range.start; - let mut completions = completions.write(); + let completions_guard = completions.read(); for (ix, mat) in matches[range].iter().enumerate() { - let completion = &mut completions[mat.candidate_id]; + let item_ix = start_ix + ix; + let candidate_id = mat.candidate_id; + let completion = &completions_guard[candidate_id]; - if completion.documentation.is_none() { + if item_ix == selected_item && completion.documentation.is_none() { if let Some(lsp_docs) = &completion.lsp_completion.documentation { let project = project .as_ref() .expect("It is impossible have LSP servers without a project"); - let language_registry = project.read(cx).languages(); + let lsp_docs = lsp_docs.clone(); + let lsp_docs = lsp_docs.clone(); + let language_registry = project.read(cx).languages().clone(); + let style = style.theme.clone(); + let completions = completions.clone(); - completion.documentation = prepare_completion_documentation( - lsp_docs, - language_registry, - None, - &style.theme, - ); + cx.spawn(|this, mut cx| async move { + let documentation = prepare_completion_documentation( + &lsp_docs, + &language_registry, + None, + &style, + ) + .await; + + this.update(&mut cx, |_, cx| { + let mut completions = completions.write(); + completions[candidate_id].documentation = documentation; + drop(completions); + cx.notify(); + }) + }) + .detach(); } } let documentation = &completion.documentation; - let item_ix = start_ix + ix; items.push( MouseEventHandler::new::( diff --git a/crates/editor/src/hover_popover.rs b/crates/editor/src/hover_popover.rs index 9341fa2da3..585a335bd6 100644 --- a/crates/editor/src/hover_popover.rs +++ b/crates/editor/src/hover_popover.rs @@ -7,7 +7,7 @@ use crate::{ use futures::FutureExt; use gpui::{ actions, - elements::{Flex, MouseEventHandler, Padding, ParentElement, Text}, + elements::{Empty, Flex, MouseEventHandler, Padding, ParentElement, Text}, fonts::HighlightStyle, platform::{CursorStyle, MouseButton}, AnyElement, AppContext, CursorRegion, Element, ModelHandle, MouseRegion, Task, ViewContext, @@ -128,7 +128,7 @@ pub fn hover_at_inlay(editor: &mut Editor, inlay_hover: InlayHover, cx: &mut Vie symbol_range: DocumentRange::Inlay(inlay_hover.range), blocks: vec![inlay_hover.tooltip], language: None, - rendered_content: None, + parsed_content: None, }; this.update(&mut cx, |this, cx| { @@ -332,7 +332,7 @@ fn show_hover( symbol_range: DocumentRange::Text(range), blocks: hover_result.contents, language: hover_result.language, - rendered_content: None, + parsed_content: None, }) }); @@ -363,12 +363,12 @@ fn show_hover( editor.hover_state.info_task = Some(task); } -fn render_blocks( +async fn render_blocks( theme_id: usize, blocks: &[HoverBlock], language_registry: &Arc, language: Option>, - style: &EditorStyle, + style: &theme::Editor, ) -> ParsedInfo { let mut text = String::new(); let mut highlights = Vec::new(); @@ -382,16 +382,19 @@ fn render_blocks( text.push_str(&block.text); } - HoverBlockKind::Markdown => markdown::parse_markdown_block( - &block.text, - language_registry, - language.clone(), - style, - &mut text, - &mut highlights, - &mut region_ranges, - &mut regions, - ), + HoverBlockKind::Markdown => { + markdown::parse_markdown_block( + &block.text, + language_registry, + language.clone(), + style, + &mut text, + &mut highlights, + &mut region_ranges, + &mut regions, + ) + .await + } HoverBlockKind::Code { language } => { if let Some(language) = language_registry @@ -482,7 +485,7 @@ pub struct InfoPopover { symbol_range: DocumentRange, pub blocks: Vec, language: Option>, - rendered_content: Option, + parsed_content: Option, } #[derive(Debug, Clone)] @@ -500,63 +503,87 @@ impl InfoPopover { style: &EditorStyle, cx: &mut ViewContext, ) -> AnyElement { - if let Some(rendered) = &self.rendered_content { - if rendered.theme_id != style.theme_id { - self.rendered_content = None; + if let Some(parsed) = &self.parsed_content { + if parsed.theme_id != style.theme_id { + self.parsed_content = None; } } - let rendered_content = self.rendered_content.get_or_insert_with(|| { - render_blocks( - style.theme_id, - &self.blocks, - self.project.read(cx).languages(), - self.language.clone(), - style, - ) - }); + let rendered = if let Some(parsed) = &self.parsed_content { + let view_id = cx.view_id(); + let regions = parsed.regions.clone(); + let code_span_background_color = style.document_highlight_read_background; + + let mut region_id = 0; + + Text::new(parsed.text.clone(), style.text.clone()) + .with_highlights(parsed.highlights.clone()) + .with_custom_runs(parsed.region_ranges.clone(), move |ix, bounds, scene, _| { + region_id += 1; + let region = regions[ix].clone(); + + if let Some(url) = region.link_url { + scene.push_cursor_region(CursorRegion { + bounds, + style: CursorStyle::PointingHand, + }); + scene.push_mouse_region( + MouseRegion::new::(view_id, region_id, bounds) + .on_click::(MouseButton::Left, move |_, _, cx| { + cx.platform().open_url(&url) + }), + ); + } + + if region.code { + scene.push_quad(gpui::Quad { + bounds, + background: Some(code_span_background_color), + border: Default::default(), + corner_radii: (2.0).into(), + }); + } + }) + .with_soft_wrap(true) + .into_any() + } else { + let theme_id = style.theme_id; + let language_registry = self.project.read(cx).languages().clone(); + let blocks = self.blocks.clone(); + let language = self.language.clone(); + let style = style.theme.clone(); + cx.spawn(|this, mut cx| async move { + let blocks = + render_blocks(theme_id, &blocks, &language_registry, language, &style).await; + _ = this.update(&mut cx, |_, cx| cx.notify()); + blocks + }) + .detach(); + + Empty::new().into_any() + }; + + // let rendered_content = self.parsed_content.get_or_insert_with(|| { + // let language_registry = self.project.read(cx).languages().clone(); + // cx.spawn(|this, mut cx| async move { + // let blocks = render_blocks( + // style.theme_id, + // &self.blocks, + // &language_registry, + // self.language.clone(), + // style, + // ) + // .await; + // this.update(&mut cx, |_, cx| cx.notify()); + // blocks + // }) + // .shared() + // }); MouseEventHandler::new::(0, cx, |_, cx| { - let mut region_id = 0; - let view_id = cx.view_id(); - - let code_span_background_color = style.document_highlight_read_background; - let regions = rendered_content.regions.clone(); Flex::column() .scrollable::(1, None, cx) - .with_child( - Text::new(rendered_content.text.clone(), style.text.clone()) - .with_highlights(rendered_content.highlights.clone()) - .with_custom_runs( - rendered_content.region_ranges.clone(), - move |ix, bounds, scene, _| { - region_id += 1; - let region = regions[ix].clone(); - if let Some(url) = region.link_url { - scene.push_cursor_region(CursorRegion { - bounds, - style: CursorStyle::PointingHand, - }); - scene.push_mouse_region( - MouseRegion::new::(view_id, region_id, bounds) - .on_click::( - MouseButton::Left, - move |_, _, cx| cx.platform().open_url(&url), - ), - ); - } - if region.code { - scene.push_quad(gpui::Quad { - bounds, - background: Some(code_span_background_color), - border: Default::default(), - corner_radii: (2.0).into(), - }); - } - }, - ) - .with_soft_wrap(true), - ) + .with_child(rendered) .contained() .with_style(style.hover_popover.container) }) @@ -877,7 +904,8 @@ mod tests { ); let style = editor.style(cx); - let rendered = render_blocks(0, &blocks, &Default::default(), None, &style); + let rendered = + smol::block_on(render_blocks(0, &blocks, &Default::default(), None, &style)); assert_eq!( rendered.text, code_str.trim(), @@ -1069,7 +1097,8 @@ mod tests { expected_styles, } in &rows[0..] { - let rendered = render_blocks(0, &blocks, &Default::default(), None, &style); + let rendered = + smol::block_on(render_blocks(0, &blocks, &Default::default(), None, &style)); let (expected_text, ranges) = marked_text_ranges(expected_marked_text, false); let expected_highlights = ranges @@ -1339,7 +1368,7 @@ mod tests { ); assert_eq!( popover - .rendered_content + .parsed_content .as_ref() .expect("should have label text for new type hint") .text, @@ -1403,7 +1432,7 @@ mod tests { ); assert_eq!( popover - .rendered_content + .parsed_content .as_ref() .expect("should have label text for struct hint") .text, diff --git a/crates/language/src/buffer.rs b/crates/language/src/buffer.rs index 344b470aa9..971494ea4f 100644 --- a/crates/language/src/buffer.rs +++ b/crates/language/src/buffer.rs @@ -7,7 +7,7 @@ pub use crate::{ use crate::{ diagnostic_set::{DiagnosticEntry, DiagnosticGroup}, language_settings::{language_settings, LanguageSettings}, - markdown, + markdown::parse_markdown, outline::OutlineItem, syntax_map::{ SyntaxLayerInfo, SyntaxMap, SyntaxMapCapture, SyntaxMapCaptures, SyntaxMapMatches, @@ -145,7 +145,7 @@ pub struct Diagnostic { pub is_unnecessary: bool, } -pub fn prepare_completion_documentation( +pub async fn prepare_completion_documentation( documentation: &lsp::Documentation, language_registry: &Arc, language: Option>, @@ -170,7 +170,7 @@ pub fn prepare_completion_documentation( } lsp::MarkupKind::Markdown => { - let parsed = markdown::parse_markdown(value, language_registry, language, style); + let parsed = parse_markdown(value, language_registry, language, style).await; Some(Documentation::MultiLineMarkdown(parsed)) } }, diff --git a/crates/language/src/markdown.rs b/crates/language/src/markdown.rs index c56a676378..de5c7e8b09 100644 --- a/crates/language/src/markdown.rs +++ b/crates/language/src/markdown.rs @@ -2,7 +2,6 @@ use std::ops::Range; use std::sync::Arc; use crate::{Language, LanguageRegistry}; -use futures::FutureExt; use gpui::fonts::{HighlightStyle, Underline, Weight}; use pulldown_cmark::{CodeBlockKind, Event, Options, Parser, Tag}; @@ -20,7 +19,7 @@ pub struct ParsedRegion { pub link_url: Option, } -pub fn parse_markdown( +pub async fn parse_markdown( markdown: &str, language_registry: &Arc, language: Option>, @@ -40,7 +39,8 @@ pub fn parse_markdown( &mut highlights, &mut region_ranges, &mut regions, - ); + ) + .await; ParsedMarkdown { text, @@ -50,7 +50,7 @@ pub fn parse_markdown( } } -pub fn parse_markdown_block( +pub async fn parse_markdown_block( markdown: &str, language_registry: &Arc, language: Option>, @@ -143,8 +143,8 @@ pub fn parse_markdown_block( current_language = if let CodeBlockKind::Fenced(language) = kind { language_registry .language_for_name(language.as_ref()) - .now_or_never() - .and_then(Result::ok) + .await + .ok() } else { language.clone() } From 8dca4c3f9ac9b923fd9bef415fb0c6de19f5becb Mon Sep 17 00:00:00 2001 From: Julia Date: Thu, 5 Oct 2023 14:40:41 -0400 Subject: [PATCH 12/30] Don't need editor style to parse markdown --- crates/editor/src/editor.rs | 118 ++++++++++++---------- crates/editor/src/hover_popover.rs | 154 ++++++----------------------- crates/language/src/buffer.rs | 3 +- crates/language/src/markdown.rs | 63 ++++++------ 4 files changed, 133 insertions(+), 205 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 257abad41b..6b2be7c719 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -60,6 +60,7 @@ use itertools::Itertools; pub use language::{char_kind, CharKind}; use language::{ language_settings::{self, all_language_settings, InlayHintSettings}, + markdown::MarkdownHighlight, point_from_lsp, prepare_completion_documentation, AutoindentMode, BracketPair, Buffer, CodeAction, CodeLabel, Completion, CursorShape, Diagnostic, DiagnosticSeverity, Documentation, File, IndentKind, IndentSize, Language, LanguageServerName, OffsetRangeExt, OffsetUtf16, Point, @@ -120,21 +121,57 @@ pub const DOCUMENT_HIGHLIGHTS_DEBOUNCE_TIMEOUT: Duration = Duration::from_millis pub const FORMAT_TIMEOUT: Duration = Duration::from_secs(2); pub fn render_parsed_markdown( - md: &language::ParsedMarkdown, - style: &EditorStyle, + parsed: &language::ParsedMarkdown, + editor_style: &EditorStyle, cx: &mut ViewContext, ) -> Text { enum RenderedMarkdown {} - let md = md.clone(); - let code_span_background_color = style.document_highlight_read_background; + let parsed = parsed.clone(); let view_id = cx.view_id(); + let code_span_background_color = editor_style.document_highlight_read_background; + let mut region_id = 0; - Text::new(md.text, style.text.clone()) - .with_highlights(md.highlights) - .with_custom_runs(md.region_ranges, move |ix, bounds, scene, _| { + + Text::new(parsed.text, editor_style.text.clone()) + .with_highlights( + parsed + .highlights + .iter() + .filter_map(|(range, highlight)| { + let highlight = match highlight { + MarkdownHighlight::Style(style) => { + let mut highlight = HighlightStyle::default(); + + if style.italic { + highlight.italic = Some(true); + } + + if style.underline { + highlight.underline = Some(fonts::Underline { + thickness: 1.0.into(), + ..Default::default() + }); + } + + if style.weight != fonts::Weight::default() { + highlight.weight = Some(style.weight); + } + + highlight + } + + MarkdownHighlight::Code(id) => id.style(&editor_style.syntax)?, + }; + + Some((range.clone(), highlight)) + }) + .collect::>(), + ) + .with_custom_runs(parsed.region_ranges, move |ix, bounds, scene, _| { region_id += 1; - let region = md.regions[ix].clone(); + let region = parsed.regions[ix].clone(); + if let Some(url) = region.link_url { scene.push_cursor_region(CursorRegion { bounds, @@ -147,6 +184,7 @@ pub fn render_parsed_markdown( }), ); } + if region.code { scene.push_quad(gpui::Quad { bounds, @@ -831,12 +869,11 @@ impl ContextMenu { fn select_first( &mut self, project: Option<&ModelHandle>, - style: theme::Editor, cx: &mut ViewContext, ) -> bool { if self.visible() { match self { - ContextMenu::Completions(menu) => menu.select_first(project, style, cx), + ContextMenu::Completions(menu) => menu.select_first(project, cx), ContextMenu::CodeActions(menu) => menu.select_first(cx), } true @@ -848,12 +885,11 @@ impl ContextMenu { fn select_prev( &mut self, project: Option<&ModelHandle>, - style: theme::Editor, cx: &mut ViewContext, ) -> bool { if self.visible() { match self { - ContextMenu::Completions(menu) => menu.select_prev(project, style, cx), + ContextMenu::Completions(menu) => menu.select_prev(project, cx), ContextMenu::CodeActions(menu) => menu.select_prev(cx), } true @@ -865,12 +901,11 @@ impl ContextMenu { fn select_next( &mut self, project: Option<&ModelHandle>, - style: theme::Editor, cx: &mut ViewContext, ) -> bool { if self.visible() { match self { - ContextMenu::Completions(menu) => menu.select_next(project, style, cx), + ContextMenu::Completions(menu) => menu.select_next(project, cx), ContextMenu::CodeActions(menu) => menu.select_next(cx), } true @@ -882,12 +917,11 @@ impl ContextMenu { fn select_last( &mut self, project: Option<&ModelHandle>, - style: theme::Editor, cx: &mut ViewContext, ) -> bool { if self.visible() { match self { - ContextMenu::Completions(menu) => menu.select_last(project, style, cx), + ContextMenu::Completions(menu) => menu.select_last(project, cx), ContextMenu::CodeActions(menu) => menu.select_last(cx), } true @@ -932,59 +966,54 @@ impl CompletionsMenu { fn select_first( &mut self, project: Option<&ModelHandle>, - style: theme::Editor, cx: &mut ViewContext, ) { self.selected_item = 0; self.list.scroll_to(ScrollTarget::Show(self.selected_item)); - self.attempt_resolve_selected_completion(project, style, cx); + self.attempt_resolve_selected_completion(project, cx); cx.notify(); } fn select_prev( &mut self, project: Option<&ModelHandle>, - style: theme::Editor, cx: &mut ViewContext, ) { if self.selected_item > 0 { self.selected_item -= 1; self.list.scroll_to(ScrollTarget::Show(self.selected_item)); } - self.attempt_resolve_selected_completion(project, style, cx); + self.attempt_resolve_selected_completion(project, cx); cx.notify(); } fn select_next( &mut self, project: Option<&ModelHandle>, - style: theme::Editor, cx: &mut ViewContext, ) { if self.selected_item + 1 < self.matches.len() { self.selected_item += 1; self.list.scroll_to(ScrollTarget::Show(self.selected_item)); } - self.attempt_resolve_selected_completion(project, style, cx); + self.attempt_resolve_selected_completion(project, cx); cx.notify(); } fn select_last( &mut self, project: Option<&ModelHandle>, - style: theme::Editor, cx: &mut ViewContext, ) { self.selected_item = self.matches.len() - 1; self.list.scroll_to(ScrollTarget::Show(self.selected_item)); - self.attempt_resolve_selected_completion(project, style, cx); + self.attempt_resolve_selected_completion(project, cx); cx.notify(); } fn attempt_resolve_selected_completion( &mut self, project: Option<&ModelHandle>, - style: theme::Editor, cx: &mut ViewContext, ) { let index = self.matches[self.selected_item].candidate_id; @@ -1029,7 +1058,6 @@ impl CompletionsMenu { &lsp_documentation, &language_registry, None, // TODO: Try to reasonably work out which language the completion is for - &style, ) .await; @@ -1097,10 +1125,8 @@ impl CompletionsMenu { .as_ref() .expect("It is impossible have LSP servers without a project"); - let lsp_docs = lsp_docs.clone(); let lsp_docs = lsp_docs.clone(); let language_registry = project.read(cx).languages().clone(); - let style = style.theme.clone(); let completions = completions.clone(); cx.spawn(|this, mut cx| async move { @@ -1108,7 +1134,6 @@ impl CompletionsMenu { &lsp_docs, &language_registry, None, - &style, ) .await; @@ -3365,11 +3390,7 @@ impl Editor { None } else { _ = this.update(&mut cx, |editor, cx| { - menu.attempt_resolve_selected_completion( - editor.project.as_ref(), - editor.style(cx).theme, - cx, - ); + menu.attempt_resolve_selected_completion(editor.project.as_ref(), cx); }); Some(menu) } @@ -5545,16 +5566,13 @@ impl Editor { return; } - if self.context_menu.is_some() { - let style = self.style(cx).theme; - if self - .context_menu - .as_mut() - .map(|menu| menu.select_last(self.project.as_ref(), style, cx)) - .unwrap_or(false) - { - return; - } + if self + .context_menu + .as_mut() + .map(|menu| menu.select_last(self.project.as_ref(), cx)) + .unwrap_or(false) + { + return; } if matches!(self.mode, EditorMode::SingleLine) { @@ -5594,30 +5612,26 @@ impl Editor { } pub fn context_menu_first(&mut self, _: &ContextMenuFirst, cx: &mut ViewContext) { - let style = self.style(cx).theme; if let Some(context_menu) = self.context_menu.as_mut() { - context_menu.select_first(self.project.as_ref(), style, cx); + context_menu.select_first(self.project.as_ref(), cx); } } pub fn context_menu_prev(&mut self, _: &ContextMenuPrev, cx: &mut ViewContext) { - let style = self.style(cx).theme; if let Some(context_menu) = self.context_menu.as_mut() { - context_menu.select_prev(self.project.as_ref(), style, cx); + context_menu.select_prev(self.project.as_ref(), cx); } } pub fn context_menu_next(&mut self, _: &ContextMenuNext, cx: &mut ViewContext) { - let style = self.style(cx).theme; if let Some(context_menu) = self.context_menu.as_mut() { - context_menu.select_next(self.project.as_ref(), style, cx); + context_menu.select_next(self.project.as_ref(), cx); } } pub fn context_menu_last(&mut self, _: &ContextMenuLast, cx: &mut ViewContext) { - let style = self.style(cx).theme; if let Some(context_menu) = self.context_menu.as_mut() { - context_menu.select_last(self.project.as_ref(), style, cx); + context_menu.select_last(self.project.as_ref(), cx); } } diff --git a/crates/editor/src/hover_popover.rs b/crates/editor/src/hover_popover.rs index 585a335bd6..51fe27e58a 100644 --- a/crates/editor/src/hover_popover.rs +++ b/crates/editor/src/hover_popover.rs @@ -8,13 +8,11 @@ use futures::FutureExt; use gpui::{ actions, elements::{Empty, Flex, MouseEventHandler, Padding, ParentElement, Text}, - fonts::HighlightStyle, platform::{CursorStyle, MouseButton}, - AnyElement, AppContext, CursorRegion, Element, ModelHandle, MouseRegion, Task, ViewContext, + AnyElement, AppContext, Element, ModelHandle, Task, ViewContext, }; use language::{ - markdown::{self, ParsedRegion}, - Bias, DiagnosticEntry, DiagnosticSeverity, Language, LanguageRegistry, + markdown, Bias, DiagnosticEntry, DiagnosticSeverity, Language, LanguageRegistry, ParsedMarkdown, }; use project::{HoverBlock, HoverBlockKind, InlayHintLabelPart, Project}; use std::{ops::Range, sync::Arc, time::Duration}; @@ -363,13 +361,11 @@ fn show_hover( editor.hover_state.info_task = Some(task); } -async fn render_blocks( - theme_id: usize, +async fn parse_blocks( blocks: &[HoverBlock], language_registry: &Arc, language: Option>, - style: &theme::Editor, -) -> ParsedInfo { +) -> markdown::ParsedMarkdown { let mut text = String::new(); let mut highlights = Vec::new(); let mut region_ranges = Vec::new(); @@ -387,7 +383,6 @@ async fn render_blocks( &block.text, language_registry, language.clone(), - style, &mut text, &mut highlights, &mut region_ranges, @@ -402,13 +397,7 @@ async fn render_blocks( .now_or_never() .and_then(Result::ok) { - markdown::highlight_code( - &mut text, - &mut highlights, - &block.text, - &language, - style, - ); + markdown::highlight_code(&mut text, &mut highlights, &block.text, &language); } else { text.push_str(&block.text); } @@ -416,8 +405,7 @@ async fn render_blocks( } } - ParsedInfo { - theme_id, + ParsedMarkdown { text: text.trim().to_string(), highlights, region_ranges, @@ -485,16 +473,7 @@ pub struct InfoPopover { symbol_range: DocumentRange, pub blocks: Vec, language: Option>, - parsed_content: Option, -} - -#[derive(Debug, Clone)] -struct ParsedInfo { - theme_id: usize, - text: String, - highlights: Vec<(Range, HighlightStyle)>, - region_ranges: Vec>, - regions: Vec, + parsed_content: Option, } impl InfoPopover { @@ -503,58 +482,14 @@ impl InfoPopover { style: &EditorStyle, cx: &mut ViewContext, ) -> AnyElement { - if let Some(parsed) = &self.parsed_content { - if parsed.theme_id != style.theme_id { - self.parsed_content = None; - } - } - let rendered = if let Some(parsed) = &self.parsed_content { - let view_id = cx.view_id(); - let regions = parsed.regions.clone(); - let code_span_background_color = style.document_highlight_read_background; - - let mut region_id = 0; - - Text::new(parsed.text.clone(), style.text.clone()) - .with_highlights(parsed.highlights.clone()) - .with_custom_runs(parsed.region_ranges.clone(), move |ix, bounds, scene, _| { - region_id += 1; - let region = regions[ix].clone(); - - if let Some(url) = region.link_url { - scene.push_cursor_region(CursorRegion { - bounds, - style: CursorStyle::PointingHand, - }); - scene.push_mouse_region( - MouseRegion::new::(view_id, region_id, bounds) - .on_click::(MouseButton::Left, move |_, _, cx| { - cx.platform().open_url(&url) - }), - ); - } - - if region.code { - scene.push_quad(gpui::Quad { - bounds, - background: Some(code_span_background_color), - border: Default::default(), - corner_radii: (2.0).into(), - }); - } - }) - .with_soft_wrap(true) - .into_any() + crate::render_parsed_markdown(parsed, style, cx).into_any() } else { - let theme_id = style.theme_id; let language_registry = self.project.read(cx).languages().clone(); let blocks = self.blocks.clone(); let language = self.language.clone(); - let style = style.theme.clone(); cx.spawn(|this, mut cx| async move { - let blocks = - render_blocks(theme_id, &blocks, &language_registry, language, &style).await; + let blocks = parse_blocks(&blocks, &language_registry, language).await; _ = this.update(&mut cx, |_, cx| cx.notify()); blocks }) @@ -563,23 +498,6 @@ impl InfoPopover { Empty::new().into_any() }; - // let rendered_content = self.parsed_content.get_or_insert_with(|| { - // let language_registry = self.project.read(cx).languages().clone(); - // cx.spawn(|this, mut cx| async move { - // let blocks = render_blocks( - // style.theme_id, - // &self.blocks, - // &language_registry, - // self.language.clone(), - // style, - // ) - // .await; - // this.update(&mut cx, |_, cx| cx.notify()); - // blocks - // }) - // .shared() - // }); - MouseEventHandler::new::(0, cx, |_, cx| { Flex::column() .scrollable::(1, None, cx) @@ -678,9 +596,12 @@ mod tests { test::editor_lsp_test_context::EditorLspTestContext, }; use collections::BTreeSet; - use gpui::fonts::{Underline, Weight}; + use gpui::fonts::Weight; use indoc::indoc; - use language::{language_settings::InlayHintSettings, Diagnostic, DiagnosticSet}; + use language::{ + language_settings::InlayHintSettings, markdown::MarkdownHighlightStyle, Diagnostic, + DiagnosticSet, + }; use lsp::LanguageServerId; use project::{HoverBlock, HoverBlockKind}; use smol::stream::StreamExt; @@ -893,7 +814,7 @@ mod tests { .await; cx.condition(|editor, _| editor.hover_state.visible()).await; - cx.editor(|editor, cx| { + cx.editor(|editor, _| { let blocks = editor.hover_state.info_popover.clone().unwrap().blocks; assert_eq!( blocks, @@ -903,9 +824,7 @@ mod tests { }], ); - let style = editor.style(cx); - let rendered = - smol::block_on(render_blocks(0, &blocks, &Default::default(), None, &style)); + let rendered = smol::block_on(parse_blocks(&blocks, &Default::default(), None)); assert_eq!( rendered.text, code_str.trim(), @@ -984,16 +903,17 @@ mod tests { #[gpui::test] fn test_render_blocks(cx: &mut gpui::TestAppContext) { + use markdown::MarkdownHighlight; + init_test(cx, |_| {}); cx.add_window(|cx| { let editor = Editor::single_line(None, cx); - let style = editor.style(cx); struct Row { blocks: Vec, expected_marked_text: String, - expected_styles: Vec, + expected_styles: Vec, } let rows = &[ @@ -1004,10 +924,10 @@ mod tests { kind: HoverBlockKind::Markdown, }], expected_marked_text: "one «two» three".to_string(), - expected_styles: vec![HighlightStyle { - weight: Some(Weight::BOLD), + expected_styles: vec![MarkdownHighlight::Style(MarkdownHighlightStyle { + weight: Weight::BOLD, ..Default::default() - }], + })], }, // Links Row { @@ -1016,13 +936,10 @@ mod tests { kind: HoverBlockKind::Markdown, }], expected_marked_text: "one «two» three".to_string(), - expected_styles: vec![HighlightStyle { - underline: Some(Underline { - thickness: 1.0.into(), - ..Default::default() - }), + expected_styles: vec![MarkdownHighlight::Style(MarkdownHighlightStyle { + underline: true, ..Default::default() - }], + })], }, // Lists Row { @@ -1047,13 +964,10 @@ mod tests { - «c» - d" .unindent(), - expected_styles: vec![HighlightStyle { - underline: Some(Underline { - thickness: 1.0.into(), - ..Default::default() - }), + expected_styles: vec![MarkdownHighlight::Style(MarkdownHighlightStyle { + underline: true, ..Default::default() - }], + })], }, // Multi-paragraph list items Row { @@ -1081,13 +995,10 @@ mod tests { - ten - six" .unindent(), - expected_styles: vec![HighlightStyle { - underline: Some(Underline { - thickness: 1.0.into(), - ..Default::default() - }), + expected_styles: vec![MarkdownHighlight::Style(MarkdownHighlightStyle { + underline: true, ..Default::default() - }], + })], }, ]; @@ -1097,8 +1008,7 @@ mod tests { expected_styles, } in &rows[0..] { - let rendered = - smol::block_on(render_blocks(0, &blocks, &Default::default(), None, &style)); + let rendered = smol::block_on(parse_blocks(&blocks, &Default::default(), None)); let (expected_text, ranges) = marked_text_ranges(expected_marked_text, false); let expected_highlights = ranges diff --git a/crates/language/src/buffer.rs b/crates/language/src/buffer.rs index 971494ea4f..d318a87b40 100644 --- a/crates/language/src/buffer.rs +++ b/crates/language/src/buffer.rs @@ -149,7 +149,6 @@ pub async fn prepare_completion_documentation( documentation: &lsp::Documentation, language_registry: &Arc, language: Option>, - style: &theme::Editor, ) -> Option { match documentation { lsp::Documentation::String(text) => { @@ -170,7 +169,7 @@ pub async fn prepare_completion_documentation( } lsp::MarkupKind::Markdown => { - let parsed = parse_markdown(value, language_registry, language, style).await; + let parsed = parse_markdown(value, language_registry, language).await; Some(Documentation::MultiLineMarkdown(parsed)) } }, diff --git a/crates/language/src/markdown.rs b/crates/language/src/markdown.rs index de5c7e8b09..9f29e7cb88 100644 --- a/crates/language/src/markdown.rs +++ b/crates/language/src/markdown.rs @@ -1,18 +1,31 @@ use std::ops::Range; use std::sync::Arc; -use crate::{Language, LanguageRegistry}; -use gpui::fonts::{HighlightStyle, Underline, Weight}; +use crate::{HighlightId, Language, LanguageRegistry}; +use gpui::fonts::Weight; use pulldown_cmark::{CodeBlockKind, Event, Options, Parser, Tag}; #[derive(Debug, Clone)] pub struct ParsedMarkdown { pub text: String, - pub highlights: Vec<(Range, HighlightStyle)>, + pub highlights: Vec<(Range, MarkdownHighlight)>, pub region_ranges: Vec>, pub regions: Vec, } +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum MarkdownHighlight { + Style(MarkdownHighlightStyle), + Code(HighlightId), +} + +#[derive(Debug, Clone, Default, PartialEq, Eq)] +pub struct MarkdownHighlightStyle { + pub italic: bool, + pub underline: bool, + pub weight: Weight, +} + #[derive(Debug, Clone)] pub struct ParsedRegion { pub code: bool, @@ -23,7 +36,6 @@ pub async fn parse_markdown( markdown: &str, language_registry: &Arc, language: Option>, - style: &theme::Editor, ) -> ParsedMarkdown { let mut text = String::new(); let mut highlights = Vec::new(); @@ -34,7 +46,6 @@ pub async fn parse_markdown( markdown, language_registry, language, - style, &mut text, &mut highlights, &mut region_ranges, @@ -54,9 +65,8 @@ pub async fn parse_markdown_block( markdown: &str, language_registry: &Arc, language: Option>, - style: &theme::Editor, text: &mut String, - highlights: &mut Vec<(Range, HighlightStyle)>, + highlights: &mut Vec<(Range, MarkdownHighlight)>, region_ranges: &mut Vec>, regions: &mut Vec, ) { @@ -71,16 +81,16 @@ pub async fn parse_markdown_block( match event { Event::Text(t) => { if let Some(language) = ¤t_language { - highlight_code(text, highlights, t.as_ref(), language, style); + highlight_code(text, highlights, t.as_ref(), language); } else { text.push_str(t.as_ref()); - let mut style = HighlightStyle::default(); + let mut style = MarkdownHighlightStyle::default(); if bold_depth > 0 { - style.weight = Some(Weight::BOLD); + style.weight = Weight::BOLD; } if italic_depth > 0 { - style.italic = Some(true); + style.italic = true; } if let Some(link_url) = link_url.clone() { region_ranges.push(prev_len..text.len()); @@ -88,22 +98,22 @@ pub async fn parse_markdown_block( link_url: Some(link_url), code: false, }); - style.underline = Some(Underline { - thickness: 1.0.into(), - ..Default::default() - }); + style.underline = true; } - if style != HighlightStyle::default() { + if style != MarkdownHighlightStyle::default() { let mut new_highlight = true; - if let Some((last_range, last_style)) = highlights.last_mut() { + if let Some((last_range, MarkdownHighlight::Style(last_style))) = + highlights.last_mut() + { if last_range.end == prev_len && last_style == &style { last_range.end = text.len(); new_highlight = false; } } if new_highlight { - highlights.push((prev_len..text.len(), style)); + let range = prev_len..text.len(); + highlights.push((range, MarkdownHighlight::Style(style))); } } } @@ -115,13 +125,10 @@ pub async fn parse_markdown_block( if link_url.is_some() { highlights.push(( prev_len..text.len(), - HighlightStyle { - underline: Some(Underline { - thickness: 1.0.into(), - ..Default::default() - }), + MarkdownHighlight::Style(MarkdownHighlightStyle { + underline: true, ..Default::default() - }, + }), )); } regions.push(ParsedRegion { @@ -204,17 +211,15 @@ pub async fn parse_markdown_block( pub fn highlight_code( text: &mut String, - highlights: &mut Vec<(Range, HighlightStyle)>, + highlights: &mut Vec<(Range, MarkdownHighlight)>, content: &str, language: &Arc, - style: &theme::Editor, ) { let prev_len = text.len(); text.push_str(content); for (range, highlight_id) in language.highlight_text(&content.into(), 0..content.len()) { - if let Some(style) = highlight_id.style(&style.syntax) { - highlights.push((prev_len + range.start..prev_len + range.end, style)); - } + let highlight = MarkdownHighlight::Code(highlight_id); + highlights.push((prev_len + range.start..prev_len + range.end, highlight)); } } From 32a29cd4d32d447c00af9157c99f45d57d219cda Mon Sep 17 00:00:00 2001 From: Julia Date: Thu, 5 Oct 2023 23:57:01 -0400 Subject: [PATCH 13/30] Unbork info popover parsing/rendering and make better --- crates/editor/src/hover_popover.rs | 105 +++++++++++++---------------- 1 file changed, 45 insertions(+), 60 deletions(-) diff --git a/crates/editor/src/hover_popover.rs b/crates/editor/src/hover_popover.rs index 51fe27e58a..7917d57865 100644 --- a/crates/editor/src/hover_popover.rs +++ b/crates/editor/src/hover_popover.rs @@ -7,7 +7,7 @@ use crate::{ use futures::FutureExt; use gpui::{ actions, - elements::{Empty, Flex, MouseEventHandler, Padding, ParentElement, Text}, + elements::{Flex, MouseEventHandler, Padding, ParentElement, Text}, platform::{CursorStyle, MouseButton}, AnyElement, AppContext, Element, ModelHandle, Task, ViewContext, }; @@ -121,12 +121,15 @@ pub fn hover_at_inlay(editor: &mut Editor, inlay_hover: InlayHover, cx: &mut Vie this.hover_state.diagnostic_popover = None; })?; + let language_registry = project.update(&mut cx, |p, _| p.languages().clone()); + let blocks = vec![inlay_hover.tooltip]; + let parsed_content = parse_blocks(&blocks, &language_registry, None).await; + let hover_popover = InfoPopover { project: project.clone(), symbol_range: DocumentRange::Inlay(inlay_hover.range), - blocks: vec![inlay_hover.tooltip], - language: None, - parsed_content: None, + blocks, + parsed_content, }; this.update(&mut cx, |this, cx| { @@ -304,35 +307,38 @@ fn show_hover( }); })?; - // Construct new hover popover from hover request - let hover_popover = hover_request.await.ok().flatten().and_then(|hover_result| { - if hover_result.is_empty() { - return None; + let hover_result = hover_request.await.ok().flatten(); + let hover_popover = match hover_result { + Some(hover_result) if !hover_result.is_empty() => { + // Create symbol range of anchors for highlighting and filtering of future requests. + let range = if let Some(range) = hover_result.range { + let start = snapshot + .buffer_snapshot + .anchor_in_excerpt(excerpt_id.clone(), range.start); + let end = snapshot + .buffer_snapshot + .anchor_in_excerpt(excerpt_id.clone(), range.end); + + start..end + } else { + anchor..anchor + }; + + let language_registry = project.update(&mut cx, |p, _| p.languages().clone()); + let blocks = hover_result.contents; + let language = hover_result.language; + let parsed_content = parse_blocks(&blocks, &language_registry, language).await; + + Some(InfoPopover { + project: project.clone(), + symbol_range: DocumentRange::Text(range), + blocks, + parsed_content, + }) } - // Create symbol range of anchors for highlighting and filtering - // of future requests. - let range = if let Some(range) = hover_result.range { - let start = snapshot - .buffer_snapshot - .anchor_in_excerpt(excerpt_id.clone(), range.start); - let end = snapshot - .buffer_snapshot - .anchor_in_excerpt(excerpt_id.clone(), range.end); - - start..end - } else { - anchor..anchor - }; - - Some(InfoPopover { - project: project.clone(), - symbol_range: DocumentRange::Text(range), - blocks: hover_result.contents, - language: hover_result.language, - parsed_content: None, - }) - }); + _ => None, + }; this.update(&mut cx, |this, cx| { if let Some(symbol_range) = hover_popover @@ -472,8 +478,7 @@ pub struct InfoPopover { pub project: ModelHandle, symbol_range: DocumentRange, pub blocks: Vec, - language: Option>, - parsed_content: Option, + parsed_content: ParsedMarkdown, } impl InfoPopover { @@ -482,26 +487,14 @@ impl InfoPopover { style: &EditorStyle, cx: &mut ViewContext, ) -> AnyElement { - let rendered = if let Some(parsed) = &self.parsed_content { - crate::render_parsed_markdown(parsed, style, cx).into_any() - } else { - let language_registry = self.project.read(cx).languages().clone(); - let blocks = self.blocks.clone(); - let language = self.language.clone(); - cx.spawn(|this, mut cx| async move { - let blocks = parse_blocks(&blocks, &language_registry, language).await; - _ = this.update(&mut cx, |_, cx| cx.notify()); - blocks - }) - .detach(); - - Empty::new().into_any() - }; - MouseEventHandler::new::(0, cx, |_, cx| { Flex::column() .scrollable::(1, None, cx) - .with_child(rendered) + .with_child(crate::render_parsed_markdown( + &self.parsed_content, + style, + cx, + )) .contained() .with_style(style.hover_popover.container) }) @@ -1277,11 +1270,7 @@ mod tests { "Popover range should match the new type label part" ); assert_eq!( - popover - .parsed_content - .as_ref() - .expect("should have label text for new type hint") - .text, + popover.parsed_content.text, format!("A tooltip for `{new_type_label}`"), "Rendered text should not anyhow alter backticks" ); @@ -1341,11 +1330,7 @@ mod tests { "Popover range should match the struct label part" ); assert_eq!( - popover - .parsed_content - .as_ref() - .expect("should have label text for struct hint") - .text, + popover.parsed_content.text, format!("A tooltip for {struct_label}"), "Rendered markdown element should remove backticks from text" ); From 9d8cff1275e8bfa6a6f08d48c286cdc9a8a5ab6e Mon Sep 17 00:00:00 2001 From: Julia Date: Fri, 6 Oct 2023 00:17:36 -0400 Subject: [PATCH 14/30] If documentation included in original completion then parse up front --- crates/editor/src/editor.rs | 51 +++++-------------------------- crates/project/src/lsp_command.rs | 27 ++++++++++++---- 2 files changed, 28 insertions(+), 50 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 6b2be7c719..6585611040 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -61,10 +61,10 @@ pub use language::{char_kind, CharKind}; use language::{ language_settings::{self, all_language_settings, InlayHintSettings}, markdown::MarkdownHighlight, - point_from_lsp, prepare_completion_documentation, AutoindentMode, BracketPair, Buffer, - CodeAction, CodeLabel, Completion, CursorShape, Diagnostic, DiagnosticSeverity, Documentation, - File, IndentKind, IndentSize, Language, LanguageServerName, OffsetRangeExt, OffsetUtf16, Point, - Selection, SelectionGoal, TransactionId, + point_from_lsp, AutoindentMode, BracketPair, Buffer, CodeAction, CodeLabel, Completion, + CursorShape, Diagnostic, DiagnosticSeverity, Documentation, File, IndentKind, IndentSize, + Language, LanguageServerName, OffsetRangeExt, OffsetUtf16, Point, Selection, SelectionGoal, + TransactionId, }; use link_go_to_definition::{ hide_link_definition, show_link_definition, GoToDefinitionLink, InlayHighlight, @@ -940,12 +940,11 @@ impl ContextMenu { fn render( &self, cursor_position: DisplayPoint, - editor: &Editor, style: EditorStyle, cx: &mut ViewContext, ) -> (DisplayPoint, AnyElement) { match self { - ContextMenu::Completions(menu) => (cursor_position, menu.render(editor, style, cx)), + ContextMenu::Completions(menu) => (cursor_position, menu.render(style, cx)), ContextMenu::CodeActions(menu) => menu.render(cursor_position, style, cx), } } @@ -1077,12 +1076,7 @@ impl CompletionsMenu { !self.matches.is_empty() } - fn render( - &self, - editor: &Editor, - style: EditorStyle, - cx: &mut ViewContext, - ) -> AnyElement { + fn render(&self, style: EditorStyle, cx: &mut ViewContext) -> AnyElement { enum CompletionTag {} let widest_completion_ix = self @@ -1103,7 +1097,6 @@ impl CompletionsMenu { }) .map(|(ix, _)| ix); - let project = editor.project.clone(); let completions = self.completions.clone(); let matches = self.matches.clone(); let selected_item = self.selected_item; @@ -1118,36 +1111,6 @@ impl CompletionsMenu { let item_ix = start_ix + ix; let candidate_id = mat.candidate_id; let completion = &completions_guard[candidate_id]; - - if item_ix == selected_item && completion.documentation.is_none() { - if let Some(lsp_docs) = &completion.lsp_completion.documentation { - let project = project - .as_ref() - .expect("It is impossible have LSP servers without a project"); - - let lsp_docs = lsp_docs.clone(); - let language_registry = project.read(cx).languages().clone(); - let completions = completions.clone(); - - cx.spawn(|this, mut cx| async move { - let documentation = prepare_completion_documentation( - &lsp_docs, - &language_registry, - None, - ) - .await; - - this.update(&mut cx, |_, cx| { - let mut completions = completions.write(); - completions[candidate_id].documentation = documentation; - drop(completions); - cx.notify(); - }) - }) - .detach(); - } - } - let documentation = &completion.documentation; items.push( @@ -4201,7 +4164,7 @@ impl Editor { ) -> Option<(DisplayPoint, AnyElement)> { self.context_menu .as_ref() - .map(|menu| menu.render(cursor_position, self, style, cx)) + .map(|menu| menu.render(cursor_position, style, cx)) } fn show_context_menu(&mut self, menu: ContextMenu, cx: &mut ViewContext) { diff --git a/crates/project/src/lsp_command.rs b/crates/project/src/lsp_command.rs index 400dbe2abf..c71b378da6 100644 --- a/crates/project/src/lsp_command.rs +++ b/crates/project/src/lsp_command.rs @@ -10,7 +10,7 @@ use futures::future; use gpui::{AppContext, AsyncAppContext, ModelHandle}; use language::{ language_settings::{language_settings, InlayHintKind}, - point_from_lsp, point_to_lsp, + point_from_lsp, point_to_lsp, prepare_completion_documentation, proto::{deserialize_anchor, deserialize_version, serialize_anchor, serialize_version}, range_from_lsp, range_to_lsp, Anchor, Bias, Buffer, BufferSnapshot, CachedLspAdapter, CharKind, CodeAction, Completion, OffsetRangeExt, PointUtf16, ToOffset, ToPointUtf16, Transaction, @@ -1341,7 +1341,7 @@ impl LspCommand for GetCompletions { async fn response_from_lsp( self, completions: Option, - _: ModelHandle, + project: ModelHandle, buffer: ModelHandle, server_id: LanguageServerId, cx: AsyncAppContext, @@ -1361,7 +1361,8 @@ impl LspCommand for GetCompletions { Vec::new() }; - let completions = buffer.read_with(&cx, |buffer, _| { + let completions = buffer.read_with(&cx, |buffer, cx| { + let language_registry = project.read(cx).languages().clone(); let language = buffer.language().cloned(); let snapshot = buffer.snapshot(); let clipped_position = buffer.clip_point_utf16(Unclipped(self.position), Bias::Left); @@ -1453,14 +1454,28 @@ impl LspCommand for GetCompletions { } }; - let language = language.clone(); LineEnding::normalize(&mut new_text); + let language_registry = language_registry.clone(); + let language = language.clone(); + Some(async move { let mut label = None; - if let Some(language) = language { + if let Some(language) = language.as_ref() { language.process_completion(&mut lsp_completion).await; label = language.label_for_completion(&lsp_completion).await; } + + let documentation = if let Some(lsp_docs) = &lsp_completion.documentation { + prepare_completion_documentation( + lsp_docs, + &language_registry, + language.clone(), + ) + .await + } else { + None + }; + Completion { old_range, new_text, @@ -1470,7 +1485,7 @@ impl LspCommand for GetCompletions { lsp_completion.filter_text.as_deref(), ) }), - documentation: None, + documentation, server_id, lsp_completion, } From f18f870206bf274a0e9a9dfd92efd8d4ed4b5c82 Mon Sep 17 00:00:00 2001 From: Julia Date: Fri, 6 Oct 2023 11:56:55 -0400 Subject: [PATCH 15/30] Re-enable language servers --- crates/zed/src/languages.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/zed/src/languages.rs b/crates/zed/src/languages.rs index 3b65255a3d..04e5292a7d 100644 --- a/crates/zed/src/languages.rs +++ b/crates/zed/src/languages.rs @@ -128,8 +128,8 @@ pub fn init( "tsx", tree_sitter_typescript::language_tsx(), vec![ - // Arc::new(typescript::TypeScriptLspAdapter::new(node_runtime.clone())), - // Arc::new(typescript::EsLintLspAdapter::new(node_runtime.clone())), + Arc::new(typescript::TypeScriptLspAdapter::new(node_runtime.clone())), + Arc::new(typescript::EsLintLspAdapter::new(node_runtime.clone())), Arc::new(tailwind::TailwindLspAdapter::new(node_runtime.clone())), ], ); From 7020050b069474ab9145cef6578a95d68f217f43 Mon Sep 17 00:00:00 2001 From: Julia Date: Mon, 9 Oct 2023 14:28:53 -0400 Subject: [PATCH 16/30] Fix `hover_popover.rs` after bad rebase --- crates/editor/src/editor.rs | 35 ++------ crates/editor/src/hover_popover.rs | 128 +++++++++++++---------------- crates/language/src/markdown.rs | 31 ++++++- 3 files changed, 92 insertions(+), 102 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 6585611040..b8c9690b90 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -60,7 +60,6 @@ use itertools::Itertools; pub use language::{char_kind, CharKind}; use language::{ language_settings::{self, all_language_settings, InlayHintSettings}, - markdown::MarkdownHighlight, point_from_lsp, AutoindentMode, BracketPair, Buffer, CodeAction, CodeLabel, Completion, CursorShape, Diagnostic, DiagnosticSeverity, Documentation, File, IndentKind, IndentSize, Language, LanguageServerName, OffsetRangeExt, OffsetUtf16, Point, Selection, SelectionGoal, @@ -139,45 +138,21 @@ pub fn render_parsed_markdown( .highlights .iter() .filter_map(|(range, highlight)| { - let highlight = match highlight { - MarkdownHighlight::Style(style) => { - let mut highlight = HighlightStyle::default(); - - if style.italic { - highlight.italic = Some(true); - } - - if style.underline { - highlight.underline = Some(fonts::Underline { - thickness: 1.0.into(), - ..Default::default() - }); - } - - if style.weight != fonts::Weight::default() { - highlight.weight = Some(style.weight); - } - - highlight - } - - MarkdownHighlight::Code(id) => id.style(&editor_style.syntax)?, - }; - + let highlight = highlight.to_highlight_style(&editor_style.syntax)?; Some((range.clone(), highlight)) }) .collect::>(), ) - .with_custom_runs(parsed.region_ranges, move |ix, bounds, scene, _| { + .with_custom_runs(parsed.region_ranges, move |ix, bounds, cx| { region_id += 1; let region = parsed.regions[ix].clone(); if let Some(url) = region.link_url { - scene.push_cursor_region(CursorRegion { + cx.scene().push_cursor_region(CursorRegion { bounds, style: CursorStyle::PointingHand, }); - scene.push_mouse_region( + cx.scene().push_mouse_region( MouseRegion::new::(view_id, region_id, bounds) .on_click::(MouseButton::Left, move |_, _, cx| { cx.platform().open_url(&url) @@ -186,7 +161,7 @@ pub fn render_parsed_markdown( } if region.code { - scene.push_quad(gpui::Quad { + cx.scene().push_quad(gpui::Quad { bounds, background: Some(code_span_background_color), border: Default::default(), diff --git a/crates/editor/src/hover_popover.rs b/crates/editor/src/hover_popover.rs index 7917d57865..d5ccb481b2 100644 --- a/crates/editor/src/hover_popover.rs +++ b/crates/editor/src/hover_popover.rs @@ -1,6 +1,6 @@ use crate::{ display_map::{InlayOffset, ToDisplayPoint}, - link_go_to_definition::{DocumentRange, InlayRange}, + link_go_to_definition::{InlayHighlight, RangeInEditor}, Anchor, AnchorRangeExt, DisplayPoint, Editor, EditorSettings, EditorSnapshot, EditorStyle, ExcerptId, RangeToAnchorExt, }; @@ -51,19 +51,18 @@ pub fn hover_at(editor: &mut Editor, point: Option, cx: &mut ViewC pub struct InlayHover { pub excerpt: ExcerptId, - pub triggered_from: InlayOffset, - pub range: InlayRange, + pub range: InlayHighlight, pub tooltip: HoverBlock, } pub fn find_hovered_hint_part( label_parts: Vec, - hint_range: Range, + hint_start: InlayOffset, hovered_offset: InlayOffset, ) -> Option<(InlayHintLabelPart, Range)> { - if hovered_offset >= hint_range.start && hovered_offset <= hint_range.end { - let mut hovered_character = (hovered_offset - hint_range.start).0; - let mut part_start = hint_range.start; + if hovered_offset >= hint_start { + let mut hovered_character = (hovered_offset - hint_start).0; + let mut part_start = hint_start; for part in label_parts { let part_len = part.value.chars().count(); if hovered_character > part_len { @@ -89,10 +88,8 @@ pub fn hover_at_inlay(editor: &mut Editor, inlay_hover: InlayHover, cx: &mut Vie }; if let Some(InfoPopover { symbol_range, .. }) = &editor.hover_state.info_popover { - if let DocumentRange::Inlay(range) = symbol_range { - if (range.highlight_start..range.highlight_end) - .contains(&inlay_hover.triggered_from) - { + if let RangeInEditor::Inlay(range) = symbol_range { + if range == &inlay_hover.range { // Hover triggered from same location as last time. Don't show again. return; } @@ -100,18 +97,6 @@ pub fn hover_at_inlay(editor: &mut Editor, inlay_hover: InlayHover, cx: &mut Vie hide_hover(editor, cx); } - let snapshot = editor.snapshot(cx); - // Don't request again if the location is the same as the previous request - if let Some(triggered_from) = editor.hover_state.triggered_from { - if inlay_hover.triggered_from - == snapshot - .display_snapshot - .anchor_to_inlay_offset(triggered_from) - { - return; - } - } - let task = cx.spawn(|this, mut cx| { async move { cx.background() @@ -127,7 +112,7 @@ pub fn hover_at_inlay(editor: &mut Editor, inlay_hover: InlayHover, cx: &mut Vie let hover_popover = InfoPopover { project: project.clone(), - symbol_range: DocumentRange::Inlay(inlay_hover.range), + symbol_range: RangeInEditor::Inlay(inlay_hover.range.clone()), blocks, parsed_content, }; @@ -331,7 +316,7 @@ fn show_hover( Some(InfoPopover { project: project.clone(), - symbol_range: DocumentRange::Text(range), + symbol_range: RangeInEditor::Text(range), blocks, parsed_content, }) @@ -449,8 +434,8 @@ impl HoverState { self.info_popover .as_ref() .map(|info_popover| match &info_popover.symbol_range { - DocumentRange::Text(range) => &range.start, - DocumentRange::Inlay(range) => &range.inlay_position, + RangeInEditor::Text(range) => &range.start, + RangeInEditor::Inlay(range) => &range.inlay_position, }) })?; let point = anchor.to_display_point(&snapshot.display_snapshot); @@ -476,7 +461,7 @@ impl HoverState { #[derive(Debug, Clone)] pub struct InfoPopover { pub project: ModelHandle, - symbol_range: DocumentRange, + symbol_range: RangeInEditor, pub blocks: Vec, parsed_content: ParsedMarkdown, } @@ -587,14 +572,12 @@ mod tests { inlay_hint_cache::tests::{cached_hint_labels, visible_hint_labels}, link_go_to_definition::update_inlay_link_and_hover_points, test::editor_lsp_test_context::EditorLspTestContext, + InlayId, }; use collections::BTreeSet; - use gpui::fonts::Weight; + use gpui::fonts::{HighlightStyle, Underline, Weight}; use indoc::indoc; - use language::{ - language_settings::InlayHintSettings, markdown::MarkdownHighlightStyle, Diagnostic, - DiagnosticSet, - }; + use language::{language_settings::InlayHintSettings, Diagnostic, DiagnosticSet}; use lsp::LanguageServerId; use project::{HoverBlock, HoverBlockKind}; use smol::stream::StreamExt; @@ -896,17 +879,16 @@ mod tests { #[gpui::test] fn test_render_blocks(cx: &mut gpui::TestAppContext) { - use markdown::MarkdownHighlight; - init_test(cx, |_| {}); cx.add_window(|cx| { let editor = Editor::single_line(None, cx); + let style = editor.style(cx); struct Row { blocks: Vec, expected_marked_text: String, - expected_styles: Vec, + expected_styles: Vec, } let rows = &[ @@ -917,10 +899,10 @@ mod tests { kind: HoverBlockKind::Markdown, }], expected_marked_text: "one «two» three".to_string(), - expected_styles: vec![MarkdownHighlight::Style(MarkdownHighlightStyle { - weight: Weight::BOLD, + expected_styles: vec![HighlightStyle { + weight: Some(Weight::BOLD), ..Default::default() - })], + }], }, // Links Row { @@ -929,10 +911,13 @@ mod tests { kind: HoverBlockKind::Markdown, }], expected_marked_text: "one «two» three".to_string(), - expected_styles: vec![MarkdownHighlight::Style(MarkdownHighlightStyle { - underline: true, + expected_styles: vec![HighlightStyle { + underline: Some(Underline { + thickness: 1.0.into(), + ..Default::default() + }), ..Default::default() - })], + }], }, // Lists Row { @@ -957,10 +942,13 @@ mod tests { - «c» - d" .unindent(), - expected_styles: vec![MarkdownHighlight::Style(MarkdownHighlightStyle { - underline: true, + expected_styles: vec![HighlightStyle { + underline: Some(Underline { + thickness: 1.0.into(), + ..Default::default() + }), ..Default::default() - })], + }], }, // Multi-paragraph list items Row { @@ -988,10 +976,13 @@ mod tests { - ten - six" .unindent(), - expected_styles: vec![MarkdownHighlight::Style(MarkdownHighlightStyle { - underline: true, + expected_styles: vec![HighlightStyle { + underline: Some(Underline { + thickness: 1.0.into(), + ..Default::default() + }), ..Default::default() - })], + }], }, ]; @@ -1012,8 +1003,18 @@ mod tests { rendered.text, expected_text, "wrong text for input {blocks:?}" ); + + let rendered_highlights: Vec<_> = rendered + .highlights + .iter() + .filter_map(|(range, highlight)| { + let highlight = highlight.to_highlight_style(&style.syntax)?; + Some((range.clone(), highlight)) + }) + .collect(); + assert_eq!( - rendered.highlights, expected_highlights, + rendered_highlights, expected_highlights, "wrong highlights for input {blocks:?}" ); } @@ -1247,25 +1248,16 @@ mod tests { .advance_clock(Duration::from_millis(HOVER_DELAY_MILLIS + 100)); cx.foreground().run_until_parked(); cx.update_editor(|editor, cx| { - let snapshot = editor.snapshot(cx); let hover_state = &editor.hover_state; assert!(hover_state.diagnostic_popover.is_none() && hover_state.info_popover.is_some()); let popover = hover_state.info_popover.as_ref().unwrap(); let buffer_snapshot = editor.buffer().update(cx, |buffer, cx| buffer.snapshot(cx)); - let entire_inlay_start = snapshot.display_point_to_inlay_offset( - inlay_range.start.to_display_point(&snapshot), - Bias::Left, - ); - - let expected_new_type_label_start = InlayOffset(entire_inlay_start.0 + ": ".len()); assert_eq!( popover.symbol_range, - DocumentRange::Inlay(InlayRange { + RangeInEditor::Inlay(InlayHighlight { + inlay: InlayId::Hint(0), inlay_position: buffer_snapshot.anchor_at(inlay_range.start, Bias::Right), - highlight_start: expected_new_type_label_start, - highlight_end: InlayOffset( - expected_new_type_label_start.0 + new_type_label.len() - ), + range: ": ".len()..": ".len() + new_type_label.len(), }), "Popover range should match the new type label part" ); @@ -1309,23 +1301,17 @@ mod tests { .advance_clock(Duration::from_millis(HOVER_DELAY_MILLIS + 100)); cx.foreground().run_until_parked(); cx.update_editor(|editor, cx| { - let snapshot = editor.snapshot(cx); let hover_state = &editor.hover_state; assert!(hover_state.diagnostic_popover.is_none() && hover_state.info_popover.is_some()); let popover = hover_state.info_popover.as_ref().unwrap(); let buffer_snapshot = editor.buffer().update(cx, |buffer, cx| buffer.snapshot(cx)); - let entire_inlay_start = snapshot.display_point_to_inlay_offset( - inlay_range.start.to_display_point(&snapshot), - Bias::Left, - ); - let expected_struct_label_start = - InlayOffset(entire_inlay_start.0 + ": ".len() + new_type_label.len() + "<".len()); assert_eq!( popover.symbol_range, - DocumentRange::Inlay(InlayRange { + RangeInEditor::Inlay(InlayHighlight { + inlay: InlayId::Hint(0), inlay_position: buffer_snapshot.anchor_at(inlay_range.start, Bias::Right), - highlight_start: expected_struct_label_start, - highlight_end: InlayOffset(expected_struct_label_start.0 + struct_label.len()), + range: ": ".len() + new_type_label.len() + "<".len() + ..": ".len() + new_type_label.len() + "<".len() + struct_label.len(), }), "Popover range should match the struct label part" ); diff --git a/crates/language/src/markdown.rs b/crates/language/src/markdown.rs index 9f29e7cb88..8be15e81f6 100644 --- a/crates/language/src/markdown.rs +++ b/crates/language/src/markdown.rs @@ -2,7 +2,7 @@ use std::ops::Range; use std::sync::Arc; use crate::{HighlightId, Language, LanguageRegistry}; -use gpui::fonts::Weight; +use gpui::fonts::{self, HighlightStyle, Weight}; use pulldown_cmark::{CodeBlockKind, Event, Options, Parser, Tag}; #[derive(Debug, Clone)] @@ -19,6 +19,35 @@ pub enum MarkdownHighlight { Code(HighlightId), } +impl MarkdownHighlight { + pub fn to_highlight_style(&self, theme: &theme::SyntaxTheme) -> Option { + match self { + MarkdownHighlight::Style(style) => { + let mut highlight = HighlightStyle::default(); + + if style.italic { + highlight.italic = Some(true); + } + + if style.underline { + highlight.underline = Some(fonts::Underline { + thickness: 1.0.into(), + ..Default::default() + }); + } + + if style.weight != fonts::Weight::default() { + highlight.weight = Some(style.weight); + } + + Some(highlight) + } + + MarkdownHighlight::Code(id) => id.style(theme), + } + } +} + #[derive(Debug, Clone, Default, PartialEq, Eq)] pub struct MarkdownHighlightStyle { pub italic: bool, From 354882f2c00d877defaf050ae8a34451ef5fa851 Mon Sep 17 00:00:00 2001 From: Julia Date: Tue, 10 Oct 2023 00:16:15 -0400 Subject: [PATCH 17/30] Enable completion menu to resolve documentation when guest --- crates/collab/src/rpc.rs | 1 + crates/editor/src/editor.rs | 74 ++++++++++++-- crates/language/src/buffer.rs | 13 +-- crates/project/src/lsp_command.rs | 12 ++- crates/project/src/project.rs | 35 +++++++ crates/rpc/proto/zed.proto | 157 ++++++++++++++++-------------- crates/rpc/src/proto.rs | 7 ++ crates/rpc/src/rpc.rs | 2 +- 8 files changed, 207 insertions(+), 94 deletions(-) diff --git a/crates/collab/src/rpc.rs b/crates/collab/src/rpc.rs index 5eb434e167..ed09cde061 100644 --- a/crates/collab/src/rpc.rs +++ b/crates/collab/src/rpc.rs @@ -224,6 +224,7 @@ impl Server { .add_request_handler(forward_project_request::) .add_request_handler(forward_project_request::) .add_request_handler(forward_project_request::) + .add_request_handler(forward_project_request::) .add_request_handler(forward_project_request::) .add_request_handler(forward_project_request::) .add_request_handler(forward_project_request::) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index b8c9690b90..3143b19629 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -60,10 +60,10 @@ use itertools::Itertools; pub use language::{char_kind, CharKind}; use language::{ language_settings::{self, all_language_settings, InlayHintSettings}, - point_from_lsp, AutoindentMode, BracketPair, Buffer, CodeAction, CodeLabel, Completion, - CursorShape, Diagnostic, DiagnosticSeverity, Documentation, File, IndentKind, IndentSize, - Language, LanguageServerName, OffsetRangeExt, OffsetUtf16, Point, Selection, SelectionGoal, - TransactionId, + markdown, point_from_lsp, AutoindentMode, BracketPair, Buffer, CodeAction, CodeLabel, + Completion, CursorShape, Diagnostic, DiagnosticSeverity, Documentation, File, IndentKind, + IndentSize, Language, LanguageServerName, OffsetRangeExt, OffsetUtf16, Point, Selection, + SelectionGoal, TransactionId, }; use link_go_to_definition::{ hide_link_definition, show_link_definition, GoToDefinitionLink, InlayHighlight, @@ -80,7 +80,7 @@ use ordered_float::OrderedFloat; use parking_lot::RwLock; use project::{FormatTrigger, Location, Project, ProjectPath, ProjectTransaction}; use rand::{seq::SliceRandom, thread_rng}; -use rpc::proto::PeerId; +use rpc::proto::{self, PeerId}; use scroll::{ autoscroll::Autoscroll, OngoingScroll, ScrollAnchor, ScrollManager, ScrollbarAutoHide, }; @@ -999,7 +999,7 @@ impl CompletionsMenu { let completions = self.completions.clone(); let completions_guard = completions.read(); let completion = &completions_guard[index]; - if completion.lsp_completion.documentation.is_some() { + if completion.documentation.is_some() { return; } @@ -1007,6 +1007,57 @@ impl CompletionsMenu { let completion = completion.lsp_completion.clone(); drop(completions_guard); + if project.read(cx).is_remote() { + let Some(project_id) = project.read(cx).remote_id() else { + log::error!("Remote project without remote_id"); + return; + }; + + let client = project.read(cx).client(); + let request = proto::ResolveCompletionDocumentation { + project_id, + language_server_id: server_id.0 as u64, + lsp_completion: serde_json::to_string(&completion).unwrap().into_bytes(), + }; + + cx.spawn(|this, mut cx| async move { + let Some(response) = client + .request(request) + .await + .context("completion documentation resolve proto request") + .log_err() + else { + return; + }; + + if response.text.is_empty() { + let mut completions = completions.write(); + let completion = &mut completions[index]; + completion.documentation = Some(Documentation::Undocumented); + } + + let documentation = if response.is_markdown { + Documentation::MultiLineMarkdown( + markdown::parse_markdown(&response.text, &language_registry, None).await, + ) + } else if response.text.lines().count() <= 1 { + Documentation::SingleLine(response.text) + } else { + Documentation::MultiLinePlainText(response.text) + }; + + let mut completions = completions.write(); + let completion = &mut completions[index]; + completion.documentation = Some(documentation); + drop(completions); + + _ = this.update(&mut cx, |_, cx| cx.notify()); + }) + .detach(); + + return; + } + let Some(server) = project.read(cx).language_server_for_id(server_id) else { return; }; @@ -1037,11 +1088,14 @@ impl CompletionsMenu { let mut completions = completions.write(); let completion = &mut completions[index]; - completion.documentation = documentation; - completion.lsp_completion.documentation = Some(lsp_documentation); + completion.documentation = Some(documentation); drop(completions); _ = this.update(&mut cx, |_, cx| cx.notify()); + } else { + let mut completions = completions.write(); + let completion = &mut completions[index]; + completion.documentation = Some(Documentation::Undocumented); } }) .detach(); @@ -1061,10 +1115,10 @@ impl CompletionsMenu { .max_by_key(|(_, mat)| { let completions = self.completions.read(); let completion = &completions[mat.candidate_id]; - let documentation = &completion.lsp_completion.documentation; + let documentation = &completion.documentation; let mut len = completion.label.text.chars().count(); - if let Some(lsp::Documentation::String(text)) = documentation { + if let Some(Documentation::SingleLine(text)) = documentation { len += text.chars().count(); } diff --git a/crates/language/src/buffer.rs b/crates/language/src/buffer.rs index d318a87b40..d8ebc1d445 100644 --- a/crates/language/src/buffer.rs +++ b/crates/language/src/buffer.rs @@ -149,28 +149,28 @@ pub async fn prepare_completion_documentation( documentation: &lsp::Documentation, language_registry: &Arc, language: Option>, -) -> Option { +) -> Documentation { match documentation { lsp::Documentation::String(text) => { if text.lines().count() <= 1 { - Some(Documentation::SingleLine(text.clone())) + Documentation::SingleLine(text.clone()) } else { - Some(Documentation::MultiLinePlainText(text.clone())) + Documentation::MultiLinePlainText(text.clone()) } } lsp::Documentation::MarkupContent(lsp::MarkupContent { kind, value }) => match kind { lsp::MarkupKind::PlainText => { if value.lines().count() <= 1 { - Some(Documentation::SingleLine(value.clone())) + Documentation::SingleLine(value.clone()) } else { - Some(Documentation::MultiLinePlainText(value.clone())) + Documentation::MultiLinePlainText(value.clone()) } } lsp::MarkupKind::Markdown => { let parsed = parse_markdown(value, language_registry, language).await; - Some(Documentation::MultiLineMarkdown(parsed)) + Documentation::MultiLineMarkdown(parsed) } }, } @@ -178,6 +178,7 @@ pub async fn prepare_completion_documentation( #[derive(Clone, Debug)] pub enum Documentation { + Undocumented, SingleLine(String), MultiLinePlainText(String), MultiLineMarkdown(ParsedMarkdown), diff --git a/crates/project/src/lsp_command.rs b/crates/project/src/lsp_command.rs index c71b378da6..72d79ca979 100644 --- a/crates/project/src/lsp_command.rs +++ b/crates/project/src/lsp_command.rs @@ -1466,12 +1466,14 @@ impl LspCommand for GetCompletions { } let documentation = if let Some(lsp_docs) = &lsp_completion.documentation { - prepare_completion_documentation( - lsp_docs, - &language_registry, - language.clone(), + Some( + prepare_completion_documentation( + lsp_docs, + &language_registry, + language.clone(), + ) + .await, ) - .await } else { None }; diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index a50e02a631..f25309b9c6 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -580,6 +580,7 @@ impl Project { client.add_model_request_handler(Self::handle_apply_code_action); client.add_model_request_handler(Self::handle_on_type_formatting); client.add_model_request_handler(Self::handle_inlay_hints); + client.add_model_request_handler(Self::handle_resolve_completion_documentation); client.add_model_request_handler(Self::handle_resolve_inlay_hint); client.add_model_request_handler(Self::handle_refresh_inlay_hints); client.add_model_request_handler(Self::handle_reload_buffers); @@ -7155,6 +7156,40 @@ impl Project { }) } + async fn handle_resolve_completion_documentation( + this: ModelHandle, + envelope: TypedEnvelope, + _: Arc, + mut cx: AsyncAppContext, + ) -> Result { + let lsp_completion = serde_json::from_slice(&envelope.payload.lsp_completion)?; + + let completion = this + .read_with(&mut cx, |this, _| { + let id = LanguageServerId(envelope.payload.language_server_id as usize); + let Some(server) = this.language_server_for_id(id) else { + return Err(anyhow!("No language server {id}")); + }; + + Ok(server.request::(lsp_completion)) + })? + .await?; + + let mut is_markdown = false; + let text = match completion.documentation { + Some(lsp::Documentation::String(text)) => text, + + Some(lsp::Documentation::MarkupContent(lsp::MarkupContent { kind, value })) => { + is_markdown = kind == lsp::MarkupKind::Markdown; + value + } + + _ => String::new(), + }; + + Ok(proto::ResolveCompletionDocumentationResponse { text, is_markdown }) + } + async fn handle_apply_code_action( this: ModelHandle, envelope: TypedEnvelope, diff --git a/crates/rpc/proto/zed.proto b/crates/rpc/proto/zed.proto index 3501e70e6a..e97ede3fee 100644 --- a/crates/rpc/proto/zed.proto +++ b/crates/rpc/proto/zed.proto @@ -89,88 +89,90 @@ message Envelope { FormatBuffersResponse format_buffers_response = 70; GetCompletions get_completions = 71; GetCompletionsResponse get_completions_response = 72; - ApplyCompletionAdditionalEdits apply_completion_additional_edits = 73; - ApplyCompletionAdditionalEditsResponse apply_completion_additional_edits_response = 74; - GetCodeActions get_code_actions = 75; - GetCodeActionsResponse get_code_actions_response = 76; - GetHover get_hover = 77; - GetHoverResponse get_hover_response = 78; - ApplyCodeAction apply_code_action = 79; - ApplyCodeActionResponse apply_code_action_response = 80; - PrepareRename prepare_rename = 81; - PrepareRenameResponse prepare_rename_response = 82; - PerformRename perform_rename = 83; - PerformRenameResponse perform_rename_response = 84; - SearchProject search_project = 85; - SearchProjectResponse search_project_response = 86; + ResolveCompletionDocumentation resolve_completion_documentation = 73; + ResolveCompletionDocumentationResponse resolve_completion_documentation_response = 74; + ApplyCompletionAdditionalEdits apply_completion_additional_edits = 75; + ApplyCompletionAdditionalEditsResponse apply_completion_additional_edits_response = 76; + GetCodeActions get_code_actions = 77; + GetCodeActionsResponse get_code_actions_response = 78; + GetHover get_hover = 79; + GetHoverResponse get_hover_response = 80; + ApplyCodeAction apply_code_action = 81; + ApplyCodeActionResponse apply_code_action_response = 82; + PrepareRename prepare_rename = 83; + PrepareRenameResponse prepare_rename_response = 84; + PerformRename perform_rename = 85; + PerformRenameResponse perform_rename_response = 86; + SearchProject search_project = 87; + SearchProjectResponse search_project_response = 88; - UpdateContacts update_contacts = 87; - UpdateInviteInfo update_invite_info = 88; - ShowContacts show_contacts = 89; + UpdateContacts update_contacts = 89; + UpdateInviteInfo update_invite_info = 90; + ShowContacts show_contacts = 91; - GetUsers get_users = 90; - FuzzySearchUsers fuzzy_search_users = 91; - UsersResponse users_response = 92; - RequestContact request_contact = 93; - RespondToContactRequest respond_to_contact_request = 94; - RemoveContact remove_contact = 95; + GetUsers get_users = 92; + FuzzySearchUsers fuzzy_search_users = 93; + UsersResponse users_response = 94; + RequestContact request_contact = 95; + RespondToContactRequest respond_to_contact_request = 96; + RemoveContact remove_contact = 97; - Follow follow = 96; - FollowResponse follow_response = 97; - UpdateFollowers update_followers = 98; - Unfollow unfollow = 99; - GetPrivateUserInfo get_private_user_info = 100; - GetPrivateUserInfoResponse get_private_user_info_response = 101; - UpdateDiffBase update_diff_base = 102; + Follow follow = 98; + FollowResponse follow_response = 99; + UpdateFollowers update_followers = 100; + Unfollow unfollow = 101; + GetPrivateUserInfo get_private_user_info = 102; + GetPrivateUserInfoResponse get_private_user_info_response = 103; + UpdateDiffBase update_diff_base = 104; - OnTypeFormatting on_type_formatting = 103; - OnTypeFormattingResponse on_type_formatting_response = 104; + OnTypeFormatting on_type_formatting = 105; + OnTypeFormattingResponse on_type_formatting_response = 106; - UpdateWorktreeSettings update_worktree_settings = 105; + UpdateWorktreeSettings update_worktree_settings = 107; - InlayHints inlay_hints = 106; - InlayHintsResponse inlay_hints_response = 107; - ResolveInlayHint resolve_inlay_hint = 108; - ResolveInlayHintResponse resolve_inlay_hint_response = 109; - RefreshInlayHints refresh_inlay_hints = 110; + InlayHints inlay_hints = 108; + InlayHintsResponse inlay_hints_response = 109; + ResolveInlayHint resolve_inlay_hint = 110; + ResolveInlayHintResponse resolve_inlay_hint_response = 111; + RefreshInlayHints refresh_inlay_hints = 112; - CreateChannel create_channel = 111; - CreateChannelResponse create_channel_response = 112; - InviteChannelMember invite_channel_member = 113; - RemoveChannelMember remove_channel_member = 114; - RespondToChannelInvite respond_to_channel_invite = 115; - UpdateChannels update_channels = 116; - JoinChannel join_channel = 117; - DeleteChannel delete_channel = 118; - GetChannelMembers get_channel_members = 119; - GetChannelMembersResponse get_channel_members_response = 120; - SetChannelMemberAdmin set_channel_member_admin = 121; - RenameChannel rename_channel = 122; - RenameChannelResponse rename_channel_response = 123; + CreateChannel create_channel = 113; + CreateChannelResponse create_channel_response = 114; + InviteChannelMember invite_channel_member = 115; + RemoveChannelMember remove_channel_member = 116; + RespondToChannelInvite respond_to_channel_invite = 117; + UpdateChannels update_channels = 118; + JoinChannel join_channel = 119; + DeleteChannel delete_channel = 120; + GetChannelMembers get_channel_members = 121; + GetChannelMembersResponse get_channel_members_response = 122; + SetChannelMemberAdmin set_channel_member_admin = 123; + RenameChannel rename_channel = 124; + RenameChannelResponse rename_channel_response = 125; - JoinChannelBuffer join_channel_buffer = 124; - JoinChannelBufferResponse join_channel_buffer_response = 125; - UpdateChannelBuffer update_channel_buffer = 126; - LeaveChannelBuffer leave_channel_buffer = 127; - UpdateChannelBufferCollaborators update_channel_buffer_collaborators = 128; - RejoinChannelBuffers rejoin_channel_buffers = 129; - RejoinChannelBuffersResponse rejoin_channel_buffers_response = 130; - AckBufferOperation ack_buffer_operation = 143; + JoinChannelBuffer join_channel_buffer = 126; + JoinChannelBufferResponse join_channel_buffer_response = 127; + UpdateChannelBuffer update_channel_buffer = 128; + LeaveChannelBuffer leave_channel_buffer = 129; + UpdateChannelBufferCollaborators update_channel_buffer_collaborators = 130; + RejoinChannelBuffers rejoin_channel_buffers = 131; + RejoinChannelBuffersResponse rejoin_channel_buffers_response = 132; + AckBufferOperation ack_buffer_operation = 145; - JoinChannelChat join_channel_chat = 131; - JoinChannelChatResponse join_channel_chat_response = 132; - LeaveChannelChat leave_channel_chat = 133; - SendChannelMessage send_channel_message = 134; - SendChannelMessageResponse send_channel_message_response = 135; - ChannelMessageSent channel_message_sent = 136; - GetChannelMessages get_channel_messages = 137; - GetChannelMessagesResponse get_channel_messages_response = 138; - RemoveChannelMessage remove_channel_message = 139; - AckChannelMessage ack_channel_message = 144; + JoinChannelChat join_channel_chat = 133; + JoinChannelChatResponse join_channel_chat_response = 134; + LeaveChannelChat leave_channel_chat = 135; + SendChannelMessage send_channel_message = 136; + SendChannelMessageResponse send_channel_message_response = 137; + ChannelMessageSent channel_message_sent = 138; + GetChannelMessages get_channel_messages = 139; + GetChannelMessagesResponse get_channel_messages_response = 140; + RemoveChannelMessage remove_channel_message = 141; + AckChannelMessage ack_channel_message = 146; - LinkChannel link_channel = 140; - UnlinkChannel unlink_channel = 141; - MoveChannel move_channel = 142; // current max: 144 + LinkChannel link_channel = 142; + UnlinkChannel unlink_channel = 143; + MoveChannel move_channel = 144; // current max: 146 } } @@ -832,6 +834,17 @@ message ResolveState { } } +message ResolveCompletionDocumentation { + uint64 project_id = 1; + uint64 language_server_id = 2; + bytes lsp_completion = 3; +} + +message ResolveCompletionDocumentationResponse { + string text = 1; + bool is_markdown = 2; +} + message ResolveInlayHint { uint64 project_id = 1; uint64 buffer_id = 2; diff --git a/crates/rpc/src/proto.rs b/crates/rpc/src/proto.rs index f0d7937f6f..abadada328 100644 --- a/crates/rpc/src/proto.rs +++ b/crates/rpc/src/proto.rs @@ -205,6 +205,8 @@ messages!( (OnTypeFormattingResponse, Background), (InlayHints, Background), (InlayHintsResponse, Background), + (ResolveCompletionDocumentation, Background), + (ResolveCompletionDocumentationResponse, Background), (ResolveInlayHint, Background), (ResolveInlayHintResponse, Background), (RefreshInlayHints, Foreground), @@ -318,6 +320,10 @@ request_messages!( (PrepareRename, PrepareRenameResponse), (OnTypeFormatting, OnTypeFormattingResponse), (InlayHints, InlayHintsResponse), + ( + ResolveCompletionDocumentation, + ResolveCompletionDocumentationResponse + ), (ResolveInlayHint, ResolveInlayHintResponse), (RefreshInlayHints, Ack), (ReloadBuffers, ReloadBuffersResponse), @@ -381,6 +387,7 @@ entity_messages!( PerformRename, OnTypeFormatting, InlayHints, + ResolveCompletionDocumentation, ResolveInlayHint, RefreshInlayHints, PrepareRename, diff --git a/crates/rpc/src/rpc.rs b/crates/rpc/src/rpc.rs index 942672b94b..5ba531a50e 100644 --- a/crates/rpc/src/rpc.rs +++ b/crates/rpc/src/rpc.rs @@ -6,4 +6,4 @@ pub use conn::Connection; pub use peer::*; mod macros; -pub const PROTOCOL_VERSION: u32 = 64; +pub const PROTOCOL_VERSION: u32 = 65; From f5af5f7334a7b523090c0741029dde913cd00c2c Mon Sep 17 00:00:00 2001 From: Julia Date: Tue, 10 Oct 2023 09:27:18 -0400 Subject: [PATCH 18/30] Avoid leaving selected item index past end of matches list Co-Authored-By: Antonio Scandurra --- crates/editor/src/editor.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 3143b19629..9b276f002c 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -1332,6 +1332,7 @@ impl CompletionsMenu { } self.matches = matches.into(); + self.selected_item = 0; } } From 801af95a13e0332af3f9a4d1a1d62c47bea07529 Mon Sep 17 00:00:00 2001 From: Julia Date: Tue, 10 Oct 2023 10:08:29 -0400 Subject: [PATCH 19/30] Make completion documentation scroll & fix accompanying panic from tag Co-Authored-By: Antonio Scandurra --- crates/editor/src/editor.rs | 26 +++++++---- crates/editor/src/hover_popover.rs | 4 +- crates/gpui/src/elements/flex.rs | 75 ++++++++++++++++++------------ 3 files changed, 64 insertions(+), 41 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 9b276f002c..06482dbbc6 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -119,7 +119,7 @@ pub const DOCUMENT_HIGHLIGHTS_DEBOUNCE_TIMEOUT: Duration = Duration::from_millis pub const FORMAT_TIMEOUT: Duration = Duration::from_secs(2); -pub fn render_parsed_markdown( +pub fn render_parsed_markdown( parsed: &language::ParsedMarkdown, editor_style: &EditorStyle, cx: &mut ViewContext, @@ -153,7 +153,7 @@ pub fn render_parsed_markdown( style: CursorStyle::PointingHand, }); cx.scene().push_mouse_region( - MouseRegion::new::(view_id, region_id, bounds) + MouseRegion::new::<(RenderedMarkdown, Tag)>(view_id, region_id, bounds) .on_click::(MouseButton::Left, move |_, _, cx| { cx.platform().open_url(&url) }), @@ -1247,6 +1247,8 @@ impl CompletionsMenu { }) .with_width_from_item(widest_completion_ix); + enum MultiLineDocumentation {} + Flex::row() .with_child(list) .with_children({ @@ -1256,13 +1258,21 @@ impl CompletionsMenu { let documentation = &completion.documentation; match documentation { - Some(Documentation::MultiLinePlainText(text)) => { - Some(Text::new(text.clone(), style.text.clone())) - } + Some(Documentation::MultiLinePlainText(text)) => Some( + Flex::column() + .scrollable::(0, None, cx) + .with_child( + Text::new(text.clone(), style.text.clone()).with_soft_wrap(true), + ), + ), - Some(Documentation::MultiLineMarkdown(parsed)) => { - Some(render_parsed_markdown(parsed, &style, cx)) - } + Some(Documentation::MultiLineMarkdown(parsed)) => Some( + Flex::column() + .scrollable::(0, None, cx) + .with_child(render_parsed_markdown::( + parsed, &style, cx, + )), + ), _ => None, } diff --git a/crates/editor/src/hover_popover.rs b/crates/editor/src/hover_popover.rs index d5ccb481b2..e8901ad6c1 100644 --- a/crates/editor/src/hover_popover.rs +++ b/crates/editor/src/hover_popover.rs @@ -474,8 +474,8 @@ impl InfoPopover { ) -> AnyElement { MouseEventHandler::new::(0, cx, |_, cx| { Flex::column() - .scrollable::(1, None, cx) - .with_child(crate::render_parsed_markdown( + .scrollable::(0, None, cx) + .with_child(crate::render_parsed_markdown::( &self.parsed_content, style, cx, diff --git a/crates/gpui/src/elements/flex.rs b/crates/gpui/src/elements/flex.rs index cdce0423fd..ba387c5e48 100644 --- a/crates/gpui/src/elements/flex.rs +++ b/crates/gpui/src/elements/flex.rs @@ -2,7 +2,8 @@ use std::{any::Any, cell::Cell, f32::INFINITY, ops::Range, rc::Rc}; use crate::{ json::{self, ToJson, Value}, - AnyElement, Axis, Element, ElementStateHandle, SizeConstraint, Vector2FExt, ViewContext, + AnyElement, Axis, Element, ElementStateHandle, SizeConstraint, TypeTag, Vector2FExt, + ViewContext, }; use pathfinder_geometry::{ rect::RectF, @@ -10,10 +11,10 @@ use pathfinder_geometry::{ }; use serde_json::json; -#[derive(Default)] struct ScrollState { scroll_to: Cell>, scroll_position: Cell, + type_tag: TypeTag, } pub struct Flex { @@ -66,8 +67,14 @@ impl Flex { where Tag: 'static, { - let scroll_state = cx.default_element_state::>(element_id); - scroll_state.read(cx).scroll_to.set(scroll_to); + let scroll_state = cx.element_state::>( + element_id, + Rc::new(ScrollState { + scroll_to: Cell::new(scroll_to), + scroll_position: Default::default(), + type_tag: TypeTag::new::(), + }), + ); self.scroll_state = Some((scroll_state, cx.handle().id())); self } @@ -276,38 +283,44 @@ impl Element for Flex { if let Some((scroll_state, id)) = &self.scroll_state { let scroll_state = scroll_state.read(cx).clone(); cx.scene().push_mouse_region( - crate::MouseRegion::new::(*id, 0, bounds) - .on_scroll({ - let axis = self.axis; - move |e, _: &mut V, cx| { - if remaining_space < 0. { - let scroll_delta = e.delta.raw(); + crate::MouseRegion::from_handlers( + scroll_state.type_tag, + *id, + 0, + bounds, + Default::default(), + ) + .on_scroll({ + let axis = self.axis; + move |e, _: &mut V, cx| { + if remaining_space < 0. { + let scroll_delta = e.delta.raw(); - let mut delta = match axis { - Axis::Horizontal => { - if scroll_delta.x().abs() >= scroll_delta.y().abs() { - scroll_delta.x() - } else { - scroll_delta.y() - } + let mut delta = match axis { + Axis::Horizontal => { + if scroll_delta.x().abs() >= scroll_delta.y().abs() { + scroll_delta.x() + } else { + scroll_delta.y() } - Axis::Vertical => scroll_delta.y(), - }; - if !e.delta.precise() { - delta *= 20.; } - - scroll_state - .scroll_position - .set(scroll_state.scroll_position.get() - delta); - - cx.notify(); - } else { - cx.propagate_event(); + Axis::Vertical => scroll_delta.y(), + }; + if !e.delta.precise() { + delta *= 20.; } + + scroll_state + .scroll_position + .set(scroll_state.scroll_position.get() - delta); + + cx.notify(); + } else { + cx.propagate_event(); } - }) - .on_move(|_, _: &mut V, _| { /* Capture move events */ }), + } + }) + .on_move(|_, _: &mut V, _| { /* Capture move events */ }), ) } From 0cec0c1c1d82c5b2c5bc0fe5b7afaf9f9073337f Mon Sep 17 00:00:00 2001 From: Julia Date: Wed, 11 Oct 2023 13:41:58 -0400 Subject: [PATCH 20/30] Fixup layout --- crates/editor/src/editor.rs | 19 ++++++++++++++++--- crates/theme/src/theme.rs | 4 +++- styles/src/style_tree/editor.ts | 4 +++- 3 files changed, 22 insertions(+), 5 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 06482dbbc6..3fc47f48e9 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -1240,6 +1240,9 @@ impl CompletionsMenu { ) .map(|task| task.detach()); }) + .constrained() + .with_min_width(style.autocomplete.completion_min_width) + .with_max_width(style.autocomplete.completion_max_width) .into_any(), ); } @@ -1250,7 +1253,7 @@ impl CompletionsMenu { enum MultiLineDocumentation {} Flex::row() - .with_child(list) + .with_child(list.flex(1., false)) .with_children({ let mat = &self.matches[selected_item]; let completions = self.completions.read(); @@ -1263,7 +1266,12 @@ impl CompletionsMenu { .scrollable::(0, None, cx) .with_child( Text::new(text.clone(), style.text.clone()).with_soft_wrap(true), - ), + ) + .contained() + .with_style(style.autocomplete.alongside_docs_container) + .constrained() + .with_max_width(style.autocomplete.alongside_docs_max_width) + .flex(1., false), ), Some(Documentation::MultiLineMarkdown(parsed)) => Some( @@ -1271,7 +1279,12 @@ impl CompletionsMenu { .scrollable::(0, None, cx) .with_child(render_parsed_markdown::( parsed, &style, cx, - )), + )) + .contained() + .with_style(style.autocomplete.alongside_docs_container) + .constrained() + .with_max_width(style.autocomplete.alongside_docs_max_width) + .flex(1., false), ), _ => None, diff --git a/crates/theme/src/theme.rs b/crates/theme/src/theme.rs index 9f7530ec18..f335444b58 100644 --- a/crates/theme/src/theme.rs +++ b/crates/theme/src/theme.rs @@ -867,10 +867,12 @@ pub struct AutocompleteStyle { pub selected_item: ContainerStyle, pub hovered_item: ContainerStyle, pub match_highlight: HighlightStyle, + pub completion_min_width: f32, + pub completion_max_width: f32, pub inline_docs_container: ContainerStyle, pub inline_docs_color: Color, pub inline_docs_size_percent: f32, - pub alongside_docs_width: f32, + pub alongside_docs_max_width: f32, pub alongside_docs_container: ContainerStyle, } diff --git a/styles/src/style_tree/editor.ts b/styles/src/style_tree/editor.ts index e7717583a8..27a6eaf195 100644 --- a/styles/src/style_tree/editor.ts +++ b/styles/src/style_tree/editor.ts @@ -206,10 +206,12 @@ export default function editor(): any { match_highlight: foreground(theme.middle, "accent", "active"), background: background(theme.middle, "active"), }, + completion_min_width: 300, + completion_max_width: 700, inline_docs_container: { padding: { left: 40 } }, inline_docs_color: text(theme.middle, "sans", "disabled", {}).color, inline_docs_size_percent: 0.75, - alongside_docs_width: 700, + alongside_docs_max_width: 700, alongside_docs_container: { padding: autocomplete_item.padding } }, diagnostic_header: { From a09ee3a41b2a95f2dc016b588970e9b97537e7b8 Mon Sep 17 00:00:00 2001 From: Julia Date: Wed, 11 Oct 2023 14:39:34 -0400 Subject: [PATCH 21/30] Fire markdown link on mouse down Previously any amount of mouse movement would disqualify the mouse down and up from being a click, being a drag instead, which is a long standing UX issue. We can get away with just firing on mouse down here for now --- crates/editor/src/editor.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 3fc47f48e9..9c1e0b3c18 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -154,7 +154,7 @@ pub fn render_parsed_markdown( }); cx.scene().push_mouse_region( MouseRegion::new::<(RenderedMarkdown, Tag)>(view_id, region_id, bounds) - .on_click::(MouseButton::Left, move |_, _, cx| { + .on_down::(MouseButton::Left, move |_, _, cx| { cx.platform().open_url(&url) }), ); From 4688a94a54501d4604b8aad6de77aecc8d556c7d Mon Sep 17 00:00:00 2001 From: Julia Date: Thu, 12 Oct 2023 12:11:27 -0400 Subject: [PATCH 22/30] Allow file links in markdown & filter links a bit aggressively --- crates/editor/src/editor.rs | 38 ++++++++++++++++----- crates/editor/src/element.rs | 10 ++++-- crates/editor/src/hover_popover.rs | 8 +++-- crates/language/src/markdown.rs | 41 ++++++++++++++++++----- crates/terminal_view/src/terminal_view.rs | 7 ++++ 5 files changed, 81 insertions(+), 23 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 9c1e0b3c18..0748a0fcf4 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -122,6 +122,7 @@ pub const FORMAT_TIMEOUT: Duration = Duration::from_secs(2); pub fn render_parsed_markdown( parsed: &language::ParsedMarkdown, editor_style: &EditorStyle, + workspace: Option>, cx: &mut ViewContext, ) -> Text { enum RenderedMarkdown {} @@ -147,15 +148,22 @@ pub fn render_parsed_markdown( region_id += 1; let region = parsed.regions[ix].clone(); - if let Some(url) = region.link_url { + if let Some(link) = region.link { cx.scene().push_cursor_region(CursorRegion { bounds, style: CursorStyle::PointingHand, }); cx.scene().push_mouse_region( MouseRegion::new::<(RenderedMarkdown, Tag)>(view_id, region_id, bounds) - .on_down::(MouseButton::Left, move |_, _, cx| { - cx.platform().open_url(&url) + .on_down::(MouseButton::Left, move |_, _, cx| match &link { + markdown::Link::Web { url } => cx.platform().open_url(url), + markdown::Link::Path { path } => { + if let Some(workspace) = &workspace { + _ = workspace.update(cx, |workspace, cx| { + workspace.open_abs_path(path.clone(), false, cx).detach(); + }); + } + } }), ); } @@ -916,10 +924,11 @@ impl ContextMenu { &self, cursor_position: DisplayPoint, style: EditorStyle, + workspace: Option>, cx: &mut ViewContext, ) -> (DisplayPoint, AnyElement) { match self { - ContextMenu::Completions(menu) => (cursor_position, menu.render(style, cx)), + ContextMenu::Completions(menu) => (cursor_position, menu.render(style, workspace, cx)), ContextMenu::CodeActions(menu) => menu.render(cursor_position, style, cx), } } @@ -1105,7 +1114,12 @@ impl CompletionsMenu { !self.matches.is_empty() } - fn render(&self, style: EditorStyle, cx: &mut ViewContext) -> AnyElement { + fn render( + &self, + style: EditorStyle, + workspace: Option>, + cx: &mut ViewContext, + ) -> AnyElement { enum CompletionTag {} let widest_completion_ix = self @@ -1278,7 +1292,7 @@ impl CompletionsMenu { Flex::column() .scrollable::(0, None, cx) .with_child(render_parsed_markdown::( - parsed, &style, cx, + parsed, &style, workspace, cx, )) .contained() .with_style(style.autocomplete.alongside_docs_container) @@ -3140,6 +3154,7 @@ impl Editor { false }); } + fn completion_query(buffer: &MultiBufferSnapshot, position: impl ToOffset) -> Option { let offset = position.to_offset(buffer); let (word_range, kind) = buffer.surrounding_word(offset); @@ -4215,9 +4230,14 @@ impl Editor { style: EditorStyle, cx: &mut ViewContext, ) -> Option<(DisplayPoint, AnyElement)> { - self.context_menu - .as_ref() - .map(|menu| menu.render(cursor_position, style, cx)) + self.context_menu.as_ref().map(|menu| { + menu.render( + cursor_position, + style, + self.workspace.as_ref().map(|(w, _)| w.clone()), + cx, + ) + }) } fn show_context_menu(&mut self, menu: ContextMenu, cx: &mut ViewContext) { diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index 924d66c21c..316e143413 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -2439,9 +2439,13 @@ impl Element for EditorElement { } let visible_rows = start_row..start_row + line_layouts.len() as u32; - let mut hover = editor - .hover_state - .render(&snapshot, &style, visible_rows, cx); + let mut hover = editor.hover_state.render( + &snapshot, + &style, + visible_rows, + editor.workspace.as_ref().map(|(w, _)| w.clone()), + cx, + ); let mode = editor.mode; let mut fold_indicators = editor.render_fold_indicators( diff --git a/crates/editor/src/hover_popover.rs b/crates/editor/src/hover_popover.rs index e8901ad6c1..00a307df68 100644 --- a/crates/editor/src/hover_popover.rs +++ b/crates/editor/src/hover_popover.rs @@ -9,7 +9,7 @@ use gpui::{ actions, elements::{Flex, MouseEventHandler, Padding, ParentElement, Text}, platform::{CursorStyle, MouseButton}, - AnyElement, AppContext, Element, ModelHandle, Task, ViewContext, + AnyElement, AppContext, Element, ModelHandle, Task, ViewContext, WeakViewHandle, }; use language::{ markdown, Bias, DiagnosticEntry, DiagnosticSeverity, Language, LanguageRegistry, ParsedMarkdown, @@ -17,6 +17,7 @@ use language::{ use project::{HoverBlock, HoverBlockKind, InlayHintLabelPart, Project}; use std::{ops::Range, sync::Arc, time::Duration}; use util::TryFutureExt; +use workspace::Workspace; pub const HOVER_DELAY_MILLIS: u64 = 350; pub const HOVER_REQUEST_DELAY_MILLIS: u64 = 200; @@ -422,6 +423,7 @@ impl HoverState { snapshot: &EditorSnapshot, style: &EditorStyle, visible_rows: Range, + workspace: Option>, cx: &mut ViewContext, ) -> Option<(DisplayPoint, Vec>)> { // If there is a diagnostic, position the popovers based on that. @@ -451,7 +453,7 @@ impl HoverState { elements.push(diagnostic_popover.render(style, cx)); } if let Some(info_popover) = self.info_popover.as_mut() { - elements.push(info_popover.render(style, cx)); + elements.push(info_popover.render(style, workspace, cx)); } Some((point, elements)) @@ -470,6 +472,7 @@ impl InfoPopover { pub fn render( &mut self, style: &EditorStyle, + workspace: Option>, cx: &mut ViewContext, ) -> AnyElement { MouseEventHandler::new::(0, cx, |_, cx| { @@ -478,6 +481,7 @@ impl InfoPopover { .with_child(crate::render_parsed_markdown::( &self.parsed_content, style, + workspace, cx, )) .contained() diff --git a/crates/language/src/markdown.rs b/crates/language/src/markdown.rs index 8be15e81f6..7f57eba309 100644 --- a/crates/language/src/markdown.rs +++ b/crates/language/src/markdown.rs @@ -1,5 +1,5 @@ -use std::ops::Range; use std::sync::Arc; +use std::{ops::Range, path::PathBuf}; use crate::{HighlightId, Language, LanguageRegistry}; use gpui::fonts::{self, HighlightStyle, Weight}; @@ -58,7 +58,28 @@ pub struct MarkdownHighlightStyle { #[derive(Debug, Clone)] pub struct ParsedRegion { pub code: bool, - pub link_url: Option, + pub link: Option, +} + +#[derive(Debug, Clone)] +pub enum Link { + Web { url: String }, + Path { path: PathBuf }, +} + +impl Link { + fn identify(text: String) -> Option { + if text.starts_with("http") { + return Some(Link::Web { url: text }); + } + + let path = PathBuf::from(text); + if path.is_absolute() { + return Some(Link::Path { path }); + } + + None + } } pub async fn parse_markdown( @@ -115,17 +136,20 @@ pub async fn parse_markdown_block( text.push_str(t.as_ref()); let mut style = MarkdownHighlightStyle::default(); + if bold_depth > 0 { style.weight = Weight::BOLD; } + if italic_depth > 0 { style.italic = true; } - if let Some(link_url) = link_url.clone() { + + if let Some(link) = link_url.clone().and_then(|u| Link::identify(u)) { region_ranges.push(prev_len..text.len()); regions.push(ParsedRegion { - link_url: Some(link_url), code: false, + link: Some(link), }); style.underline = true; } @@ -151,7 +175,9 @@ pub async fn parse_markdown_block( Event::Code(t) => { text.push_str(t.as_ref()); region_ranges.push(prev_len..text.len()); - if link_url.is_some() { + + let link = link_url.clone().and_then(|u| Link::identify(u)); + if link.is_some() { highlights.push(( prev_len..text.len(), MarkdownHighlight::Style(MarkdownHighlightStyle { @@ -160,10 +186,7 @@ pub async fn parse_markdown_block( }), )); } - regions.push(ParsedRegion { - code: true, - link_url: link_url.clone(), - }); + regions.push(ParsedRegion { code: true, link }); } Event::Start(tag) => match tag { diff --git a/crates/terminal_view/src/terminal_view.rs b/crates/terminal_view/src/terminal_view.rs index cd939b5604..5a13efd07a 100644 --- a/crates/terminal_view/src/terminal_view.rs +++ b/crates/terminal_view/src/terminal_view.rs @@ -150,11 +150,14 @@ impl TerminalView { cx.notify(); cx.emit(Event::Wakeup); } + Event::Bell => { this.has_bell = true; cx.emit(Event::Wakeup); } + Event::BlinkChanged => this.blinking_on = !this.blinking_on, + Event::TitleChanged => { if let Some(foreground_info) = &this.terminal().read(cx).foreground_process_info { let cwd = foreground_info.cwd.clone(); @@ -171,6 +174,7 @@ impl TerminalView { .detach(); } } + Event::NewNavigationTarget(maybe_navigation_target) => { this.can_navigate_to_selected_word = match maybe_navigation_target { Some(MaybeNavigationTarget::Url(_)) => true, @@ -180,8 +184,10 @@ impl TerminalView { None => false, } } + Event::Open(maybe_navigation_target) => match maybe_navigation_target { MaybeNavigationTarget::Url(url) => cx.platform().open_url(url), + MaybeNavigationTarget::PathLike(maybe_path) => { if !this.can_navigate_to_selected_word { return; @@ -246,6 +252,7 @@ impl TerminalView { } } }, + _ => cx.emit(event.clone()), }) .detach(); From 85332eacbd861847582526cec0642f2d76e88944 Mon Sep 17 00:00:00 2001 From: Julia Date: Thu, 12 Oct 2023 13:23:26 -0400 Subject: [PATCH 23/30] Race completion filter w/completion request & make not block UI --- crates/editor/src/editor.rs | 107 +++++++++++++++++++----------- crates/editor/src/editor_tests.rs | 14 ++-- crates/editor/src/element.rs | 2 +- 3 files changed, 76 insertions(+), 47 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 0748a0fcf4..d7ef82da36 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -656,7 +656,7 @@ pub struct Editor { background_highlights: BTreeMap, inlay_background_highlights: TreeMap, InlayBackgroundHighlight>, nav_history: Option, - context_menu: Option, + context_menu: RwLock>, mouse_context_menu: ViewHandle, completion_tasks: Vec<(CompletionId, Task>)>, next_completion_id: CompletionId, @@ -934,12 +934,13 @@ impl ContextMenu { } } +#[derive(Clone)] struct CompletionsMenu { id: CompletionId, initial_position: Anchor, buffer: ModelHandle, completions: Arc>>, - match_candidates: Vec, + match_candidates: Arc<[StringMatchCandidate]>, matches: Arc<[StringMatch]>, selected_item: usize, list: UniformListState, @@ -1333,13 +1334,13 @@ impl CompletionsMenu { .collect() }; - //Remove all candidates where the query's start does not match the start of any word in the candidate + // Remove all candidates where the query's start does not match the start of any word in the candidate if let Some(query) = query { if let Some(query_start) = query.chars().next() { matches.retain(|string_match| { split_words(&string_match.string).any(|word| { - //Check that the first codepoint of the word as lowercase matches the first - //codepoint of the query as lowercase + // Check that the first codepoint of the word as lowercase matches the first + // codepoint of the query as lowercase word.chars() .flat_map(|codepoint| codepoint.to_lowercase()) .zip(query_start.to_lowercase()) @@ -1805,7 +1806,7 @@ impl Editor { background_highlights: Default::default(), inlay_background_highlights: Default::default(), nav_history: None, - context_menu: None, + context_menu: RwLock::new(None), mouse_context_menu: cx .add_view(|cx| context_menu::ContextMenu::new(editor_view_id, cx)), completion_tasks: Default::default(), @@ -2100,10 +2101,12 @@ impl Editor { if local { let new_cursor_position = self.selections.newest_anchor().head(); - let completion_menu = match self.context_menu.as_mut() { + let mut context_menu = self.context_menu.write(); + let completion_menu = match context_menu.as_ref() { Some(ContextMenu::Completions(menu)) => Some(menu), + _ => { - self.context_menu.take(); + *context_menu = None; None } }; @@ -2115,13 +2118,39 @@ impl Editor { if kind == Some(CharKind::Word) && word_range.to_inclusive().contains(&cursor_position) { + let mut completion_menu = completion_menu.clone(); + drop(context_menu); + let query = Self::completion_query(buffer, cursor_position); - cx.background() - .block(completion_menu.filter(query.as_deref(), cx.background().clone())); + cx.spawn(move |this, mut cx| async move { + completion_menu + .filter(query.as_deref(), cx.background().clone()) + .await; + + this.update(&mut cx, |this, cx| { + let mut context_menu = this.context_menu.write(); + let Some(ContextMenu::Completions(menu)) = context_menu.as_ref() else { + return; + }; + + if menu.id > completion_menu.id { + return; + } + + *context_menu = Some(ContextMenu::Completions(completion_menu)); + drop(context_menu); + cx.notify(); + }) + }) + .detach(); + self.show_completions(&ShowCompletions, cx); } else { + drop(context_menu); self.hide_context_menu(cx); } + } else { + drop(context_menu); } hide_hover(self, cx); @@ -3432,23 +3461,31 @@ impl Editor { this.update(&mut cx, |this, cx| { this.completion_tasks.retain(|(task_id, _)| *task_id > id); - match this.context_menu.as_ref() { + let mut context_menu = this.context_menu.write(); + match context_menu.as_ref() { None => {} + Some(ContextMenu::Completions(prev_menu)) => { if prev_menu.id > id { return; } } + _ => return, } if this.focused && menu.is_some() { let menu = menu.unwrap(); - this.show_context_menu(ContextMenu::Completions(menu), cx); + *context_menu = Some(ContextMenu::Completions(menu)); + drop(context_menu); + this.completion_tasks.clear(); + this.discard_copilot_suggestion(cx); + cx.notify(); } else if this.completion_tasks.is_empty() { // If there are no more completion tasks and the last menu was // empty, we should hide it. If it was already hidden, we should // also show the copilot suggestion when available. + drop(context_menu); if this.hide_context_menu(cx).is_none() { this.update_visible_copilot_suggestion(cx); } @@ -3593,14 +3630,13 @@ impl Editor { } pub fn toggle_code_actions(&mut self, action: &ToggleCodeActions, cx: &mut ViewContext) { - if matches!( - self.context_menu.as_ref(), - Some(ContextMenu::CodeActions(_)) - ) { - self.context_menu.take(); + let mut context_menu = self.context_menu.write(); + if matches!(context_menu.as_ref(), Some(ContextMenu::CodeActions(_))) { + *context_menu = None; cx.notify(); return; } + drop(context_menu); let deployed_from_indicator = action.deployed_from_indicator; let mut task = self.code_actions_task.take(); @@ -3613,16 +3649,16 @@ impl Editor { this.update(&mut cx, |this, cx| { if this.focused { if let Some((buffer, actions)) = this.available_code_actions.clone() { - this.show_context_menu( - ContextMenu::CodeActions(CodeActionsMenu { + this.completion_tasks.clear(); + this.discard_copilot_suggestion(cx); + *this.context_menu.write() = + Some(ContextMenu::CodeActions(CodeActionsMenu { buffer, actions, selected_item: Default::default(), list: Default::default(), deployed_from_indicator, - }), - cx, - ); + })); } } })?; @@ -4086,7 +4122,7 @@ impl Editor { let selection = self.selections.newest_anchor(); let cursor = selection.head(); - if self.context_menu.is_some() + if self.context_menu.read().is_some() || !self.completion_tasks.is_empty() || selection.start != selection.end { @@ -4220,6 +4256,7 @@ impl Editor { pub fn context_menu_visible(&self) -> bool { self.context_menu + .read() .as_ref() .map_or(false, |menu| menu.visible()) } @@ -4230,7 +4267,7 @@ impl Editor { style: EditorStyle, cx: &mut ViewContext, ) -> Option<(DisplayPoint, AnyElement)> { - self.context_menu.as_ref().map(|menu| { + self.context_menu.read().as_ref().map(|menu| { menu.render( cursor_position, style, @@ -4240,19 +4277,10 @@ impl Editor { }) } - fn show_context_menu(&mut self, menu: ContextMenu, cx: &mut ViewContext) { - if !matches!(menu, ContextMenu::Completions(_)) { - self.completion_tasks.clear(); - } - self.context_menu = Some(menu); - self.discard_copilot_suggestion(cx); - cx.notify(); - } - fn hide_context_menu(&mut self, cx: &mut ViewContext) -> Option { cx.notify(); self.completion_tasks.clear(); - let context_menu = self.context_menu.take(); + let context_menu = self.context_menu.write().take(); if context_menu.is_some() { self.update_visible_copilot_suggestion(cx); } @@ -5604,6 +5632,7 @@ impl Editor { if self .context_menu + .write() .as_mut() .map(|menu| menu.select_last(self.project.as_ref(), cx)) .unwrap_or(false) @@ -5648,25 +5677,25 @@ impl Editor { } pub fn context_menu_first(&mut self, _: &ContextMenuFirst, cx: &mut ViewContext) { - if let Some(context_menu) = self.context_menu.as_mut() { + if let Some(context_menu) = self.context_menu.write().as_mut() { context_menu.select_first(self.project.as_ref(), cx); } } pub fn context_menu_prev(&mut self, _: &ContextMenuPrev, cx: &mut ViewContext) { - if let Some(context_menu) = self.context_menu.as_mut() { + if let Some(context_menu) = self.context_menu.write().as_mut() { context_menu.select_prev(self.project.as_ref(), cx); } } pub fn context_menu_next(&mut self, _: &ContextMenuNext, cx: &mut ViewContext) { - if let Some(context_menu) = self.context_menu.as_mut() { + if let Some(context_menu) = self.context_menu.write().as_mut() { context_menu.select_next(self.project.as_ref(), cx); } } pub fn context_menu_last(&mut self, _: &ContextMenuLast, cx: &mut ViewContext) { - if let Some(context_menu) = self.context_menu.as_mut() { + if let Some(context_menu) = self.context_menu.write().as_mut() { context_menu.select_last(self.project.as_ref(), cx); } } @@ -9164,7 +9193,7 @@ impl View for Editor { keymap.add_identifier("renaming"); } if self.context_menu_visible() { - match self.context_menu.as_ref() { + match self.context_menu.read().as_ref() { Some(ContextMenu::Completions(_)) => { keymap.add_identifier("menu"); keymap.add_identifier("showing_completions") diff --git a/crates/editor/src/editor_tests.rs b/crates/editor/src/editor_tests.rs index dee27e0121..4be29ea084 100644 --- a/crates/editor/src/editor_tests.rs +++ b/crates/editor/src/editor_tests.rs @@ -5430,9 +5430,9 @@ async fn test_completion(cx: &mut gpui::TestAppContext) { additional edit "}); cx.simulate_keystroke(" "); - assert!(cx.editor(|e, _| e.context_menu.is_none())); + assert!(cx.editor(|e, _| e.context_menu.read().is_none())); cx.simulate_keystroke("s"); - assert!(cx.editor(|e, _| e.context_menu.is_none())); + assert!(cx.editor(|e, _| e.context_menu.read().is_none())); cx.assert_editor_state(indoc! {" one.second_completion @@ -5494,12 +5494,12 @@ async fn test_completion(cx: &mut gpui::TestAppContext) { }); cx.set_state("editorˇ"); cx.simulate_keystroke("."); - assert!(cx.editor(|e, _| e.context_menu.is_none())); + assert!(cx.editor(|e, _| e.context_menu.read().is_none())); cx.simulate_keystroke("c"); cx.simulate_keystroke("l"); cx.simulate_keystroke("o"); cx.assert_editor_state("editor.cloˇ"); - assert!(cx.editor(|e, _| e.context_menu.is_none())); + assert!(cx.editor(|e, _| e.context_menu.read().is_none())); cx.update_editor(|editor, cx| { editor.show_completions(&ShowCompletions, cx); }); @@ -7788,7 +7788,7 @@ async fn test_completions_in_languages_with_extra_word_characters(cx: &mut gpui: cx.simulate_keystroke("-"); cx.foreground().run_until_parked(); cx.update_editor(|editor, _| { - if let Some(ContextMenu::Completions(menu)) = &editor.context_menu { + if let Some(ContextMenu::Completions(menu)) = editor.context_menu.read().as_ref() { assert_eq!( menu.matches.iter().map(|m| &m.string).collect::>(), &["bg-red", "bg-blue", "bg-yellow"] @@ -7801,7 +7801,7 @@ async fn test_completions_in_languages_with_extra_word_characters(cx: &mut gpui: cx.simulate_keystroke("l"); cx.foreground().run_until_parked(); cx.update_editor(|editor, _| { - if let Some(ContextMenu::Completions(menu)) = &editor.context_menu { + if let Some(ContextMenu::Completions(menu)) = editor.context_menu.read().as_ref() { assert_eq!( menu.matches.iter().map(|m| &m.string).collect::>(), &["bg-blue", "bg-yellow"] @@ -7817,7 +7817,7 @@ async fn test_completions_in_languages_with_extra_word_characters(cx: &mut gpui: cx.simulate_keystroke("l"); cx.foreground().run_until_parked(); cx.update_editor(|editor, _| { - if let Some(ContextMenu::Completions(menu)) = &editor.context_menu { + if let Some(ContextMenu::Completions(menu)) = editor.context_menu.read().as_ref() { assert_eq!( menu.matches.iter().map(|m| &m.string).collect::>(), &["bg-yellow"] diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index 316e143413..00c8508b6c 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -2428,7 +2428,7 @@ impl Element for EditorElement { } let active = matches!( - editor.context_menu, + editor.context_menu.read().as_ref(), Some(crate::ContextMenu::CodeActions(_)) ); From d23bb3b05da84c29ce9626f4d7a68461f2e19c93 Mon Sep 17 00:00:00 2001 From: Julia Date: Thu, 12 Oct 2023 16:18:54 -0400 Subject: [PATCH 24/30] Unbork markdown parse test by making links match --- crates/editor/src/hover_popover.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/editor/src/hover_popover.rs b/crates/editor/src/hover_popover.rs index 00a307df68..5b3985edf9 100644 --- a/crates/editor/src/hover_popover.rs +++ b/crates/editor/src/hover_popover.rs @@ -911,7 +911,7 @@ mod tests { // Links Row { blocks: vec![HoverBlock { - text: "one [two](the-url) three".to_string(), + text: "one [two](https://the-url) three".to_string(), kind: HoverBlockKind::Markdown, }], expected_marked_text: "one «two» three".to_string(), @@ -932,7 +932,7 @@ mod tests { - a - b * two - - [c](the-url) + - [c](https://the-url) - d" .unindent(), kind: HoverBlockKind::Markdown, From c4fc9f7ed81e6f0199c24ed99a60a64dd1eb98cb Mon Sep 17 00:00:00 2001 From: Julia Date: Thu, 12 Oct 2023 19:28:17 -0400 Subject: [PATCH 25/30] Eagerly attempt to resolve missing completion documentation --- crates/editor/src/editor.rs | 264 ++++++++++++++++++++++++++---------- 1 file changed, 191 insertions(+), 73 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index d7ef82da36..bdacf0be38 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -25,7 +25,7 @@ use ::git::diff::DiffHunk; use aho_corasick::AhoCorasick; use anyhow::{anyhow, Context, Result}; use blink_manager::BlinkManager; -use client::{ClickhouseEvent, Collaborator, ParticipantIndex, TelemetrySettings}; +use client::{ClickhouseEvent, Client, Collaborator, ParticipantIndex, TelemetrySettings}; use clock::{Global, ReplicaId}; use collections::{BTreeMap, Bound, HashMap, HashSet, VecDeque}; use convert_case::{Case, Casing}; @@ -62,8 +62,8 @@ use language::{ language_settings::{self, all_language_settings, InlayHintSettings}, markdown, point_from_lsp, AutoindentMode, BracketPair, Buffer, CodeAction, CodeLabel, Completion, CursorShape, Diagnostic, DiagnosticSeverity, Documentation, File, IndentKind, - IndentSize, Language, LanguageServerName, OffsetRangeExt, OffsetUtf16, Point, Selection, - SelectionGoal, TransactionId, + IndentSize, Language, LanguageRegistry, LanguageServerName, OffsetRangeExt, OffsetUtf16, Point, + Selection, SelectionGoal, TransactionId, }; use link_go_to_definition::{ hide_link_definition, show_link_definition, GoToDefinitionLink, InlayHighlight, @@ -954,7 +954,7 @@ impl CompletionsMenu { ) { self.selected_item = 0; self.list.scroll_to(ScrollTarget::Show(self.selected_item)); - self.attempt_resolve_selected_completion(project, cx); + self.attempt_resolve_selected_completion_documentation(project, cx); cx.notify(); } @@ -967,7 +967,7 @@ impl CompletionsMenu { self.selected_item -= 1; self.list.scroll_to(ScrollTarget::Show(self.selected_item)); } - self.attempt_resolve_selected_completion(project, cx); + self.attempt_resolve_selected_completion_documentation(project, cx); cx.notify(); } @@ -980,7 +980,7 @@ impl CompletionsMenu { self.selected_item += 1; self.list.scroll_to(ScrollTarget::Show(self.selected_item)); } - self.attempt_resolve_selected_completion(project, cx); + self.attempt_resolve_selected_completion_documentation(project, cx); cx.notify(); } @@ -991,16 +991,99 @@ impl CompletionsMenu { ) { self.selected_item = self.matches.len() - 1; self.list.scroll_to(ScrollTarget::Show(self.selected_item)); - self.attempt_resolve_selected_completion(project, cx); + self.attempt_resolve_selected_completion_documentation(project, cx); cx.notify(); } - fn attempt_resolve_selected_completion( + fn pre_resolve_completion_documentation( + &self, + project: Option>, + cx: &mut ViewContext, + ) { + let Some(project) = project else { + return; + }; + let client = project.read(cx).client(); + let language_registry = project.read(cx).languages().clone(); + + let is_remote = project.read(cx).is_remote(); + let project_id = project.read(cx).remote_id(); + + let completions = self.completions.clone(); + let completion_indices: Vec<_> = self.matches.iter().map(|m| m.candidate_id).collect(); + + cx.spawn(move |this, mut cx| async move { + if is_remote { + let Some(project_id) = project_id else { + log::error!("Remote project without remote_id"); + return; + }; + + for completion_index in completion_indices { + let completions_guard = completions.read(); + let completion = &completions_guard[completion_index]; + if completion.documentation.is_some() { + continue; + } + + let server_id = completion.server_id; + let completion = completion.lsp_completion.clone(); + drop(completions_guard); + + Self::resolve_completion_documentation_remote( + project_id, + server_id, + completions.clone(), + completion_index, + completion, + client.clone(), + language_registry.clone(), + ) + .await; + + _ = this.update(&mut cx, |_, cx| cx.notify()); + } + } else { + for completion_index in completion_indices { + let completions_guard = completions.read(); + let completion = &completions_guard[completion_index]; + if completion.documentation.is_some() { + continue; + } + + let server_id = completion.server_id; + let completion = completion.lsp_completion.clone(); + drop(completions_guard); + + let server = project.read_with(&mut cx, |project, _| { + project.language_server_for_id(server_id) + }); + let Some(server) = server else { + return; + }; + + Self::resolve_completion_documentation_local( + server, + completions.clone(), + completion_index, + completion, + language_registry.clone(), + ) + .await; + + _ = this.update(&mut cx, |_, cx| cx.notify()); + } + } + }) + .detach(); + } + + fn attempt_resolve_selected_completion_documentation( &mut self, project: Option<&ModelHandle>, cx: &mut ViewContext, ) { - let index = self.matches[self.selected_item].candidate_id; + let completion_index = self.matches[self.selected_item].candidate_id; let Some(project) = project else { return; }; @@ -1008,7 +1091,7 @@ impl CompletionsMenu { let completions = self.completions.clone(); let completions_guard = completions.read(); - let completion = &completions_guard[index]; + let completion = &completions_guard[completion_index]; if completion.documentation.is_some() { return; } @@ -1024,54 +1107,95 @@ impl CompletionsMenu { }; let client = project.read(cx).client(); - let request = proto::ResolveCompletionDocumentation { - project_id, - language_server_id: server_id.0 as u64, - lsp_completion: serde_json::to_string(&completion).unwrap().into_bytes(), - }; - cx.spawn(|this, mut cx| async move { - let Some(response) = client - .request(request) - .await - .context("completion documentation resolve proto request") - .log_err() - else { - return; - }; - - if response.text.is_empty() { - let mut completions = completions.write(); - let completion = &mut completions[index]; - completion.documentation = Some(Documentation::Undocumented); - } - - let documentation = if response.is_markdown { - Documentation::MultiLineMarkdown( - markdown::parse_markdown(&response.text, &language_registry, None).await, - ) - } else if response.text.lines().count() <= 1 { - Documentation::SingleLine(response.text) - } else { - Documentation::MultiLinePlainText(response.text) - }; - - let mut completions = completions.write(); - let completion = &mut completions[index]; - completion.documentation = Some(documentation); - drop(completions); + cx.spawn(move |this, mut cx| async move { + Self::resolve_completion_documentation_remote( + project_id, + server_id, + completions.clone(), + completion_index, + completion, + client, + language_registry.clone(), + ) + .await; _ = this.update(&mut cx, |_, cx| cx.notify()); }) .detach(); + } else { + let Some(server) = project.read(cx).language_server_for_id(server_id) else { + return; + }; - return; + cx.spawn(move |this, mut cx| async move { + Self::resolve_completion_documentation_local( + server, + completions, + completion_index, + completion, + language_registry, + ) + .await; + + _ = this.update(&mut cx, |_, cx| cx.notify()); + }) + .detach(); } + } - let Some(server) = project.read(cx).language_server_for_id(server_id) else { + async fn resolve_completion_documentation_remote( + project_id: u64, + server_id: LanguageServerId, + completions: Arc>>, + completion_index: usize, + completion: lsp::CompletionItem, + client: Arc, + language_registry: Arc, + ) { + let request = proto::ResolveCompletionDocumentation { + project_id, + language_server_id: server_id.0 as u64, + lsp_completion: serde_json::to_string(&completion).unwrap().into_bytes(), + }; + + let Some(response) = client + .request(request) + .await + .context("completion documentation resolve proto request") + .log_err() + else { return; }; + if response.text.is_empty() { + let mut completions = completions.write(); + let completion = &mut completions[completion_index]; + completion.documentation = Some(Documentation::Undocumented); + } + + let documentation = if response.is_markdown { + Documentation::MultiLineMarkdown( + markdown::parse_markdown(&response.text, &language_registry, None).await, + ) + } else if response.text.lines().count() <= 1 { + Documentation::SingleLine(response.text) + } else { + Documentation::MultiLinePlainText(response.text) + }; + + let mut completions = completions.write(); + let completion = &mut completions[completion_index]; + completion.documentation = Some(documentation); + } + + async fn resolve_completion_documentation_local( + server: Arc, + completions: Arc>>, + completion_index: usize, + completion: lsp::CompletionItem, + language_registry: Arc, + ) { let can_resolve = server .capabilities() .completion_provider @@ -1082,33 +1206,27 @@ impl CompletionsMenu { return; } - cx.spawn(|this, mut cx| async move { - let request = server.request::(completion); - let Some(completion_item) = request.await.log_err() else { - return; - }; + let request = server.request::(completion); + let Some(completion_item) = request.await.log_err() else { + return; + }; - if let Some(lsp_documentation) = completion_item.documentation { - let documentation = language::prepare_completion_documentation( - &lsp_documentation, - &language_registry, - None, // TODO: Try to reasonably work out which language the completion is for - ) - .await; + if let Some(lsp_documentation) = completion_item.documentation { + let documentation = language::prepare_completion_documentation( + &lsp_documentation, + &language_registry, + None, // TODO: Try to reasonably work out which language the completion is for + ) + .await; - let mut completions = completions.write(); - let completion = &mut completions[index]; - completion.documentation = Some(documentation); - drop(completions); - - _ = this.update(&mut cx, |_, cx| cx.notify()); - } else { - let mut completions = completions.write(); - let completion = &mut completions[index]; - completion.documentation = Some(Documentation::Undocumented); - } - }) - .detach(); + let mut completions = completions.write(); + let completion = &mut completions[completion_index]; + completion.documentation = Some(documentation); + } else { + let mut completions = completions.write(); + let completion = &mut completions[completion_index]; + completion.documentation = Some(Documentation::Undocumented); + } } fn visible(&self) -> bool { @@ -3450,7 +3568,7 @@ impl Editor { None } else { _ = this.update(&mut cx, |editor, cx| { - menu.attempt_resolve_selected_completion(editor.project.as_ref(), cx); + menu.pre_resolve_completion_documentation(editor.project.clone(), cx); }); Some(menu) } From 1c3ecc4ad242047a700a602ceda0b3630b7118d0 Mon Sep 17 00:00:00 2001 From: Julia Date: Thu, 12 Oct 2023 21:00:31 -0400 Subject: [PATCH 26/30] Whooooops --- crates/editor/src/editor.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index bdacf0be38..1a17f38f92 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -3596,7 +3596,6 @@ impl Editor { let menu = menu.unwrap(); *context_menu = Some(ContextMenu::Completions(menu)); drop(context_menu); - this.completion_tasks.clear(); this.discard_copilot_suggestion(cx); cx.notify(); } else if this.completion_tasks.is_empty() { From ec4391b88e9a41e47de295896cb20764f007e053 Mon Sep 17 00:00:00 2001 From: Julia Date: Thu, 12 Oct 2023 22:08:47 -0400 Subject: [PATCH 27/30] Add setting to disable completion docs --- assets/settings/default.json | 3 +++ crates/editor/src/editor.rs | 24 ++++++++++++++++++++++-- crates/editor/src/editor_settings.rs | 2 ++ 3 files changed, 27 insertions(+), 2 deletions(-) diff --git a/assets/settings/default.json b/assets/settings/default.json index 8fb73a2ecb..8a3598eed1 100644 --- a/assets/settings/default.json +++ b/assets/settings/default.json @@ -50,6 +50,9 @@ // Whether to pop the completions menu while typing in an editor without // explicitly requesting it. "show_completions_on_input": true, + // Whether to display inline and alongside documentation for items in the + // completions menu + "show_completion_documentation": true, // Whether to show wrap guides in the editor. Setting this to true will // show a guide at the 'preferred_line_length' value if softwrap is set to // 'preferred_line_length', and will show any additional guides as specified diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 1a17f38f92..bb6d693d82 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -1000,6 +1000,11 @@ impl CompletionsMenu { project: Option>, cx: &mut ViewContext, ) { + let settings = settings::get::(cx); + if !settings.show_completion_documentation { + return; + } + let Some(project) = project else { return; }; @@ -1083,6 +1088,11 @@ impl CompletionsMenu { project: Option<&ModelHandle>, cx: &mut ViewContext, ) { + let settings = settings::get::(cx); + if !settings.show_completion_documentation { + return; + } + let completion_index = self.matches[self.selected_item].candidate_id; let Some(project) = project else { return; @@ -1241,6 +1251,9 @@ impl CompletionsMenu { ) -> AnyElement { enum CompletionTag {} + let settings = settings::get::(cx); + let show_completion_documentation = settings.show_completion_documentation; + let widest_completion_ix = self .matches .iter() @@ -1252,7 +1265,9 @@ impl CompletionsMenu { let mut len = completion.label.text.chars().count(); if let Some(Documentation::SingleLine(text)) = documentation { - len += text.chars().count(); + if show_completion_documentation { + len += text.chars().count(); + } } len @@ -1273,7 +1288,12 @@ impl CompletionsMenu { let item_ix = start_ix + ix; let candidate_id = mat.candidate_id; let completion = &completions_guard[candidate_id]; - let documentation = &completion.documentation; + + let documentation = if show_completion_documentation { + &completion.documentation + } else { + &None + }; items.push( MouseEventHandler::new::( diff --git a/crates/editor/src/editor_settings.rs b/crates/editor/src/editor_settings.rs index b06f23429a..75f8b800f9 100644 --- a/crates/editor/src/editor_settings.rs +++ b/crates/editor/src/editor_settings.rs @@ -7,6 +7,7 @@ pub struct EditorSettings { pub cursor_blink: bool, pub hover_popover_enabled: bool, pub show_completions_on_input: bool, + pub show_completion_documentation: bool, pub use_on_type_format: bool, pub scrollbar: Scrollbar, pub relative_line_numbers: bool, @@ -33,6 +34,7 @@ pub struct EditorSettingsContent { pub cursor_blink: Option, pub hover_popover_enabled: Option, pub show_completions_on_input: Option, + pub show_completion_documentation: Option, pub use_on_type_format: Option, pub scrollbar: Option, pub relative_line_numbers: Option, From 525ff6bf7458a2f48747fc9e06dd10e83845554b Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Fri, 13 Oct 2023 10:20:55 +0300 Subject: [PATCH 28/30] Remove zed -> ... -> semantic_index -> zed Cargo dependency cycle --- Cargo.lock | 2 +- crates/semantic_index/Cargo.toml | 4 ---- crates/zed/Cargo.toml | 4 ++++ .../examples/eval.rs => zed/examples/semantic_index_eval.rs} | 0 4 files changed, 5 insertions(+), 5 deletions(-) rename crates/{semantic_index/examples/eval.rs => zed/examples/semantic_index_eval.rs} (100%) diff --git a/Cargo.lock b/Cargo.lock index 01153ca0f8..85b80fa487 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6956,7 +6956,6 @@ dependencies = [ "unindent", "util", "workspace", - "zed", ] [[package]] @@ -10049,6 +10048,7 @@ name = "zed" version = "0.109.0" dependencies = [ "activity_indicator", + "ai", "anyhow", "assistant", "async-compression", diff --git a/crates/semantic_index/Cargo.toml b/crates/semantic_index/Cargo.toml index 34850f7035..1febb2af78 100644 --- a/crates/semantic_index/Cargo.toml +++ b/crates/semantic_index/Cargo.toml @@ -51,7 +51,6 @@ workspace = { path = "../workspace", features = ["test-support"] } settings = { path = "../settings", features = ["test-support"]} rust-embed = { version = "8.0", features = ["include-exclude"] } client = { path = "../client" } -zed = { path = "../zed"} node_runtime = { path = "../node_runtime"} pretty_assertions.workspace = true @@ -70,6 +69,3 @@ tree-sitter-elixir.workspace = true tree-sitter-lua.workspace = true tree-sitter-ruby.workspace = true tree-sitter-php.workspace = true - -[[example]] -name = "eval" diff --git a/crates/zed/Cargo.toml b/crates/zed/Cargo.toml index 4174f7d6d5..f9abcc1e91 100644 --- a/crates/zed/Cargo.toml +++ b/crates/zed/Cargo.toml @@ -15,6 +15,9 @@ doctest = false name = "Zed" path = "src/main.rs" +[[example]] +name = "semantic_index_eval" + [dependencies] audio = { path = "../audio" } activity_indicator = { path = "../activity_indicator" } @@ -141,6 +144,7 @@ urlencoding = "2.1.2" uuid.workspace = true [dev-dependencies] +ai = { path = "../ai" } call = { path = "../call", features = ["test-support"] } client = { path = "../client", features = ["test-support"] } editor = { path = "../editor", features = ["test-support"] } diff --git a/crates/semantic_index/examples/eval.rs b/crates/zed/examples/semantic_index_eval.rs similarity index 100% rename from crates/semantic_index/examples/eval.rs rename to crates/zed/examples/semantic_index_eval.rs From 803ab81eb6e1cc718679fdba862163c23d7ef174 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Fri, 13 Oct 2023 12:13:18 +0300 Subject: [PATCH 29/30] Update diagnostics indicator when diagnostics are udpated --- crates/diagnostics/src/items.rs | 4 ++++ crates/project/src/project.rs | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/crates/diagnostics/src/items.rs b/crates/diagnostics/src/items.rs index c3733018b6..8d3c2fedd6 100644 --- a/crates/diagnostics/src/items.rs +++ b/crates/diagnostics/src/items.rs @@ -38,6 +38,10 @@ impl DiagnosticIndicator { this.in_progress_checks.remove(language_server_id); cx.notify(); } + project::Event::DiagnosticsUpdated { .. } => { + this.summary = project.read(cx).diagnostic_summary(cx); + cx.notify(); + } _ => {} }) .detach(); diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index f9e1b1ce96..e3251d7483 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -2934,8 +2934,8 @@ impl Project { move |mut params, mut cx| { let this = this; let adapter = adapter.clone(); - adapter.process_diagnostics(&mut params); if let Some(this) = this.upgrade(&cx) { + adapter.process_diagnostics(&mut params); this.update(&mut cx, |this, cx| { this.update_diagnostics( server_id, From bfbe4ae4b47140024cdaf8d9680956bd228d6b84 Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Fri, 13 Oct 2023 18:58:59 +0200 Subject: [PATCH 30/30] Piotr/z 651 vue support (#3123) Release Notes: - Added Vue language support. --- Cargo.lock | 10 + Cargo.toml | 2 +- crates/language/src/language.rs | 3 - crates/project/src/project.rs | 25 ++- crates/util/src/github.rs | 1 + crates/zed/Cargo.toml | 1 + crates/zed/src/languages.rs | 10 +- crates/zed/src/languages/vue.rs | 214 ++++++++++++++++++++ crates/zed/src/languages/vue/brackets.scm | 2 + crates/zed/src/languages/vue/config.toml | 14 ++ crates/zed/src/languages/vue/highlights.scm | 15 ++ crates/zed/src/languages/vue/injections.scm | 7 + 12 files changed, 286 insertions(+), 18 deletions(-) create mode 100644 crates/zed/src/languages/vue.rs create mode 100644 crates/zed/src/languages/vue/brackets.scm create mode 100644 crates/zed/src/languages/vue/config.toml create mode 100644 crates/zed/src/languages/vue/highlights.scm create mode 100644 crates/zed/src/languages/vue/injections.scm diff --git a/Cargo.lock b/Cargo.lock index 85b80fa487..2ef86073ce 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8792,6 +8792,15 @@ dependencies = [ "tree-sitter", ] +[[package]] +name = "tree-sitter-vue" +version = "0.0.1" +source = "git+https://github.com/zed-industries/tree-sitter-vue?rev=95b2890#95b28908d90e928c308866f7631e73ef6e1d4b5f" +dependencies = [ + "cc", + "tree-sitter", +] + [[package]] name = "tree-sitter-yaml" version = "0.0.1" @@ -10161,6 +10170,7 @@ dependencies = [ "tree-sitter-svelte", "tree-sitter-toml", "tree-sitter-typescript", + "tree-sitter-vue", "tree-sitter-yaml", "unindent", "url", diff --git a/Cargo.toml b/Cargo.toml index 532610efd6..995cd15edd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -149,7 +149,7 @@ tree-sitter-yaml = { git = "https://github.com/zed-industries/tree-sitter-yaml", tree-sitter-lua = "0.0.14" tree-sitter-nix = { git = "https://github.com/nix-community/tree-sitter-nix", rev = "66e3e9ce9180ae08fc57372061006ef83f0abde7" } tree-sitter-nu = { git = "https://github.com/nushell/tree-sitter-nu", rev = "786689b0562b9799ce53e824cb45a1a2a04dc673"} - +tree-sitter-vue = {git = "https://github.com/zed-industries/tree-sitter-vue", rev = "95b2890"} [patch.crates-io] tree-sitter = { git = "https://github.com/tree-sitter/tree-sitter", rev = "35a6052fbcafc5e5fc0f9415b8652be7dcaf7222" } async-task = { git = "https://github.com/zed-industries/async-task", rev = "341b57d6de98cdfd7b418567b8de2022ca993a6e" } diff --git a/crates/language/src/language.rs b/crates/language/src/language.rs index bd389652a0..eb6f6e89f7 100644 --- a/crates/language/src/language.rs +++ b/crates/language/src/language.rs @@ -110,7 +110,6 @@ pub struct LanguageServerName(pub Arc); pub struct CachedLspAdapter { pub name: LanguageServerName, pub short_name: &'static str, - pub initialization_options: Option, pub disk_based_diagnostic_sources: Vec, pub disk_based_diagnostics_progress_token: Option, pub language_ids: HashMap, @@ -121,7 +120,6 @@ impl CachedLspAdapter { pub async fn new(adapter: Arc) -> Arc { let name = adapter.name().await; let short_name = adapter.short_name(); - let initialization_options = adapter.initialization_options().await; let disk_based_diagnostic_sources = adapter.disk_based_diagnostic_sources().await; let disk_based_diagnostics_progress_token = adapter.disk_based_diagnostics_progress_token().await; @@ -130,7 +128,6 @@ impl CachedLspAdapter { Arc::new(CachedLspAdapter { name, short_name, - initialization_options, disk_based_diagnostic_sources, disk_based_diagnostics_progress_token, language_ids, diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index e3251d7483..875086a4e3 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -2751,15 +2751,6 @@ impl Project { let lsp = project_settings.lsp.get(&adapter.name.0); let override_options = lsp.map(|s| s.initialization_options.clone()).flatten(); - let mut initialization_options = adapter.initialization_options.clone(); - match (&mut initialization_options, override_options) { - (Some(initialization_options), Some(override_options)) => { - merge_json_value_into(override_options, initialization_options); - } - (None, override_options) => initialization_options = override_options, - _ => {} - } - let server_id = pending_server.server_id; let container_dir = pending_server.container_dir.clone(); let state = LanguageServerState::Starting({ @@ -2771,7 +2762,7 @@ impl Project { cx.spawn_weak(|this, mut cx| async move { let result = Self::setup_and_insert_language_server( this, - initialization_options, + override_options, pending_server, adapter.clone(), language.clone(), @@ -2874,7 +2865,7 @@ impl Project { async fn setup_and_insert_language_server( this: WeakModelHandle, - initialization_options: Option, + override_initialization_options: Option, pending_server: PendingLanguageServer, adapter: Arc, language: Arc, @@ -2884,7 +2875,7 @@ impl Project { ) -> Result>> { let setup = Self::setup_pending_language_server( this, - initialization_options, + override_initialization_options, pending_server, adapter.clone(), server_id, @@ -2916,7 +2907,7 @@ impl Project { async fn setup_pending_language_server( this: WeakModelHandle, - initialization_options: Option, + override_options: Option, pending_server: PendingLanguageServer, adapter: Arc, server_id: LanguageServerId, @@ -3062,6 +3053,14 @@ impl Project { } }) .detach(); + let mut initialization_options = adapter.adapter.initialization_options().await; + match (&mut initialization_options, override_options) { + (Some(initialization_options), Some(override_options)) => { + merge_json_value_into(override_options, initialization_options); + } + (None, override_options) => initialization_options = override_options, + _ => {} + } let language_server = language_server.initialize(initialization_options).await?; diff --git a/crates/util/src/github.rs b/crates/util/src/github.rs index b1e981ae49..a3df4c996b 100644 --- a/crates/util/src/github.rs +++ b/crates/util/src/github.rs @@ -16,6 +16,7 @@ pub struct GithubRelease { pub pre_release: bool, pub assets: Vec, pub tarball_url: String, + pub zipball_url: String, } #[derive(Deserialize, Debug)] diff --git a/crates/zed/Cargo.toml b/crates/zed/Cargo.toml index f9abcc1e91..aeabd4b453 100644 --- a/crates/zed/Cargo.toml +++ b/crates/zed/Cargo.toml @@ -138,6 +138,7 @@ tree-sitter-yaml.workspace = true tree-sitter-lua.workspace = true tree-sitter-nix.workspace = true tree-sitter-nu.workspace = true +tree-sitter-vue.workspace = true url = "2.2" urlencoding = "2.1.2" diff --git a/crates/zed/src/languages.rs b/crates/zed/src/languages.rs index 04e5292a7d..caf3cbf7c9 100644 --- a/crates/zed/src/languages.rs +++ b/crates/zed/src/languages.rs @@ -24,6 +24,7 @@ mod rust; mod svelte; mod tailwind; mod typescript; +mod vue; mod yaml; // 1. Add tree-sitter-{language} parser to zed crate @@ -190,13 +191,20 @@ pub fn init( language( "php", tree_sitter_php::language(), - vec![Arc::new(php::IntelephenseLspAdapter::new(node_runtime))], + vec![Arc::new(php::IntelephenseLspAdapter::new( + node_runtime.clone(), + ))], ); language("elm", tree_sitter_elm::language(), vec![]); language("glsl", tree_sitter_glsl::language(), vec![]); language("nix", tree_sitter_nix::language(), vec![]); language("nu", tree_sitter_nu::language(), vec![]); + language( + "vue", + tree_sitter_vue::language(), + vec![Arc::new(vue::VueLspAdapter::new(node_runtime))], + ); } #[cfg(any(test, feature = "test-support"))] diff --git a/crates/zed/src/languages/vue.rs b/crates/zed/src/languages/vue.rs new file mode 100644 index 0000000000..f0374452df --- /dev/null +++ b/crates/zed/src/languages/vue.rs @@ -0,0 +1,214 @@ +use anyhow::{anyhow, Result}; +use async_trait::async_trait; +use futures::StreamExt; +pub use language::*; +use lsp::{CodeActionKind, LanguageServerBinary}; +use node_runtime::NodeRuntime; +use parking_lot::Mutex; +use serde_json::Value; +use smol::fs::{self}; +use std::{ + any::Any, + ffi::OsString, + path::{Path, PathBuf}, + sync::Arc, +}; +use util::ResultExt; + +pub struct VueLspVersion { + vue_version: String, + ts_version: String, +} + +pub struct VueLspAdapter { + node: Arc, + typescript_install_path: Mutex>, +} + +impl VueLspAdapter { + const SERVER_PATH: &'static str = + "node_modules/@vue/language-server/bin/vue-language-server.js"; + // TODO: this can't be hardcoded, yet we have to figure out how to pass it in initialization_options. + const TYPESCRIPT_PATH: &'static str = "node_modules/typescript/lib"; + pub fn new(node: Arc) -> Self { + let typescript_install_path = Mutex::new(None); + Self { + node, + typescript_install_path, + } + } +} +#[async_trait] +impl super::LspAdapter for VueLspAdapter { + async fn name(&self) -> LanguageServerName { + LanguageServerName("vue-language-server".into()) + } + + fn short_name(&self) -> &'static str { + "vue-language-server" + } + + async fn fetch_latest_server_version( + &self, + _: &dyn LspAdapterDelegate, + ) -> Result> { + Ok(Box::new(VueLspVersion { + vue_version: self + .node + .npm_package_latest_version("@vue/language-server") + .await?, + ts_version: self.node.npm_package_latest_version("typescript").await?, + }) as Box<_>) + } + async fn initialization_options(&self) -> Option { + let typescript_sdk_path = self.typescript_install_path.lock(); + let typescript_sdk_path = typescript_sdk_path + .as_ref() + .expect("initialization_options called without a container_dir for typescript"); + + Some(serde_json::json!({ + "typescript": { + "tsdk": typescript_sdk_path + } + })) + } + fn code_action_kinds(&self) -> Option> { + // REFACTOR is explicitly disabled, as vue-lsp does not adhere to LSP protocol for code actions with these - it + // sends back a CodeAction with neither `command` nor `edits` fields set, which is against the spec. + Some(vec![ + CodeActionKind::EMPTY, + CodeActionKind::QUICKFIX, + CodeActionKind::REFACTOR_REWRITE, + ]) + } + async fn fetch_server_binary( + &self, + version: Box, + container_dir: PathBuf, + _: &dyn LspAdapterDelegate, + ) -> Result { + let version = version.downcast::().unwrap(); + let server_path = container_dir.join(Self::SERVER_PATH); + let ts_path = container_dir.join(Self::TYPESCRIPT_PATH); + if fs::metadata(&server_path).await.is_err() { + self.node + .npm_install_packages( + &container_dir, + &[("@vue/language-server", version.vue_version.as_str())], + ) + .await?; + } + assert!(fs::metadata(&server_path).await.is_ok()); + if fs::metadata(&ts_path).await.is_err() { + self.node + .npm_install_packages( + &container_dir, + &[("typescript", version.ts_version.as_str())], + ) + .await?; + } + + assert!(fs::metadata(&ts_path).await.is_ok()); + *self.typescript_install_path.lock() = Some(ts_path); + Ok(LanguageServerBinary { + path: self.node.binary_path().await?, + arguments: vue_server_binary_arguments(&server_path), + }) + } + + async fn cached_server_binary( + &self, + container_dir: PathBuf, + _: &dyn LspAdapterDelegate, + ) -> Option { + let (server, ts_path) = get_cached_server_binary(container_dir, self.node.clone()).await?; + *self.typescript_install_path.lock() = Some(ts_path); + Some(server) + } + + async fn installation_test_binary( + &self, + container_dir: PathBuf, + ) -> Option { + let (server, ts_path) = get_cached_server_binary(container_dir, self.node.clone()) + .await + .map(|(mut binary, ts_path)| { + binary.arguments = vec!["--help".into()]; + (binary, ts_path) + })?; + *self.typescript_install_path.lock() = Some(ts_path); + Some(server) + } + + async fn label_for_completion( + &self, + item: &lsp::CompletionItem, + language: &Arc, + ) -> Option { + use lsp::CompletionItemKind as Kind; + let len = item.label.len(); + let grammar = language.grammar()?; + let highlight_id = match item.kind? { + Kind::CLASS | Kind::INTERFACE => grammar.highlight_id_for_name("type"), + Kind::CONSTRUCTOR => grammar.highlight_id_for_name("type"), + Kind::CONSTANT => grammar.highlight_id_for_name("constant"), + Kind::FUNCTION | Kind::METHOD => grammar.highlight_id_for_name("function"), + Kind::PROPERTY | Kind::FIELD => grammar.highlight_id_for_name("tag"), + Kind::VARIABLE => grammar.highlight_id_for_name("type"), + Kind::KEYWORD => grammar.highlight_id_for_name("keyword"), + Kind::VALUE => grammar.highlight_id_for_name("tag"), + _ => None, + }?; + + let text = match &item.detail { + Some(detail) => format!("{} {}", item.label, detail), + None => item.label.clone(), + }; + + Some(language::CodeLabel { + text, + runs: vec![(0..len, highlight_id)], + filter_range: 0..len, + }) + } +} + +fn vue_server_binary_arguments(server_path: &Path) -> Vec { + vec![server_path.into(), "--stdio".into()] +} + +type TypescriptPath = PathBuf; +async fn get_cached_server_binary( + container_dir: PathBuf, + node: Arc, +) -> Option<(LanguageServerBinary, TypescriptPath)> { + (|| async move { + let mut last_version_dir = None; + let mut entries = fs::read_dir(&container_dir).await?; + while let Some(entry) = entries.next().await { + let entry = entry?; + if entry.file_type().await?.is_dir() { + last_version_dir = Some(entry.path()); + } + } + let last_version_dir = last_version_dir.ok_or_else(|| anyhow!("no cached binary"))?; + let server_path = last_version_dir.join(VueLspAdapter::SERVER_PATH); + let typescript_path = last_version_dir.join(VueLspAdapter::TYPESCRIPT_PATH); + if server_path.exists() && typescript_path.exists() { + Ok(( + LanguageServerBinary { + path: node.binary_path().await?, + arguments: vue_server_binary_arguments(&server_path), + }, + typescript_path, + )) + } else { + Err(anyhow!( + "missing executable in directory {:?}", + last_version_dir + )) + } + })() + .await + .log_err() +} diff --git a/crates/zed/src/languages/vue/brackets.scm b/crates/zed/src/languages/vue/brackets.scm new file mode 100644 index 0000000000..2d12b17daa --- /dev/null +++ b/crates/zed/src/languages/vue/brackets.scm @@ -0,0 +1,2 @@ +("<" @open ">" @close) +("\"" @open "\"" @close) diff --git a/crates/zed/src/languages/vue/config.toml b/crates/zed/src/languages/vue/config.toml new file mode 100644 index 0000000000..c41a667b75 --- /dev/null +++ b/crates/zed/src/languages/vue/config.toml @@ -0,0 +1,14 @@ +name = "Vue.js" +path_suffixes = ["vue"] +block_comment = [""] +autoclose_before = ";:.,=}])>" +brackets = [ + { start = "{", end = "}", close = true, newline = true }, + { start = "[", end = "]", close = true, newline = true }, + { start = "(", end = ")", close = true, newline = true }, + { start = "<", end = ">", close = true, newline = true, not_in = ["string", "comment"] }, + { start = "\"", end = "\"", close = true, newline = false, not_in = ["string"] }, + { start = "'", end = "'", close = true, newline = false, not_in = ["string", "comment"] }, + { start = "`", end = "`", close = true, newline = false, not_in = ["string"] }, +] +word_characters = ["-"] diff --git a/crates/zed/src/languages/vue/highlights.scm b/crates/zed/src/languages/vue/highlights.scm new file mode 100644 index 0000000000..1a80c84f68 --- /dev/null +++ b/crates/zed/src/languages/vue/highlights.scm @@ -0,0 +1,15 @@ +(attribute) @property +(directive_attribute) @property +(quoted_attribute_value) @string +(interpolation) @punctuation.special +(raw_text) @embedded + +((tag_name) @type + (#match? @type "^[A-Z]")) + +((directive_name) @keyword + (#match? @keyword "^v-")) + +(start_tag) @tag +(end_tag) @tag +(self_closing_tag) @tag diff --git a/crates/zed/src/languages/vue/injections.scm b/crates/zed/src/languages/vue/injections.scm new file mode 100644 index 0000000000..9084e373f2 --- /dev/null +++ b/crates/zed/src/languages/vue/injections.scm @@ -0,0 +1,7 @@ +(script_element + (raw_text) @content + (#set! "language" "javascript")) + +(style_element + (raw_text) @content + (#set! "language" "css"))