Autocomplete docs (#3126)

Release Notes:

- Added documentation display for autocomplete items.
- Fixed autocomplete filtering blocking the Zed UI, causing hitches and
input delays with large completion lists.
- Fixed hover popup link not firing if the mouse moved a slight amount
while clicking.
- Added support for absolute path file links in hover popup and
autocomplete docs.
This commit is contained in:
Julia 2023-10-13 13:26:45 -04:00 committed by GitHub
commit 2323fd17b0
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
24 changed files with 1237 additions and 345 deletions

2
Cargo.lock generated
View file

@ -2404,7 +2404,6 @@ dependencies = [
"parking_lot 0.11.2",
"postage",
"project",
"pulldown-cmark",
"rand 0.8.5",
"rich_text",
"rpc",
@ -3989,6 +3988,7 @@ dependencies = [
"lsp",
"parking_lot 0.11.2",
"postage",
"pulldown-cmark",
"rand 0.8.5",
"regex",
"rpc",

View file

@ -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

View file

@ -225,6 +225,7 @@ impl Server {
.add_request_handler(forward_project_request::<proto::OpenBufferByPath>)
.add_request_handler(forward_project_request::<proto::GetCompletions>)
.add_request_handler(forward_project_request::<proto::ApplyCompletionAdditionalEdits>)
.add_request_handler(forward_project_request::<proto::ResolveCompletionDocumentation>)
.add_request_handler(forward_project_request::<proto::GetCodeActions>)
.add_request_handler(forward_project_request::<proto::ApplyCodeAction>)
.add_request_handler(forward_project_request::<proto::PrepareRename>)

View file

@ -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

File diff suppressed because it is too large Load diff

View file

@ -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<bool>,
pub hover_popover_enabled: Option<bool>,
pub show_completions_on_input: Option<bool>,
pub show_completion_documentation: Option<bool>,
pub use_on_type_format: Option<bool>,
pub scrollbar: Option<ScrollbarContent>,
pub relative_line_numbers: Option<bool>,

View file

@ -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::<Vec<_>>(),
&["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::<Vec<_>>(),
&["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::<Vec<_>>(),
&["bg-yellow"]

View file

@ -2428,7 +2428,7 @@ impl Element<Editor> for EditorElement {
}
let active = matches!(
editor.context_menu,
editor.context_menu.read().as_ref(),
Some(crate::ContextMenu::CodeActions(_))
);
@ -2439,9 +2439,13 @@ impl Element<Editor> 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(

View file

@ -9,13 +9,15 @@ 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,
};
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;
use workspace::Workspace;
pub const HOVER_DELAY_MILLIS: u64 = 350;
pub const HOVER_REQUEST_DELAY_MILLIS: u64 = 200;
@ -105,12 +107,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: RangeInEditor::Inlay(inlay_hover.range.clone()),
blocks: vec![inlay_hover.tooltip],
language: None,
rendered_content: None,
blocks,
parsed_content,
};
this.update(&mut cx, |this, cx| {
@ -288,35 +293,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: RangeInEditor::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: RangeInEditor::Text(range),
blocks: hover_result.contents,
language: hover_result.language,
rendered_content: None,
})
});
_ => None,
};
this.update(&mut cx, |this, cx| {
if let Some(symbol_range) = hover_popover
@ -345,44 +353,56 @@ fn show_hover(
editor.hover_state.info_task = Some(task);
}
fn render_blocks(
async fn parse_blocks(
blocks: &[HoverBlock],
language_registry: &Arc<LanguageRegistry>,
language: Option<&Arc<Language>>,
) -> RichText {
let mut data = RichText {
text: Default::default(),
highlights: Default::default(),
region_ranges: Default::default(),
regions: Default::default(),
};
language: Option<Arc<Language>>,
) -> markdown::ParsedMarkdown {
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);
markdown::new_paragraph(&mut text, &mut Vec::new());
text.push_str(&block.text);
}
HoverBlockKind::Markdown => {
render_markdown_mut(&block.text, language_registry, language, &mut data)
markdown::parse_markdown_block(
&block.text,
language_registry,
language.clone(),
&mut text,
&mut highlights,
&mut region_ranges,
&mut regions,
)
.await
}
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);
markdown::highlight_code(&mut text, &mut highlights, &block.text, &language);
} else {
data.text.push_str(&block.text);
text.push_str(&block.text);
}
}
}
}
data.text = data.text.trim().to_string();
data
ParsedMarkdown {
text: text.trim().to_string(),
highlights,
region_ranges,
regions,
}
}
#[derive(Default)]
@ -403,6 +423,7 @@ impl HoverState {
snapshot: &EditorSnapshot,
style: &EditorStyle,
visible_rows: Range<u32>,
workspace: Option<WeakViewHandle<Workspace>>,
cx: &mut ViewContext<Editor>,
) -> Option<(DisplayPoint, Vec<AnyElement<Editor>>)> {
// If there is a diagnostic, position the popovers based on that.
@ -432,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))
@ -444,32 +465,23 @@ pub struct InfoPopover {
pub project: ModelHandle<Project>,
symbol_range: RangeInEditor,
pub blocks: Vec<HoverBlock>,
language: Option<Arc<Language>>,
rendered_content: Option<RichText>,
parsed_content: ParsedMarkdown,
}
impl InfoPopover {
pub fn render(
&mut self,
style: &EditorStyle,
workspace: Option<WeakViewHandle<Workspace>>,
cx: &mut ViewContext<Editor>,
) -> AnyElement<Editor> {
let rendered_content = self.rendered_content.get_or_insert_with(|| {
render_blocks(
&self.blocks,
self.project.read(cx).languages(),
self.language.as_ref(),
)
});
MouseEventHandler::new::<InfoPopover, _>(0, cx, move |_, cx| {
let code_span_background_color = style.document_highlight_read_background;
MouseEventHandler::new::<InfoPopover, _>(0, cx, |_, cx| {
Flex::column()
.scrollable::<HoverBlock>(1, None, cx)
.with_child(rendered_content.element(
style.syntax.clone(),
style.text.clone(),
code_span_background_color,
.scrollable::<HoverBlock>(0, None, cx)
.with_child(crate::render_parsed_markdown::<HoverBlock>(
&self.parsed_content,
style,
workspace,
cx,
))
.contained()
@ -572,7 +584,6 @@ mod tests {
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;
@ -793,7 +804,7 @@ mod tests {
}],
);
let rendered = render_blocks(&blocks, &Default::default(), None);
let rendered = smol::block_on(parse_blocks(&blocks, &Default::default(), None));
assert_eq!(
rendered.text,
code_str.trim(),
@ -900,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(),
@ -921,7 +932,7 @@ mod tests {
- a
- b
* two
- [c](the-url)
- [c](https://the-url)
- d"
.unindent(),
kind: HoverBlockKind::Markdown,
@ -985,7 +996,7 @@ mod tests {
expected_styles,
} in &rows[0..]
{
let rendered = render_blocks(&blocks, &Default::default(), None);
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
@ -1001,11 +1012,8 @@ mod tests {
.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))
let highlight = highlight.to_highlight_style(&style.syntax)?;
Some((range.clone(), highlight))
})
.collect();
@ -1258,11 +1266,7 @@ mod tests {
"Popover range should match the new type label part"
);
assert_eq!(
popover
.rendered_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"
);
@ -1316,11 +1320,7 @@ mod tests {
"Popover range should match the struct label part"
);
assert_eq!(
popover
.rendered_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"
);

View file

@ -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<Option<usize>>,
scroll_position: Cell<f32>,
type_tag: TypeTag,
}
pub struct Flex<V> {
@ -66,8 +67,14 @@ impl<V: 'static> Flex<V> {
where
Tag: 'static,
{
let scroll_state = cx.default_element_state::<Tag, Rc<ScrollState>>(element_id);
scroll_state.read(cx).scroll_to.set(scroll_to);
let scroll_state = cx.element_state::<Tag, Rc<ScrollState>>(
element_id,
Rc::new(ScrollState {
scroll_to: Cell::new(scroll_to),
scroll_position: Default::default(),
type_tag: TypeTag::new::<Tag>(),
}),
);
self.scroll_state = Some((scroll_state, cx.handle().id()));
self
}
@ -276,38 +283,44 @@ impl<V: 'static> Element<V> for Flex<V> {
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::<Self>(*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 */ }),
)
}

View file

@ -45,6 +45,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

View file

@ -1,11 +1,13 @@
pub use crate::{
diagnostic_set::DiagnosticSet,
highlight_map::{HighlightId, HighlightMap},
markdown::ParsedMarkdown,
proto, BracketPair, Grammar, Language, LanguageConfig, LanguageRegistry, PLAIN_TEXT,
};
use crate::{
diagnostic_set::{DiagnosticEntry, DiagnosticGroup},
language_settings::{language_settings, LanguageSettings},
markdown::parse_markdown,
outline::OutlineItem,
syntax_map::{
SyntaxLayerInfo, SyntaxMap, SyntaxMapCapture, SyntaxMapCaptures, SyntaxMapMatches,
@ -143,11 +145,51 @@ pub struct Diagnostic {
pub is_unnecessary: bool,
}
pub async fn prepare_completion_documentation(
documentation: &lsp::Documentation,
language_registry: &Arc<LanguageRegistry>,
language: Option<Arc<Language>>,
) -> Documentation {
match documentation {
lsp::Documentation::String(text) => {
if text.lines().count() <= 1 {
Documentation::SingleLine(text.clone())
} else {
Documentation::MultiLinePlainText(text.clone())
}
}
lsp::Documentation::MarkupContent(lsp::MarkupContent { kind, value }) => match kind {
lsp::MarkupKind::PlainText => {
if value.lines().count() <= 1 {
Documentation::SingleLine(value.clone())
} else {
Documentation::MultiLinePlainText(value.clone())
}
}
lsp::MarkupKind::Markdown => {
let parsed = parse_markdown(value, language_registry, language).await;
Documentation::MultiLineMarkdown(parsed)
}
},
}
}
#[derive(Clone, Debug)]
pub enum Documentation {
Undocumented,
SingleLine(String),
MultiLinePlainText(String),
MultiLineMarkdown(ParsedMarkdown),
}
#[derive(Clone, Debug)]
pub struct Completion {
pub old_range: Range<Anchor>,
pub new_text: String,
pub label: CodeLabel,
pub documentation: Option<Documentation>,
pub server_id: LanguageServerId,
pub lsp_completion: lsp::CompletionItem,
}

View file

@ -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;

View file

@ -0,0 +1,301 @@
use std::sync::Arc;
use std::{ops::Range, path::PathBuf};
use crate::{HighlightId, Language, LanguageRegistry};
use gpui::fonts::{self, HighlightStyle, Weight};
use pulldown_cmark::{CodeBlockKind, Event, Options, Parser, Tag};
#[derive(Debug, Clone)]
pub struct ParsedMarkdown {
pub text: String,
pub highlights: Vec<(Range<usize>, MarkdownHighlight)>,
pub region_ranges: Vec<Range<usize>>,
pub regions: Vec<ParsedRegion>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum MarkdownHighlight {
Style(MarkdownHighlightStyle),
Code(HighlightId),
}
impl MarkdownHighlight {
pub fn to_highlight_style(&self, theme: &theme::SyntaxTheme) -> Option<HighlightStyle> {
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,
pub underline: bool,
pub weight: Weight,
}
#[derive(Debug, Clone)]
pub struct ParsedRegion {
pub code: bool,
pub link: Option<Link>,
}
#[derive(Debug, Clone)]
pub enum Link {
Web { url: String },
Path { path: PathBuf },
}
impl Link {
fn identify(text: String) -> Option<Link> {
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(
markdown: &str,
language_registry: &Arc<LanguageRegistry>,
language: Option<Arc<Language>>,
) -> ParsedMarkdown {
let mut text = String::new();
let mut highlights = Vec::new();
let mut region_ranges = Vec::new();
let mut regions = Vec::new();
parse_markdown_block(
markdown,
language_registry,
language,
&mut text,
&mut highlights,
&mut region_ranges,
&mut regions,
)
.await;
ParsedMarkdown {
text,
highlights,
region_ranges,
regions,
}
}
pub async fn parse_markdown_block(
markdown: &str,
language_registry: &Arc<LanguageRegistry>,
language: Option<Arc<Language>>,
text: &mut String,
highlights: &mut Vec<(Range<usize>, MarkdownHighlight)>,
region_ranges: &mut Vec<Range<usize>>,
regions: &mut Vec<ParsedRegion>,
) {
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) = &current_language {
highlight_code(text, highlights, t.as_ref(), language);
} else {
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) = link_url.clone().and_then(|u| Link::identify(u)) {
region_ranges.push(prev_len..text.len());
regions.push(ParsedRegion {
code: false,
link: Some(link),
});
style.underline = true;
}
if style != MarkdownHighlightStyle::default() {
let mut new_highlight = true;
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 {
let range = prev_len..text.len();
highlights.push((range, MarkdownHighlight::Style(style)));
}
}
}
}
Event::Code(t) => {
text.push_str(t.as_ref());
region_ranges.push(prev_len..text.len());
let link = link_url.clone().and_then(|u| Link::identify(u));
if link.is_some() {
highlights.push((
prev_len..text.len(),
MarkdownHighlight::Style(MarkdownHighlightStyle {
underline: true,
..Default::default()
}),
));
}
regions.push(ParsedRegion { code: true, link });
}
Event::Start(tag) => match tag {
Tag::Paragraph => new_paragraph(text, &mut list_stack),
Tag::Heading(_, _, _) => {
new_paragraph(text, &mut list_stack);
bold_depth += 1;
}
Tag::CodeBlock(kind) => {
new_paragraph(text, &mut list_stack);
current_language = if let CodeBlockKind::Fenced(language) = kind {
language_registry
.language_for_name(language.as_ref())
.await
.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(' '),
_ => {}
}
}
}
pub fn highlight_code(
text: &mut String,
highlights: &mut Vec<(Range<usize>, MarkdownHighlight)>,
content: &str,
language: &Arc<Language>,
) {
let prev_len = text.len();
text.push_str(content);
for (range, highlight_id) in language.highlight_text(&content.into(), 0..content.len()) {
let highlight = MarkdownHighlight::Code(highlight_id);
highlights.push((prev_len + range.start..prev_len + range.end, highlight));
}
}
pub fn new_paragraph(text: &mut String, list_stack: &mut Vec<(Option<u64>, 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(" ");
}
}

View file

@ -482,6 +482,7 @@ pub async fn deserialize_completion(
lsp_completion.filter_text.as_deref(),
)
}),
documentation: None,
server_id: LanguageServerId(completion.server_id as usize),
lsp_completion,
})

View file

@ -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
// <Foo className="bar" />
// <Foo className={`bar`} />
// <Foo className={something + "bar"} />
// <Foo className={something + "bar"} />
// const classes = "awesome ";
// <Foo className={classes} />
fn request_internal<T: request::Request>(
next_id: &AtomicUsize,
response_handlers: &Mutex<Option<HashMap<usize, ResponseHandler>>>,

View file

@ -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<lsp::CompletionResponse>,
_: ModelHandle<Project>,
project: ModelHandle<Project>,
buffer: ModelHandle<Buffer>,
server_id: LanguageServerId,
cx: AsyncAppContext,
@ -1358,10 +1358,11 @@ impl LspCommand for GetCompletions {
}
}
} else {
Default::default()
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);
@ -1370,6 +1371,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.
@ -1445,14 +1454,30 @@ 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 {
Some(
prepare_completion_documentation(
lsp_docs,
&language_registry,
language.clone(),
)
.await,
)
} else {
None
};
Completion {
old_range,
new_text,
@ -1462,6 +1487,7 @@ impl LspCommand for GetCompletions {
lsp_completion.filter_text.as_deref(),
)
}),
documentation,
server_id,
lsp_completion,
}

View file

@ -592,6 +592,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);
@ -7352,6 +7353,40 @@ impl Project {
})
}
async fn handle_resolve_completion_documentation(
this: ModelHandle<Self>,
envelope: TypedEnvelope<proto::ResolveCompletionDocumentation>,
_: Arc<Client>,
mut cx: AsyncAppContext,
) -> Result<proto::ResolveCompletionDocumentationResponse> {
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::request::ResolveCompletionItem>(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<Self>,
envelope: TypedEnvelope<proto::ApplyCodeAction>,

View file

@ -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;

View file

@ -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,

View file

@ -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;

View file

@ -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();

View file

@ -867,9 +867,13 @@ 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 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_max_width: f32,
pub alongside_docs_container: ContainerStyle,
}
#[derive(Clone, Copy, Default, Deserialize, JsonSchema)]

View file

@ -206,9 +206,13 @@ 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,
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_max_width: 700,
alongside_docs_container: { padding: autocomplete_item.padding }
},
diagnostic_header: {
background: background(theme.middle),