Detailed changes
@@ -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",
@@ -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
@@ -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>)
@@ -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
@@ -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};
@@ -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};
@@ -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,
+ markdown, point_from_lsp, AutoindentMode, BracketPair, Buffer, CodeAction, CodeLabel,
+ Completion, CursorShape, Diagnostic, DiagnosticSeverity, Documentation, File, IndentKind,
+ IndentSize, Language, LanguageRegistry, LanguageServerName, OffsetRangeExt, OffsetUtf16, Point,
+ Selection, SelectionGoal, TransactionId,
};
use link_go_to_definition::{
hide_link_definition, show_link_definition, GoToDefinitionLink, InlayHighlight,
@@ -77,9 +77,10 @@ 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;
+use rpc::proto::{self, PeerId};
use scroll::{
autoscroll::Autoscroll, OngoingScroll, ScrollAnchor, ScrollManager, ScrollbarAutoHide,
};
@@ -118,6 +119,67 @@ pub const DOCUMENT_HIGHLIGHTS_DEBOUNCE_TIMEOUT: Duration = Duration::from_millis
pub const FORMAT_TIMEOUT: Duration = Duration::from_secs(2);
+pub fn render_parsed_markdown<Tag: 'static>(
+ parsed: &language::ParsedMarkdown,
+ editor_style: &EditorStyle,
+ workspace: Option<WeakViewHandle<Workspace>>,
+ cx: &mut ViewContext<Editor>,
+) -> Text {
+ enum RenderedMarkdown {}
+
+ 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(parsed.text, editor_style.text.clone())
+ .with_highlights(
+ parsed
+ .highlights
+ .iter()
+ .filter_map(|(range, highlight)| {
+ let highlight = highlight.to_highlight_style(&editor_style.syntax)?;
+ Some((range.clone(), highlight))
+ })
+ .collect::<Vec<_>>(),
+ )
+ .with_custom_runs(parsed.region_ranges, move |ix, bounds, cx| {
+ region_id += 1;
+ let region = parsed.regions[ix].clone();
+
+ 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::<Editor, _>(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();
+ });
+ }
+ }
+ }),
+ );
+ }
+
+ if region.code {
+ cx.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)]
@@ -594,7 +656,7 @@ pub struct Editor {
background_highlights: BTreeMap<TypeId, BackgroundHighlight>,
inlay_background_highlights: TreeMap<Option<TypeId>, InlayBackgroundHighlight>,
nav_history: Option<ItemNavHistory>,
- context_menu: Option<ContextMenu>,
+ context_menu: RwLock<Option<ContextMenu>>,
mouse_context_menu: ViewHandle<context_menu::ContextMenu>,
completion_tasks: Vec<(CompletionId, Task<Option<()>>)>,
next_completion_id: CompletionId,
@@ -787,10 +849,14 @@ enum ContextMenu {
}
impl ContextMenu {
- fn select_first(&mut self, cx: &mut ViewContext<Editor>) -> bool {
+ fn select_first(
+ &mut self,
+ project: Option<&ModelHandle<Project>>,
+ cx: &mut ViewContext<Editor>,
+ ) -> 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
@@ -799,10 +865,14 @@ impl ContextMenu {
}
}
- fn select_prev(&mut self, cx: &mut ViewContext<Editor>) -> bool {
+ fn select_prev(
+ &mut self,
+ project: Option<&ModelHandle<Project>>,
+ cx: &mut ViewContext<Editor>,
+ ) -> 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
@@ -811,10 +881,14 @@ impl ContextMenu {
}
}
- fn select_next(&mut self, cx: &mut ViewContext<Editor>) -> bool {
+ fn select_next(
+ &mut self,
+ project: Option<&ModelHandle<Project>>,
+ cx: &mut ViewContext<Editor>,
+ ) -> 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
@@ -823,10 +897,14 @@ impl ContextMenu {
}
}
- fn select_last(&mut self, cx: &mut ViewContext<Editor>) -> bool {
+ fn select_last(
+ &mut self,
+ project: Option<&ModelHandle<Project>>,
+ cx: &mut ViewContext<Editor>,
+ ) -> 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
@@ -846,99 +924,350 @@ impl ContextMenu {
&self,
cursor_position: DisplayPoint,
style: EditorStyle,
+ workspace: Option<WeakViewHandle<Workspace>>,
cx: &mut ViewContext<Editor>,
) -> (DisplayPoint, AnyElement<Editor>) {
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),
}
}
}
+#[derive(Clone)]
struct CompletionsMenu {
id: CompletionId,
initial_position: Anchor,
buffer: ModelHandle<Buffer>,
- project: Option<ModelHandle<Project>>,
- completions: Arc<[Completion]>,
- match_candidates: Vec<StringMatchCandidate>,
+ completions: Arc<RwLock<Box<[Completion]>>>,
+ match_candidates: Arc<[StringMatchCandidate]>,
matches: Arc<[StringMatch]>,
selected_item: usize,
list: UniformListState,
}
impl CompletionsMenu {
- fn select_first(&mut self, cx: &mut ViewContext<Editor>) {
+ fn select_first(
+ &mut self,
+ project: Option<&ModelHandle<Project>>,
+ cx: &mut ViewContext<Editor>,
+ ) {
self.selected_item = 0;
self.list.scroll_to(ScrollTarget::Show(self.selected_item));
+ self.attempt_resolve_selected_completion_documentation(project, cx);
cx.notify();
}
- fn select_prev(&mut self, cx: &mut ViewContext<Editor>) {
+ fn select_prev(
+ &mut self,
+ project: Option<&ModelHandle<Project>>,
+ cx: &mut ViewContext<Editor>,
+ ) {
if self.selected_item > 0 {
self.selected_item -= 1;
self.list.scroll_to(ScrollTarget::Show(self.selected_item));
}
+ self.attempt_resolve_selected_completion_documentation(project, cx);
cx.notify();
}
- fn select_next(&mut self, cx: &mut ViewContext<Editor>) {
+ fn select_next(
+ &mut self,
+ project: Option<&ModelHandle<Project>>,
+ cx: &mut ViewContext<Editor>,
+ ) {
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_documentation(project, cx);
cx.notify();
}
- fn select_last(&mut self, cx: &mut ViewContext<Editor>) {
+ fn select_last(
+ &mut self,
+ project: Option<&ModelHandle<Project>>,
+ cx: &mut ViewContext<Editor>,
+ ) {
self.selected_item = self.matches.len() - 1;
self.list.scroll_to(ScrollTarget::Show(self.selected_item));
+ self.attempt_resolve_selected_completion_documentation(project, cx);
cx.notify();
}
+ fn pre_resolve_completion_documentation(
+ &self,
+ project: Option<ModelHandle<Project>>,
+ cx: &mut ViewContext<Editor>,
+ ) {
+ let settings = settings::get::<EditorSettings>(cx);
+ if !settings.show_completion_documentation {
+ return;
+ }
+
+ 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<Project>>,
+ cx: &mut ViewContext<Editor>,
+ ) {
+ let settings = settings::get::<EditorSettings>(cx);
+ if !settings.show_completion_documentation {
+ return;
+ }
+
+ let completion_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();
+ let completion = &completions_guard[completion_index];
+ if completion.documentation.is_some() {
+ return;
+ }
+
+ let server_id = completion.server_id;
+ 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();
+
+ 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;
+ };
+
+ 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();
+ }
+ }
+
+ async fn resolve_completion_documentation_remote(
+ project_id: u64,
+ server_id: LanguageServerId,
+ completions: Arc<RwLock<Box<[Completion]>>>,
+ completion_index: usize,
+ completion: lsp::CompletionItem,
+ client: Arc<Client>,
+ language_registry: Arc<LanguageRegistry>,
+ ) {
+ 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<lsp::LanguageServer>,
+ completions: Arc<RwLock<Box<[Completion]>>>,
+ completion_index: usize,
+ completion: lsp::CompletionItem,
+ language_registry: Arc<LanguageRegistry>,
+ ) {
+ let can_resolve = server
+ .capabilities()
+ .completion_provider
+ .as_ref()
+ .and_then(|options| options.resolve_provider)
+ .unwrap_or(false);
+ if !can_resolve {
+ return;
+ }
+
+ let request = server.request::<lsp::request::ResolveCompletionItem>(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;
+
+ 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 {
!self.matches.is_empty()
}
- fn render(&self, style: EditorStyle, cx: &mut ViewContext<Editor>) -> AnyElement<Editor> {
+ fn render(
+ &self,
+ style: EditorStyle,
+ workspace: Option<WeakViewHandle<Workspace>>,
+ cx: &mut ViewContext<Editor>,
+ ) -> AnyElement<Editor> {
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::<Vec<_>>()
- });
- 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 settings = settings::get::<EditorSettings>(cx);
+ let show_completion_documentation = settings.show_completion_documentation;
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 completions = self.completions.read();
+ let completion = &completions[mat.candidate_id];
+ let documentation = &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(Documentation::SingleLine(text)) = documentation {
+ if show_completion_documentation {
+ len += text.chars().count();
+ }
}
len
@@ -948,16 +1277,24 @@ 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(
- 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;
+ let completions_guard = completions.read();
+
for (ix, mat) in matches[range].iter().enumerate() {
- let completion = &completions[mat.candidate_id];
let item_ix = start_ix + ix;
+ let candidate_id = mat.candidate_id;
+ let completion = &completions_guard[candidate_id];
+
+ let documentation = if show_completion_documentation {
+ &completion.documentation
+ } else {
+ &None
+ };
+
items.push(
MouseEventHandler::new::<CompletionTag, _>(
mat.candidate_id,
@@ -986,22 +1323,18 @@ impl CompletionsMenu {
),
);
- if let Some(server_name) = get_server_name(completion.server_id) {
+ if let Some(Documentation::SingleLine(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 +1354,7 @@ impl CompletionsMenu {
.with_style(
style
.autocomplete
- .server_name_container,
+ .inline_docs_container,
)
.into_any(),
)
@@ -1060,15 +1393,59 @@ 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(),
);
}
- },
- )
- .with_width_from_item(widest_completion_ix)
- .contained()
- .with_style(container_style)
- .into_any()
+ }
+ })
+ .with_width_from_item(widest_completion_ix);
+
+ enum MultiLineDocumentation {}
+
+ Flex::row()
+ .with_child(list.flex(1., false))
+ .with_children({
+ let mat = &self.matches[selected_item];
+ let completions = self.completions.read();
+ let completion = &completions[mat.candidate_id];
+ let documentation = &completion.documentation;
+
+ match documentation {
+ Some(Documentation::MultiLinePlainText(text)) => Some(
+ Flex::column()
+ .scrollable::<MultiLineDocumentation>(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(
+ Flex::column()
+ .scrollable::<MultiLineDocumentation>(0, None, cx)
+ .with_child(render_parsed_markdown::<MultiLineDocumentation>(
+ parsed, &style, workspace, cx,
+ ))
+ .contained()
+ .with_style(style.autocomplete.alongside_docs_container)
+ .constrained()
+ .with_max_width(style.autocomplete.alongside_docs_max_width)
+ .flex(1., false),
+ ),
+
+ _ => None,
+ }
+ })
+ .contained()
+ .with_style(style.autocomplete.container)
+ .into_any()
}
pub async fn filter(&mut self, query: Option<&str>, executor: Arc<executor::Background>) {
@@ -1095,13 +1472,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())
@@ -1111,23 +1488,27 @@ 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;
}
}
self.matches = matches.into();
+ self.selected_item = 0;
}
}
@@ -1563,7 +1944,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(),
@@ -1858,10 +2239,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
}
};
@@ -1873,13 +2256,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);
@@ -2912,6 +3321,7 @@ impl Editor {
false
});
}
+
fn completion_query(buffer: &MultiBufferSnapshot, position: impl ToOffset) -> Option<String> {
let offset = position.to_offset(buffer);
let (word_range, kind) = buffer.surrounding_word(offset);
@@ -3150,7 +3560,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,8 +3578,7 @@ impl Editor {
})
.collect(),
buffer,
- project,
- completions: completions.into(),
+ completions: Arc::new(RwLock::new(completions.into())),
matches: Vec::new().into(),
selected_item: 0,
list: Default::default(),
@@ -3179,6 +3587,9 @@ impl Editor {
if menu.matches.is_empty() {
None
} else {
+ _ = this.update(&mut cx, |editor, cx| {
+ menu.pre_resolve_completion_documentation(editor.project.clone(), cx);
+ });
Some(menu)
}
} else {
@@ -3188,23 +3599,30 @@ 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.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);
}
@@ -3235,7 +3653,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;
@@ -3348,14 +3767,13 @@ impl Editor {
}
pub fn toggle_code_actions(&mut self, action: &ToggleCodeActions, cx: &mut ViewContext<Self>) {
- 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();
@@ -3368,16 +3786,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,
- );
+ }));
}
}
})?;
@@ -3841,7 +4259,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
{
@@ -3975,6 +4393,7 @@ impl Editor {
pub fn context_menu_visible(&self) -> bool {
self.context_menu
+ .read()
.as_ref()
.map_or(false, |menu| menu.visible())
}
@@ -3985,24 +4404,20 @@ impl Editor {
style: EditorStyle,
cx: &mut ViewContext<Editor>,
) -> Option<(DisplayPoint, AnyElement<Editor>)> {
- self.context_menu
- .as_ref()
- .map(|menu| menu.render(cursor_position, style, cx))
- }
-
- fn show_context_menu(&mut self, menu: ContextMenu, cx: &mut ViewContext<Self>) {
- if !matches!(menu, ContextMenu::Completions(_)) {
- self.completion_tasks.clear();
- }
- self.context_menu = Some(menu);
- self.discard_copilot_suggestion(cx);
- cx.notify();
+ self.context_menu.read().as_ref().map(|menu| {
+ menu.render(
+ cursor_position,
+ style,
+ self.workspace.as_ref().map(|(w, _)| w.clone()),
+ cx,
+ )
+ })
}
fn hide_context_menu(&mut self, cx: &mut ViewContext<Self>) -> Option<ContextMenu> {
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);
}
@@ -5354,8 +5769,9 @@ impl Editor {
if self
.context_menu
+ .write()
.as_mut()
- .map(|menu| menu.select_last(cx))
+ .map(|menu| menu.select_last(self.project.as_ref(), cx))
.unwrap_or(false)
{
return;
@@ -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>,
@@ -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"]
@@ -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(
@@ -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 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"
);
@@ -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();
-
- let mut delta = match axis {
- Axis::Horizontal => {
- if scroll_delta.x().abs() >= scroll_delta.y().abs() {
- scroll_delta.x()
- } else {
- scroll_delta.y()
- }
+ 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()
}
- Axis::Vertical => scroll_delta.y(),
- };
- if !e.delta.precise() {
- delta *= 20.;
}
+ Axis::Vertical => scroll_delta.y(),
+ };
+ if !e.delta.precise() {
+ delta *= 20.;
+ }
- scroll_state
- .scroll_position
- .set(scroll_state.scroll_position.get() - delta);
+ scroll_state
+ .scroll_position
+ .set(scroll_state.scroll_position.get() - delta);
- cx.notify();
- } else {
- cx.propagate_event();
- }
+ cx.notify();
+ } else {
+ cx.propagate_event();
}
- })
- .on_move(|_, _: &mut V, _| { /* Capture move events */ }),
+ }
+ })
+ .on_move(|_, _: &mut V, _| { /* Capture move events */ }),
)
}
@@ -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
@@ -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,
}
@@ -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;
@@ -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) = ¤t_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(" ");
+ }
+}
@@ -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,
})
@@ -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>>>,
@@ -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,
}
@@ -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>,
@@ -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;
-
- UpdateContacts update_contacts = 87;
- UpdateInviteInfo update_invite_info = 88;
- ShowContacts show_contacts = 89;
-
- 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;
-
- 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;
-
- OnTypeFormatting on_type_formatting = 103;
- OnTypeFormattingResponse on_type_formatting_response = 104;
-
- UpdateWorktreeSettings update_worktree_settings = 105;
-
- 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;
-
- 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;
-
- 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;
-
- 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;
-
- LinkChannel link_channel = 140;
- UnlinkChannel unlink_channel = 141;
- MoveChannel move_channel = 142; // current max: 144
+ 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 = 89;
+ UpdateInviteInfo update_invite_info = 90;
+ ShowContacts show_contacts = 91;
+
+ 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 = 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 = 105;
+ OnTypeFormattingResponse on_type_formatting_response = 106;
+
+ UpdateWorktreeSettings update_worktree_settings = 107;
+
+ 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 = 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 = 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 = 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 = 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;
@@ -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,
@@ -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;
@@ -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();
@@ -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)]
@@ -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),