Add caching of parsed completion documentation markdown to reduce flicker when selecting (#31546)
Related to #31460 and #28635. Release Notes: - Fixed redraw delay of documentation from language server completions and added caching to reduce flicker when using arrow keys to change selection.
This commit is contained in:
parent
31d908fc74
commit
506beafe10
8 changed files with 300 additions and 95 deletions
1
Cargo.lock
generated
1
Cargo.lock
generated
|
@ -9567,6 +9567,7 @@ dependencies = [
|
||||||
"assets",
|
"assets",
|
||||||
"base64 0.22.1",
|
"base64 0.22.1",
|
||||||
"env_logger 0.11.8",
|
"env_logger 0.11.8",
|
||||||
|
"futures 0.3.31",
|
||||||
"gpui",
|
"gpui",
|
||||||
"language",
|
"language",
|
||||||
"languages",
|
"languages",
|
||||||
|
|
|
@ -4,8 +4,9 @@ use gpui::{
|
||||||
Size, StrikethroughStyle, StyledText, UniformListScrollHandle, div, px, uniform_list,
|
Size, StrikethroughStyle, StyledText, UniformListScrollHandle, div, px, uniform_list,
|
||||||
};
|
};
|
||||||
use gpui::{AsyncWindowContext, WeakEntity};
|
use gpui::{AsyncWindowContext, WeakEntity};
|
||||||
use language::Buffer;
|
use itertools::Itertools;
|
||||||
use language::CodeLabel;
|
use language::CodeLabel;
|
||||||
|
use language::{Buffer, LanguageName, LanguageRegistry};
|
||||||
use markdown::{Markdown, MarkdownElement};
|
use markdown::{Markdown, MarkdownElement};
|
||||||
use multi_buffer::{Anchor, ExcerptId};
|
use multi_buffer::{Anchor, ExcerptId};
|
||||||
use ordered_float::OrderedFloat;
|
use ordered_float::OrderedFloat;
|
||||||
|
@ -15,6 +16,8 @@ use project::{CodeAction, Completion, TaskSourceKind};
|
||||||
use task::DebugScenario;
|
use task::DebugScenario;
|
||||||
use task::TaskContext;
|
use task::TaskContext;
|
||||||
|
|
||||||
|
use std::collections::VecDeque;
|
||||||
|
use std::sync::Arc;
|
||||||
use std::{
|
use std::{
|
||||||
cell::RefCell,
|
cell::RefCell,
|
||||||
cmp::{Reverse, min},
|
cmp::{Reverse, min},
|
||||||
|
@ -41,6 +44,25 @@ pub const MENU_ASIDE_X_PADDING: Pixels = px(16.);
|
||||||
pub const MENU_ASIDE_MIN_WIDTH: Pixels = px(260.);
|
pub const MENU_ASIDE_MIN_WIDTH: Pixels = px(260.);
|
||||||
pub const MENU_ASIDE_MAX_WIDTH: Pixels = px(500.);
|
pub const MENU_ASIDE_MAX_WIDTH: Pixels = px(500.);
|
||||||
|
|
||||||
|
// Constants for the markdown cache. The purpose of this cache is to reduce flickering due to
|
||||||
|
// documentation not yet being parsed.
|
||||||
|
//
|
||||||
|
// The size of the cache is set to the number of items fetched around the current selection plus one
|
||||||
|
// for the current selection and another to avoid cases where and adjacent selection exits the
|
||||||
|
// cache. The only current benefit of a larger cache would be doing less markdown parsing when the
|
||||||
|
// selection revisits items.
|
||||||
|
//
|
||||||
|
// One future benefit of a larger cache would be reducing flicker on backspace. This would require
|
||||||
|
// not recreating the menu on every change, by not re-querying the language server when
|
||||||
|
// `is_incomplete = false`.
|
||||||
|
const MARKDOWN_CACHE_MAX_SIZE: usize = MARKDOWN_CACHE_BEFORE_ITEMS + MARKDOWN_CACHE_AFTER_ITEMS + 2;
|
||||||
|
const MARKDOWN_CACHE_BEFORE_ITEMS: usize = 2;
|
||||||
|
const MARKDOWN_CACHE_AFTER_ITEMS: usize = 2;
|
||||||
|
|
||||||
|
// Number of items beyond the visible items to resolve documentation.
|
||||||
|
const RESOLVE_BEFORE_ITEMS: usize = 4;
|
||||||
|
const RESOLVE_AFTER_ITEMS: usize = 4;
|
||||||
|
|
||||||
pub enum CodeContextMenu {
|
pub enum CodeContextMenu {
|
||||||
Completions(CompletionsMenu),
|
Completions(CompletionsMenu),
|
||||||
CodeActions(CodeActionsMenu),
|
CodeActions(CodeActionsMenu),
|
||||||
|
@ -148,13 +170,12 @@ impl CodeContextMenu {
|
||||||
|
|
||||||
pub fn render_aside(
|
pub fn render_aside(
|
||||||
&mut self,
|
&mut self,
|
||||||
editor: &Editor,
|
|
||||||
max_size: Size<Pixels>,
|
max_size: Size<Pixels>,
|
||||||
window: &mut Window,
|
window: &mut Window,
|
||||||
cx: &mut Context<Editor>,
|
cx: &mut Context<Editor>,
|
||||||
) -> Option<AnyElement> {
|
) -> Option<AnyElement> {
|
||||||
match self {
|
match self {
|
||||||
CodeContextMenu::Completions(menu) => menu.render_aside(editor, max_size, window, cx),
|
CodeContextMenu::Completions(menu) => menu.render_aside(max_size, window, cx),
|
||||||
CodeContextMenu::CodeActions(_) => None,
|
CodeContextMenu::CodeActions(_) => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -162,7 +183,7 @@ impl CodeContextMenu {
|
||||||
pub fn focused(&self, window: &mut Window, cx: &mut Context<Editor>) -> bool {
|
pub fn focused(&self, window: &mut Window, cx: &mut Context<Editor>) -> bool {
|
||||||
match self {
|
match self {
|
||||||
CodeContextMenu::Completions(completions_menu) => completions_menu
|
CodeContextMenu::Completions(completions_menu) => completions_menu
|
||||||
.markdown_element
|
.get_or_create_entry_markdown(completions_menu.selected_item, cx)
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.is_some_and(|markdown| markdown.focus_handle(cx).contains_focused(window, cx)),
|
.is_some_and(|markdown| markdown.focus_handle(cx).contains_focused(window, cx)),
|
||||||
CodeContextMenu::CodeActions(_) => false,
|
CodeContextMenu::CodeActions(_) => false,
|
||||||
|
@ -176,7 +197,7 @@ pub enum ContextMenuOrigin {
|
||||||
QuickActionBar,
|
QuickActionBar,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone)]
|
||||||
pub struct CompletionsMenu {
|
pub struct CompletionsMenu {
|
||||||
pub id: CompletionId,
|
pub id: CompletionId,
|
||||||
sort_completions: bool,
|
sort_completions: bool,
|
||||||
|
@ -191,7 +212,9 @@ pub struct CompletionsMenu {
|
||||||
show_completion_documentation: bool,
|
show_completion_documentation: bool,
|
||||||
pub(super) ignore_completion_provider: bool,
|
pub(super) ignore_completion_provider: bool,
|
||||||
last_rendered_range: Rc<RefCell<Option<Range<usize>>>>,
|
last_rendered_range: Rc<RefCell<Option<Range<usize>>>>,
|
||||||
markdown_element: Option<Entity<Markdown>>,
|
markdown_cache: Rc<RefCell<VecDeque<(usize, Entity<Markdown>)>>>,
|
||||||
|
language_registry: Option<Arc<LanguageRegistry>>,
|
||||||
|
language: Option<LanguageName>,
|
||||||
snippet_sort_order: SnippetSortOrder,
|
snippet_sort_order: SnippetSortOrder,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -205,6 +228,9 @@ impl CompletionsMenu {
|
||||||
buffer: Entity<Buffer>,
|
buffer: Entity<Buffer>,
|
||||||
completions: Box<[Completion]>,
|
completions: Box<[Completion]>,
|
||||||
snippet_sort_order: SnippetSortOrder,
|
snippet_sort_order: SnippetSortOrder,
|
||||||
|
language_registry: Option<Arc<LanguageRegistry>>,
|
||||||
|
language: Option<LanguageName>,
|
||||||
|
cx: &mut Context<Editor>,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
let match_candidates = completions
|
let match_candidates = completions
|
||||||
.iter()
|
.iter()
|
||||||
|
@ -212,7 +238,7 @@ impl CompletionsMenu {
|
||||||
.map(|(id, completion)| StringMatchCandidate::new(id, &completion.label.filter_text()))
|
.map(|(id, completion)| StringMatchCandidate::new(id, &completion.label.filter_text()))
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
Self {
|
let completions_menu = Self {
|
||||||
id,
|
id,
|
||||||
sort_completions,
|
sort_completions,
|
||||||
initial_position,
|
initial_position,
|
||||||
|
@ -226,9 +252,15 @@ impl CompletionsMenu {
|
||||||
scroll_handle: UniformListScrollHandle::new(),
|
scroll_handle: UniformListScrollHandle::new(),
|
||||||
resolve_completions: true,
|
resolve_completions: true,
|
||||||
last_rendered_range: RefCell::new(None).into(),
|
last_rendered_range: RefCell::new(None).into(),
|
||||||
markdown_element: None,
|
markdown_cache: RefCell::new(VecDeque::with_capacity(MARKDOWN_CACHE_MAX_SIZE)).into(),
|
||||||
|
language_registry,
|
||||||
|
language,
|
||||||
snippet_sort_order,
|
snippet_sort_order,
|
||||||
}
|
};
|
||||||
|
|
||||||
|
completions_menu.start_markdown_parse_for_nearby_entries(cx);
|
||||||
|
|
||||||
|
completions_menu
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn new_snippet_choices(
|
pub fn new_snippet_choices(
|
||||||
|
@ -286,7 +318,9 @@ impl CompletionsMenu {
|
||||||
show_completion_documentation: false,
|
show_completion_documentation: false,
|
||||||
ignore_completion_provider: false,
|
ignore_completion_provider: false,
|
||||||
last_rendered_range: RefCell::new(None).into(),
|
last_rendered_range: RefCell::new(None).into(),
|
||||||
markdown_element: None,
|
markdown_cache: RefCell::new(VecDeque::new()).into(),
|
||||||
|
language_registry: None,
|
||||||
|
language: None,
|
||||||
snippet_sort_order,
|
snippet_sort_order,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -359,6 +393,7 @@ impl CompletionsMenu {
|
||||||
self.scroll_handle
|
self.scroll_handle
|
||||||
.scroll_to_item(self.selected_item, ScrollStrategy::Top);
|
.scroll_to_item(self.selected_item, ScrollStrategy::Top);
|
||||||
self.resolve_visible_completions(provider, cx);
|
self.resolve_visible_completions(provider, cx);
|
||||||
|
self.start_markdown_parse_for_nearby_entries(cx);
|
||||||
if let Some(provider) = provider {
|
if let Some(provider) = provider {
|
||||||
self.handle_selection_changed(provider, window, cx);
|
self.handle_selection_changed(provider, window, cx);
|
||||||
}
|
}
|
||||||
|
@ -433,11 +468,10 @@ impl CompletionsMenu {
|
||||||
|
|
||||||
// Expand the range to resolve more completions than are predicted to be visible, to reduce
|
// Expand the range to resolve more completions than are predicted to be visible, to reduce
|
||||||
// jank on navigation.
|
// jank on navigation.
|
||||||
const EXTRA_TO_RESOLVE: usize = 4;
|
let entry_indices = util::expanded_and_wrapped_usize_range(
|
||||||
let entry_indices = util::iterate_expanded_and_wrapped_usize_range(
|
|
||||||
entry_range.clone(),
|
entry_range.clone(),
|
||||||
EXTRA_TO_RESOLVE,
|
RESOLVE_BEFORE_ITEMS,
|
||||||
EXTRA_TO_RESOLVE,
|
RESOLVE_AFTER_ITEMS,
|
||||||
entries.len(),
|
entries.len(),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -467,14 +501,120 @@ impl CompletionsMenu {
|
||||||
cx,
|
cx,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
let completion_id = self.id;
|
||||||
cx.spawn(async move |editor, cx| {
|
cx.spawn(async move |editor, cx| {
|
||||||
if let Some(true) = resolve_task.await.log_err() {
|
if let Some(true) = resolve_task.await.log_err() {
|
||||||
editor.update(cx, |_, cx| cx.notify()).ok();
|
editor
|
||||||
|
.update(cx, |editor, cx| {
|
||||||
|
// `resolve_completions` modified state affecting display.
|
||||||
|
cx.notify();
|
||||||
|
editor.with_completions_menu_matching_id(
|
||||||
|
completion_id,
|
||||||
|
|| (),
|
||||||
|
|this| this.start_markdown_parse_for_nearby_entries(cx),
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.ok();
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.detach();
|
.detach();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn start_markdown_parse_for_nearby_entries(&self, cx: &mut Context<Editor>) {
|
||||||
|
// Enqueue parse tasks of nearer items first.
|
||||||
|
//
|
||||||
|
// TODO: This means that the nearer items will actually be further back in the cache, which
|
||||||
|
// is not ideal. In practice this is fine because `get_or_create_markdown` moves the current
|
||||||
|
// selection to the front (when `is_render = true`).
|
||||||
|
let entry_indices = util::wrapped_usize_outward_from(
|
||||||
|
self.selected_item,
|
||||||
|
MARKDOWN_CACHE_BEFORE_ITEMS,
|
||||||
|
MARKDOWN_CACHE_AFTER_ITEMS,
|
||||||
|
self.entries.borrow().len(),
|
||||||
|
);
|
||||||
|
|
||||||
|
for index in entry_indices {
|
||||||
|
self.get_or_create_entry_markdown(index, cx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_or_create_entry_markdown(
|
||||||
|
&self,
|
||||||
|
index: usize,
|
||||||
|
cx: &mut Context<Editor>,
|
||||||
|
) -> Option<Entity<Markdown>> {
|
||||||
|
let entries = self.entries.borrow();
|
||||||
|
if index >= entries.len() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let candidate_id = entries[index].candidate_id;
|
||||||
|
match &self.completions.borrow()[candidate_id].documentation {
|
||||||
|
Some(CompletionDocumentation::MultiLineMarkdown(source)) if !source.is_empty() => Some(
|
||||||
|
self.get_or_create_markdown(candidate_id, source.clone(), false, cx)
|
||||||
|
.1,
|
||||||
|
),
|
||||||
|
Some(_) => None,
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_or_create_markdown(
|
||||||
|
&self,
|
||||||
|
candidate_id: usize,
|
||||||
|
source: SharedString,
|
||||||
|
is_render: bool,
|
||||||
|
cx: &mut Context<Editor>,
|
||||||
|
) -> (bool, Entity<Markdown>) {
|
||||||
|
let mut markdown_cache = self.markdown_cache.borrow_mut();
|
||||||
|
if let Some((cache_index, (_, markdown))) = markdown_cache
|
||||||
|
.iter()
|
||||||
|
.find_position(|(id, _)| *id == candidate_id)
|
||||||
|
{
|
||||||
|
let markdown = if is_render && cache_index != 0 {
|
||||||
|
// Move the current selection's cache entry to the front.
|
||||||
|
markdown_cache.rotate_right(1);
|
||||||
|
let cache_len = markdown_cache.len();
|
||||||
|
markdown_cache.swap(0, (cache_index + 1) % cache_len);
|
||||||
|
&markdown_cache[0].1
|
||||||
|
} else {
|
||||||
|
markdown
|
||||||
|
};
|
||||||
|
|
||||||
|
let is_parsing = markdown.update(cx, |markdown, cx| {
|
||||||
|
// `reset` is called as it's possible for documentation to change due to resolve
|
||||||
|
// requests. It does nothing if `source` is unchanged.
|
||||||
|
markdown.reset(source, cx);
|
||||||
|
markdown.is_parsing()
|
||||||
|
});
|
||||||
|
return (is_parsing, markdown.clone());
|
||||||
|
}
|
||||||
|
|
||||||
|
if markdown_cache.len() < MARKDOWN_CACHE_MAX_SIZE {
|
||||||
|
let markdown = cx.new(|cx| {
|
||||||
|
Markdown::new(
|
||||||
|
source,
|
||||||
|
self.language_registry.clone(),
|
||||||
|
self.language.clone(),
|
||||||
|
cx,
|
||||||
|
)
|
||||||
|
});
|
||||||
|
// Handles redraw when the markdown is done parsing. The current render is for a
|
||||||
|
// deferred draw, and so without this did not redraw when `markdown` notified.
|
||||||
|
cx.observe(&markdown, |_, _, cx| cx.notify()).detach();
|
||||||
|
markdown_cache.push_front((candidate_id, markdown.clone()));
|
||||||
|
(true, markdown)
|
||||||
|
} else {
|
||||||
|
debug_assert_eq!(markdown_cache.capacity(), MARKDOWN_CACHE_MAX_SIZE);
|
||||||
|
// Moves the last cache entry to the start. The ring buffer is full, so this does no
|
||||||
|
// copying and just shifts indexes.
|
||||||
|
markdown_cache.rotate_right(1);
|
||||||
|
markdown_cache[0].0 = candidate_id;
|
||||||
|
let markdown = &markdown_cache[0].1;
|
||||||
|
markdown.update(cx, |markdown, cx| markdown.reset(source, cx));
|
||||||
|
(true, markdown.clone())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn visible(&self) -> bool {
|
pub fn visible(&self) -> bool {
|
||||||
!self.entries.borrow().is_empty()
|
!self.entries.borrow().is_empty()
|
||||||
}
|
}
|
||||||
|
@ -625,7 +765,6 @@ impl CompletionsMenu {
|
||||||
|
|
||||||
fn render_aside(
|
fn render_aside(
|
||||||
&mut self,
|
&mut self,
|
||||||
editor: &Editor,
|
|
||||||
max_size: Size<Pixels>,
|
max_size: Size<Pixels>,
|
||||||
window: &mut Window,
|
window: &mut Window,
|
||||||
cx: &mut Context<Editor>,
|
cx: &mut Context<Editor>,
|
||||||
|
@ -644,33 +783,14 @@ impl CompletionsMenu {
|
||||||
plain_text: Some(text),
|
plain_text: Some(text),
|
||||||
..
|
..
|
||||||
} => div().child(text.clone()),
|
} => div().child(text.clone()),
|
||||||
CompletionDocumentation::MultiLineMarkdown(parsed) if !parsed.is_empty() => {
|
CompletionDocumentation::MultiLineMarkdown(source) if !source.is_empty() => {
|
||||||
let markdown = self.markdown_element.get_or_insert_with(|| {
|
let (is_parsing, markdown) =
|
||||||
let markdown = cx.new(|cx| {
|
self.get_or_create_markdown(mat.candidate_id, source.clone(), true, cx);
|
||||||
let languages = editor
|
|
||||||
.workspace
|
|
||||||
.as_ref()
|
|
||||||
.and_then(|(workspace, _)| workspace.upgrade())
|
|
||||||
.map(|workspace| workspace.read(cx).app_state().languages.clone());
|
|
||||||
let language = editor
|
|
||||||
.language_at(self.initial_position, cx)
|
|
||||||
.map(|l| l.name().to_proto());
|
|
||||||
Markdown::new(SharedString::default(), languages, language, cx)
|
|
||||||
});
|
|
||||||
// Handles redraw when the markdown is done parsing. The current render is for a
|
|
||||||
// deferred draw and so was not getting redrawn when `markdown` notified.
|
|
||||||
cx.observe(&markdown, |_, _, cx| cx.notify()).detach();
|
|
||||||
markdown
|
|
||||||
});
|
|
||||||
let is_parsing = markdown.update(cx, |markdown, cx| {
|
|
||||||
markdown.reset(parsed.clone(), cx);
|
|
||||||
markdown.is_parsing()
|
|
||||||
});
|
|
||||||
if is_parsing {
|
if is_parsing {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
div().child(
|
div().child(
|
||||||
MarkdownElement::new(markdown.clone(), hover_markdown_style(window, cx))
|
MarkdownElement::new(markdown, hover_markdown_style(window, cx))
|
||||||
.code_block_renderer(markdown::CodeBlockRenderer::Default {
|
.code_block_renderer(markdown::CodeBlockRenderer::Default {
|
||||||
copy_button: false,
|
copy_button: false,
|
||||||
copy_button_on_hover: false,
|
copy_button_on_hover: false,
|
||||||
|
@ -882,13 +1002,7 @@ impl CompletionsMenu {
|
||||||
// another opened. `provider.selection_changed` should not be called in this case.
|
// another opened. `provider.selection_changed` should not be called in this case.
|
||||||
let this_menu_still_active = editor
|
let this_menu_still_active = editor
|
||||||
.read_with(cx, |editor, _cx| {
|
.read_with(cx, |editor, _cx| {
|
||||||
if let Some(CodeContextMenu::Completions(completions_menu)) =
|
editor.with_completions_menu_matching_id(self.id, || false, |_| true)
|
||||||
editor.context_menu.borrow().as_ref()
|
|
||||||
{
|
|
||||||
completions_menu.id == self.id
|
|
||||||
} else {
|
|
||||||
false
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
.unwrap_or(false);
|
.unwrap_or(false);
|
||||||
if this_menu_still_active {
|
if this_menu_still_active {
|
||||||
|
|
|
@ -4987,14 +4987,12 @@ impl Editor {
|
||||||
(buffer_position..buffer_position, None)
|
(buffer_position..buffer_position, None)
|
||||||
};
|
};
|
||||||
|
|
||||||
let completion_settings = language_settings(
|
let language = buffer_snapshot
|
||||||
buffer_snapshot
|
.language_at(buffer_position)
|
||||||
.language_at(buffer_position)
|
.map(|language| language.name());
|
||||||
.map(|language| language.name()),
|
|
||||||
buffer_snapshot.file(),
|
let completion_settings =
|
||||||
cx,
|
language_settings(language.clone(), buffer_snapshot.file(), cx).completions;
|
||||||
)
|
|
||||||
.completions;
|
|
||||||
|
|
||||||
// The document can be large, so stay in reasonable bounds when searching for words,
|
// The document can be large, so stay in reasonable bounds when searching for words,
|
||||||
// otherwise completion pop-up might be slow to appear.
|
// otherwise completion pop-up might be slow to appear.
|
||||||
|
@ -5106,16 +5104,26 @@ impl Editor {
|
||||||
let menu = if completions.is_empty() {
|
let menu = if completions.is_empty() {
|
||||||
None
|
None
|
||||||
} else {
|
} else {
|
||||||
let mut menu = CompletionsMenu::new(
|
let mut menu = editor.update(cx, |editor, cx| {
|
||||||
id,
|
let languages = editor
|
||||||
sort_completions,
|
.workspace
|
||||||
show_completion_documentation,
|
.as_ref()
|
||||||
ignore_completion_provider,
|
.and_then(|(workspace, _)| workspace.upgrade())
|
||||||
position,
|
.map(|workspace| workspace.read(cx).app_state().languages.clone());
|
||||||
buffer.clone(),
|
CompletionsMenu::new(
|
||||||
completions.into(),
|
id,
|
||||||
snippet_sort_order,
|
sort_completions,
|
||||||
);
|
show_completion_documentation,
|
||||||
|
ignore_completion_provider,
|
||||||
|
position,
|
||||||
|
buffer.clone(),
|
||||||
|
completions.into(),
|
||||||
|
snippet_sort_order,
|
||||||
|
languages,
|
||||||
|
language,
|
||||||
|
cx,
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
|
||||||
menu.filter(
|
menu.filter(
|
||||||
if filter_completions {
|
if filter_completions {
|
||||||
|
@ -5190,6 +5198,22 @@ impl Editor {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn with_completions_menu_matching_id<R>(
|
||||||
|
&self,
|
||||||
|
id: CompletionId,
|
||||||
|
on_absent: impl FnOnce() -> R,
|
||||||
|
on_match: impl FnOnce(&mut CompletionsMenu) -> R,
|
||||||
|
) -> R {
|
||||||
|
let mut context_menu = self.context_menu.borrow_mut();
|
||||||
|
let Some(CodeContextMenu::Completions(completions_menu)) = &mut *context_menu else {
|
||||||
|
return on_absent();
|
||||||
|
};
|
||||||
|
if completions_menu.id != id {
|
||||||
|
return on_absent();
|
||||||
|
}
|
||||||
|
on_match(completions_menu)
|
||||||
|
}
|
||||||
|
|
||||||
pub fn confirm_completion(
|
pub fn confirm_completion(
|
||||||
&mut self,
|
&mut self,
|
||||||
action: &ConfirmCompletion,
|
action: &ConfirmCompletion,
|
||||||
|
@ -8686,7 +8710,7 @@ impl Editor {
|
||||||
) -> Option<AnyElement> {
|
) -> Option<AnyElement> {
|
||||||
self.context_menu.borrow_mut().as_mut().and_then(|menu| {
|
self.context_menu.borrow_mut().as_mut().and_then(|menu| {
|
||||||
if menu.visible() {
|
if menu.visible() {
|
||||||
menu.render_aside(self, max_size, window, cx)
|
menu.render_aside(max_size, window, cx)
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
|
@ -583,13 +583,6 @@ async fn parse_blocks(
|
||||||
language: Option<Arc<Language>>,
|
language: Option<Arc<Language>>,
|
||||||
cx: &mut AsyncWindowContext,
|
cx: &mut AsyncWindowContext,
|
||||||
) -> Option<Entity<Markdown>> {
|
) -> Option<Entity<Markdown>> {
|
||||||
let fallback_language_name = if let Some(ref l) = language {
|
|
||||||
let l = Arc::clone(l);
|
|
||||||
Some(l.lsp_id().clone())
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
};
|
|
||||||
|
|
||||||
let combined_text = blocks
|
let combined_text = blocks
|
||||||
.iter()
|
.iter()
|
||||||
.map(|block| match &block.kind {
|
.map(|block| match &block.kind {
|
||||||
|
@ -607,7 +600,7 @@ async fn parse_blocks(
|
||||||
Markdown::new(
|
Markdown::new(
|
||||||
combined_text.into(),
|
combined_text.into(),
|
||||||
Some(language_registry.clone()),
|
Some(language_registry.clone()),
|
||||||
fallback_language_name,
|
language.map(|language| language.name()),
|
||||||
cx,
|
cx,
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
|
@ -20,6 +20,7 @@ test-support = [
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
base64.workspace = true
|
base64.workspace = true
|
||||||
|
futures.workspace = true
|
||||||
gpui.workspace = true
|
gpui.workspace = true
|
||||||
language.workspace = true
|
language.workspace = true
|
||||||
linkify.workspace = true
|
linkify.workspace = true
|
||||||
|
|
|
@ -67,14 +67,8 @@ struct MarkdownExample {
|
||||||
|
|
||||||
impl MarkdownExample {
|
impl MarkdownExample {
|
||||||
pub fn new(text: SharedString, language_registry: Arc<LanguageRegistry>, cx: &mut App) -> Self {
|
pub fn new(text: SharedString, language_registry: Arc<LanguageRegistry>, cx: &mut App) -> Self {
|
||||||
let markdown = cx.new(|cx| {
|
let markdown = cx
|
||||||
Markdown::new(
|
.new(|cx| Markdown::new(text, Some(language_registry), Some("TypeScript".into()), cx));
|
||||||
text,
|
|
||||||
Some(language_registry),
|
|
||||||
Some("TypeScript".to_string()),
|
|
||||||
cx,
|
|
||||||
)
|
|
||||||
});
|
|
||||||
Self { markdown }
|
Self { markdown }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,6 +2,8 @@ pub mod parser;
|
||||||
mod path_range;
|
mod path_range;
|
||||||
|
|
||||||
use base64::Engine as _;
|
use base64::Engine as _;
|
||||||
|
use futures::FutureExt as _;
|
||||||
|
use language::LanguageName;
|
||||||
use log::Level;
|
use log::Level;
|
||||||
pub use path_range::{LineCol, PathWithRange};
|
pub use path_range::{LineCol, PathWithRange};
|
||||||
|
|
||||||
|
@ -101,7 +103,7 @@ pub struct Markdown {
|
||||||
pending_parse: Option<Task<()>>,
|
pending_parse: Option<Task<()>>,
|
||||||
focus_handle: FocusHandle,
|
focus_handle: FocusHandle,
|
||||||
language_registry: Option<Arc<LanguageRegistry>>,
|
language_registry: Option<Arc<LanguageRegistry>>,
|
||||||
fallback_code_block_language: Option<String>,
|
fallback_code_block_language: Option<LanguageName>,
|
||||||
options: Options,
|
options: Options,
|
||||||
copied_code_blocks: HashSet<ElementId>,
|
copied_code_blocks: HashSet<ElementId>,
|
||||||
}
|
}
|
||||||
|
@ -144,7 +146,7 @@ impl Markdown {
|
||||||
pub fn new(
|
pub fn new(
|
||||||
source: SharedString,
|
source: SharedString,
|
||||||
language_registry: Option<Arc<LanguageRegistry>>,
|
language_registry: Option<Arc<LanguageRegistry>>,
|
||||||
fallback_code_block_language: Option<String>,
|
fallback_code_block_language: Option<LanguageName>,
|
||||||
cx: &mut Context<Self>,
|
cx: &mut Context<Self>,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
let focus_handle = cx.focus_handle();
|
let focus_handle = cx.focus_handle();
|
||||||
|
@ -310,9 +312,9 @@ impl Markdown {
|
||||||
if let Some(registry) = language_registry.as_ref() {
|
if let Some(registry) = language_registry.as_ref() {
|
||||||
for name in language_names {
|
for name in language_names {
|
||||||
let language = if !name.is_empty() {
|
let language = if !name.is_empty() {
|
||||||
registry.language_for_name_or_extension(&name)
|
registry.language_for_name_or_extension(&name).left_future()
|
||||||
} else if let Some(fallback) = &fallback {
|
} else if let Some(fallback) = &fallback {
|
||||||
registry.language_for_name_or_extension(fallback)
|
registry.language_for_name(fallback.as_ref()).right_future()
|
||||||
} else {
|
} else {
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
|
|
|
@ -670,7 +670,7 @@ pub fn measure<R>(label: &str, f: impl FnOnce() -> R) -> R {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn iterate_expanded_and_wrapped_usize_range(
|
pub fn expanded_and_wrapped_usize_range(
|
||||||
range: Range<usize>,
|
range: Range<usize>,
|
||||||
additional_before: usize,
|
additional_before: usize,
|
||||||
additional_after: usize,
|
additional_after: usize,
|
||||||
|
@ -699,6 +699,43 @@ pub fn iterate_expanded_and_wrapped_usize_range(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Yields `[i, i + 1, i - 1, i + 2, ..]`, each modulo `wrap_length` and bounded by
|
||||||
|
/// `additional_before` and `additional_after`. If the wrapping causes overlap, duplicates are not
|
||||||
|
/// emitted. If wrap_length is 0, nothing is yielded.
|
||||||
|
pub fn wrapped_usize_outward_from(
|
||||||
|
start: usize,
|
||||||
|
additional_before: usize,
|
||||||
|
additional_after: usize,
|
||||||
|
wrap_length: usize,
|
||||||
|
) -> impl Iterator<Item = usize> {
|
||||||
|
let mut count = 0;
|
||||||
|
let mut after_offset = 1;
|
||||||
|
let mut before_offset = 1;
|
||||||
|
|
||||||
|
std::iter::from_fn(move || {
|
||||||
|
count += 1;
|
||||||
|
if count > wrap_length {
|
||||||
|
None
|
||||||
|
} else if count == 1 {
|
||||||
|
Some(start % wrap_length)
|
||||||
|
} else if after_offset <= additional_after && after_offset <= before_offset {
|
||||||
|
let value = (start + after_offset) % wrap_length;
|
||||||
|
after_offset += 1;
|
||||||
|
Some(value)
|
||||||
|
} else if before_offset <= additional_before {
|
||||||
|
let value = (start + wrap_length - before_offset) % wrap_length;
|
||||||
|
before_offset += 1;
|
||||||
|
Some(value)
|
||||||
|
} else if after_offset <= additional_after {
|
||||||
|
let value = (start + after_offset) % wrap_length;
|
||||||
|
after_offset += 1;
|
||||||
|
Some(value)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(target_os = "windows")]
|
#[cfg(target_os = "windows")]
|
||||||
pub fn get_windows_system_shell() -> String {
|
pub fn get_windows_system_shell() -> String {
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
@ -1462,49 +1499,88 @@ Line 3"#
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_iterate_expanded_and_wrapped_usize_range() {
|
fn test_expanded_and_wrapped_usize_range() {
|
||||||
// Neither wrap
|
// Neither wrap
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
iterate_expanded_and_wrapped_usize_range(2..4, 1, 1, 8).collect::<Vec<usize>>(),
|
expanded_and_wrapped_usize_range(2..4, 1, 1, 8).collect::<Vec<usize>>(),
|
||||||
(1..5).collect::<Vec<usize>>()
|
(1..5).collect::<Vec<usize>>()
|
||||||
);
|
);
|
||||||
// Start wraps
|
// Start wraps
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
iterate_expanded_and_wrapped_usize_range(2..4, 3, 1, 8).collect::<Vec<usize>>(),
|
expanded_and_wrapped_usize_range(2..4, 3, 1, 8).collect::<Vec<usize>>(),
|
||||||
((0..5).chain(7..8)).collect::<Vec<usize>>()
|
((0..5).chain(7..8)).collect::<Vec<usize>>()
|
||||||
);
|
);
|
||||||
// Start wraps all the way around
|
// Start wraps all the way around
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
iterate_expanded_and_wrapped_usize_range(2..4, 5, 1, 8).collect::<Vec<usize>>(),
|
expanded_and_wrapped_usize_range(2..4, 5, 1, 8).collect::<Vec<usize>>(),
|
||||||
(0..8).collect::<Vec<usize>>()
|
(0..8).collect::<Vec<usize>>()
|
||||||
);
|
);
|
||||||
// Start wraps all the way around and past 0
|
// Start wraps all the way around and past 0
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
iterate_expanded_and_wrapped_usize_range(2..4, 10, 1, 8).collect::<Vec<usize>>(),
|
expanded_and_wrapped_usize_range(2..4, 10, 1, 8).collect::<Vec<usize>>(),
|
||||||
(0..8).collect::<Vec<usize>>()
|
(0..8).collect::<Vec<usize>>()
|
||||||
);
|
);
|
||||||
// End wraps
|
// End wraps
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
iterate_expanded_and_wrapped_usize_range(3..5, 1, 4, 8).collect::<Vec<usize>>(),
|
expanded_and_wrapped_usize_range(3..5, 1, 4, 8).collect::<Vec<usize>>(),
|
||||||
(0..1).chain(2..8).collect::<Vec<usize>>()
|
(0..1).chain(2..8).collect::<Vec<usize>>()
|
||||||
);
|
);
|
||||||
// End wraps all the way around
|
// End wraps all the way around
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
iterate_expanded_and_wrapped_usize_range(3..5, 1, 5, 8).collect::<Vec<usize>>(),
|
expanded_and_wrapped_usize_range(3..5, 1, 5, 8).collect::<Vec<usize>>(),
|
||||||
(0..8).collect::<Vec<usize>>()
|
(0..8).collect::<Vec<usize>>()
|
||||||
);
|
);
|
||||||
// End wraps all the way around and past the end
|
// End wraps all the way around and past the end
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
iterate_expanded_and_wrapped_usize_range(3..5, 1, 10, 8).collect::<Vec<usize>>(),
|
expanded_and_wrapped_usize_range(3..5, 1, 10, 8).collect::<Vec<usize>>(),
|
||||||
(0..8).collect::<Vec<usize>>()
|
(0..8).collect::<Vec<usize>>()
|
||||||
);
|
);
|
||||||
// Both start and end wrap
|
// Both start and end wrap
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
iterate_expanded_and_wrapped_usize_range(3..5, 4, 4, 8).collect::<Vec<usize>>(),
|
expanded_and_wrapped_usize_range(3..5, 4, 4, 8).collect::<Vec<usize>>(),
|
||||||
(0..8).collect::<Vec<usize>>()
|
(0..8).collect::<Vec<usize>>()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_wrapped_usize_outward_from() {
|
||||||
|
// No wrapping
|
||||||
|
assert_eq!(
|
||||||
|
wrapped_usize_outward_from(4, 2, 2, 10).collect::<Vec<usize>>(),
|
||||||
|
vec![4, 5, 3, 6, 2]
|
||||||
|
);
|
||||||
|
// Wrapping at end
|
||||||
|
assert_eq!(
|
||||||
|
wrapped_usize_outward_from(8, 2, 3, 10).collect::<Vec<usize>>(),
|
||||||
|
vec![8, 9, 7, 0, 6, 1]
|
||||||
|
);
|
||||||
|
// Wrapping at start
|
||||||
|
assert_eq!(
|
||||||
|
wrapped_usize_outward_from(1, 3, 2, 10).collect::<Vec<usize>>(),
|
||||||
|
vec![1, 2, 0, 3, 9, 8]
|
||||||
|
);
|
||||||
|
// All values wrap around
|
||||||
|
assert_eq!(
|
||||||
|
wrapped_usize_outward_from(5, 10, 10, 8).collect::<Vec<usize>>(),
|
||||||
|
vec![5, 6, 4, 7, 3, 0, 2, 1]
|
||||||
|
);
|
||||||
|
// None before / after
|
||||||
|
assert_eq!(
|
||||||
|
wrapped_usize_outward_from(3, 0, 0, 8).collect::<Vec<usize>>(),
|
||||||
|
vec![3]
|
||||||
|
);
|
||||||
|
// Starting point already wrapped
|
||||||
|
assert_eq!(
|
||||||
|
wrapped_usize_outward_from(15, 2, 2, 10).collect::<Vec<usize>>(),
|
||||||
|
vec![5, 6, 4, 7, 3]
|
||||||
|
);
|
||||||
|
// wrap_length of 0
|
||||||
|
assert_eq!(
|
||||||
|
wrapped_usize_outward_from(4, 2, 2, 0).collect::<Vec<usize>>(),
|
||||||
|
Vec::<usize>::new()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_truncate_lines_to_byte_limit() {
|
fn test_truncate_lines_to_byte_limit() {
|
||||||
let text = "Line 1\nLine 2\nLine 3\nLine 4";
|
let text = "Line 1\nLine 2\nLine 3\nLine 4";
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue