Compare commits
14 commits
main
...
editor_tea
Author | SHA1 | Date | |
---|---|---|---|
![]() |
575e193ea3 | ||
![]() |
a9e3d8c9a4 | ||
![]() |
b11d1e2db8 | ||
![]() |
e6448d7f71 | ||
![]() |
8996ef9d20 | ||
![]() |
c0f6075302 | ||
![]() |
f93b23cfc4 | ||
![]() |
9e60c836e0 | ||
![]() |
1a5897e264 | ||
![]() |
1724d5dde2 | ||
![]() |
ebe8e7d4a0 | ||
![]() |
722a508c2e | ||
![]() |
840309a6b7 | ||
![]() |
c961a1cd2b |
7 changed files with 1555 additions and 1189 deletions
|
@ -5796,7 +5796,7 @@ async fn test_collaborating_with_renames(
|
||||||
prepare_rename.await.unwrap();
|
prepare_rename.await.unwrap();
|
||||||
editor_b.update(cx_b, |editor, cx| {
|
editor_b.update(cx_b, |editor, cx| {
|
||||||
use editor::ToOffset;
|
use editor::ToOffset;
|
||||||
let rename = editor.pending_rename().unwrap();
|
let rename = editor.pending_rename(cx).unwrap();
|
||||||
let buffer = editor.buffer().read(cx).snapshot(cx);
|
let buffer = editor.buffer().read(cx).snapshot(cx);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
rename.range.start.to_offset(&buffer)..rename.range.end.to_offset(&buffer),
|
rename.range.start.to_offset(&buffer)..rename.range.end.to_offset(&buffer),
|
||||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -2,8 +2,9 @@ use super::*;
|
||||||
use crate::{
|
use crate::{
|
||||||
scroll::scroll_amount::ScrollAmount,
|
scroll::scroll_amount::ScrollAmount,
|
||||||
test::{
|
test::{
|
||||||
assert_text_with_selections, build_editor, editor_lsp_test_context::EditorLspTestContext,
|
assert_text_with_selections, build_editor, build_editor_with_project,
|
||||||
editor_test_context::EditorTestContext, select_ranges,
|
editor_lsp_test_context::EditorLspTestContext, editor_test_context::EditorTestContext,
|
||||||
|
select_ranges,
|
||||||
},
|
},
|
||||||
JoinLines,
|
JoinLines,
|
||||||
};
|
};
|
||||||
|
@ -4895,7 +4896,9 @@ async fn test_document_format_during_save(cx: &mut gpui::TestAppContext) {
|
||||||
let fake_server = fake_servers.next().await.unwrap();
|
let fake_server = fake_servers.next().await.unwrap();
|
||||||
|
|
||||||
let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx));
|
let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx));
|
||||||
let editor = cx.add_window(|cx| build_editor(buffer, cx)).root(cx);
|
let editor = cx
|
||||||
|
.add_window(|cx| build_editor_with_project(project.clone(), buffer, cx))
|
||||||
|
.root(cx);
|
||||||
editor.update(cx, |editor, cx| editor.set_text("one\ntwo\nthree\n", cx));
|
editor.update(cx, |editor, cx| editor.set_text("one\ntwo\nthree\n", cx));
|
||||||
assert!(cx.read(|cx| editor.is_dirty(cx)));
|
assert!(cx.read(|cx| editor.is_dirty(cx)));
|
||||||
|
|
||||||
|
@ -5007,7 +5010,9 @@ async fn test_range_format_during_save(cx: &mut gpui::TestAppContext) {
|
||||||
let fake_server = fake_servers.next().await.unwrap();
|
let fake_server = fake_servers.next().await.unwrap();
|
||||||
|
|
||||||
let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx));
|
let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx));
|
||||||
let editor = cx.add_window(|cx| build_editor(buffer, cx)).root(cx);
|
let editor = cx
|
||||||
|
.add_window(|cx| build_editor_with_project(project.clone(), buffer, cx))
|
||||||
|
.root(cx);
|
||||||
editor.update(cx, |editor, cx| editor.set_text("one\ntwo\nthree\n", cx));
|
editor.update(cx, |editor, cx| editor.set_text("one\ntwo\nthree\n", cx));
|
||||||
assert!(cx.read(|cx| editor.is_dirty(cx)));
|
assert!(cx.read(|cx| editor.is_dirty(cx)));
|
||||||
|
|
||||||
|
@ -5128,11 +5133,13 @@ async fn test_document_format_manual_trigger(cx: &mut gpui::TestAppContext) {
|
||||||
let fake_server = fake_servers.next().await.unwrap();
|
let fake_server = fake_servers.next().await.unwrap();
|
||||||
|
|
||||||
let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx));
|
let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx));
|
||||||
let editor = cx.add_window(|cx| build_editor(buffer, cx)).root(cx);
|
let editor = cx
|
||||||
|
.add_window(|cx| build_editor_with_project(project, buffer, cx))
|
||||||
|
.root(cx);
|
||||||
editor.update(cx, |editor, cx| editor.set_text("one\ntwo\nthree\n", cx));
|
editor.update(cx, |editor, cx| editor.set_text("one\ntwo\nthree\n", cx));
|
||||||
|
|
||||||
let format = editor.update(cx, |editor, cx| {
|
let format = editor.update(cx, |editor, cx| {
|
||||||
editor.perform_format(project.clone(), FormatTrigger::Manual, cx)
|
editor.perform_format(FormatTrigger::Manual, cx)
|
||||||
});
|
});
|
||||||
fake_server
|
fake_server
|
||||||
.handle_request::<lsp::request::Formatting, _, _>(move |params, _| async move {
|
.handle_request::<lsp::request::Formatting, _, _>(move |params, _| async move {
|
||||||
|
@ -5149,7 +5156,7 @@ async fn test_document_format_manual_trigger(cx: &mut gpui::TestAppContext) {
|
||||||
.next()
|
.next()
|
||||||
.await;
|
.await;
|
||||||
cx.foreground().start_waiting();
|
cx.foreground().start_waiting();
|
||||||
format.await.unwrap();
|
format.unwrap().await.unwrap();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
editor.read_with(cx, |editor, cx| editor.text(cx)),
|
editor.read_with(cx, |editor, cx| editor.text(cx)),
|
||||||
"one, two\nthree\n"
|
"one, two\nthree\n"
|
||||||
|
@ -5166,11 +5173,11 @@ async fn test_document_format_manual_trigger(cx: &mut gpui::TestAppContext) {
|
||||||
unreachable!()
|
unreachable!()
|
||||||
});
|
});
|
||||||
let format = editor.update(cx, |editor, cx| {
|
let format = editor.update(cx, |editor, cx| {
|
||||||
editor.perform_format(project, FormatTrigger::Manual, cx)
|
editor.perform_format(FormatTrigger::Manual, cx)
|
||||||
});
|
});
|
||||||
cx.foreground().advance_clock(super::FORMAT_TIMEOUT);
|
cx.foreground().advance_clock(super::FORMAT_TIMEOUT);
|
||||||
cx.foreground().start_waiting();
|
cx.foreground().start_waiting();
|
||||||
format.await.unwrap();
|
format.unwrap().await.unwrap();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
editor.read_with(cx, |editor, cx| editor.text(cx)),
|
editor.read_with(cx, |editor, cx| editor.text(cx)),
|
||||||
"one\ntwo\nthree\n"
|
"one\ntwo\nthree\n"
|
||||||
|
@ -8010,13 +8017,15 @@ async fn test_document_format_with_prettier(cx: &mut gpui::TestAppContext) {
|
||||||
|
|
||||||
let buffer_text = "one\ntwo\nthree\n";
|
let buffer_text = "one\ntwo\nthree\n";
|
||||||
let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx));
|
let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx));
|
||||||
let editor = cx.add_window(|cx| build_editor(buffer, cx)).root(cx);
|
let editor = cx
|
||||||
|
.add_window(|cx| build_editor_with_project(project.clone(), buffer, cx))
|
||||||
|
.root(cx);
|
||||||
editor.update(cx, |editor, cx| editor.set_text(buffer_text, cx));
|
editor.update(cx, |editor, cx| editor.set_text(buffer_text, cx));
|
||||||
|
|
||||||
let format = editor.update(cx, |editor, cx| {
|
let format = editor.update(cx, |editor, cx| {
|
||||||
editor.perform_format(project.clone(), FormatTrigger::Manual, cx)
|
editor.perform_format(FormatTrigger::Manual, cx)
|
||||||
});
|
});
|
||||||
format.await.unwrap();
|
format.unwrap().await.unwrap();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
editor.read_with(cx, |editor, cx| editor.text(cx)),
|
editor.read_with(cx, |editor, cx| editor.text(cx)),
|
||||||
buffer_text.to_string() + prettier_format_suffix,
|
buffer_text.to_string() + prettier_format_suffix,
|
||||||
|
@ -8027,9 +8036,9 @@ async fn test_document_format_with_prettier(cx: &mut gpui::TestAppContext) {
|
||||||
settings.defaults.formatter = Some(language_settings::Formatter::Auto)
|
settings.defaults.formatter = Some(language_settings::Formatter::Auto)
|
||||||
});
|
});
|
||||||
let format = editor.update(cx, |editor, cx| {
|
let format = editor.update(cx, |editor, cx| {
|
||||||
editor.perform_format(project.clone(), FormatTrigger::Manual, cx)
|
editor.perform_format(FormatTrigger::Manual, cx)
|
||||||
});
|
});
|
||||||
format.await.unwrap();
|
format.unwrap().await.unwrap();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
editor.read_with(cx, |editor, cx| editor.text(cx)),
|
editor.read_with(cx, |editor, cx| editor.text(cx)),
|
||||||
buffer_text.to_string() + prettier_format_suffix + "\n" + prettier_format_suffix,
|
buffer_text.to_string() + prettier_format_suffix + "\n" + prettier_format_suffix,
|
||||||
|
|
|
@ -80,7 +80,7 @@ pub fn find_hovered_hint_part(
|
||||||
|
|
||||||
pub fn hover_at_inlay(editor: &mut Editor, inlay_hover: InlayHover, cx: &mut ViewContext<Editor>) {
|
pub fn hover_at_inlay(editor: &mut Editor, inlay_hover: InlayHover, cx: &mut ViewContext<Editor>) {
|
||||||
if settings::get::<EditorSettings>(cx).hover_popover_enabled {
|
if settings::get::<EditorSettings>(cx).hover_popover_enabled {
|
||||||
if editor.pending_rename.is_some() {
|
if editor.has_pending_rename(cx) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -166,7 +166,7 @@ fn show_hover(
|
||||||
ignore_timeout: bool,
|
ignore_timeout: bool,
|
||||||
cx: &mut ViewContext<Editor>,
|
cx: &mut ViewContext<Editor>,
|
||||||
) {
|
) {
|
||||||
if editor.pending_rename.is_some() {
|
if editor.has_pending_rename(cx) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -11,10 +11,10 @@ use crate::{
|
||||||
use anyhow::Context;
|
use anyhow::Context;
|
||||||
use clock::Global;
|
use clock::Global;
|
||||||
use futures::future;
|
use futures::future;
|
||||||
use gpui::{ModelContext, ModelHandle, Task, ViewContext};
|
use gpui::{Entity, ModelContext, ModelHandle, Task, ViewContext};
|
||||||
use language::{language_settings::InlayHintKind, Buffer, BufferSnapshot};
|
use language::{language_settings::InlayHintKind, Buffer, BufferSnapshot};
|
||||||
use parking_lot::RwLock;
|
use parking_lot::RwLock;
|
||||||
use project::{InlayHint, ResolveState};
|
use project::{InlayHint, Project, ResolveState};
|
||||||
|
|
||||||
use collections::{hash_map, HashMap, HashSet};
|
use collections::{hash_map, HashMap, HashSet};
|
||||||
use language::language_settings::InlayHintSettings;
|
use language::language_settings::InlayHintSettings;
|
||||||
|
@ -24,9 +24,11 @@ use text::{ToOffset, ToPoint};
|
||||||
use util::post_inc;
|
use util::post_inc;
|
||||||
|
|
||||||
pub struct InlayHintCache {
|
pub struct InlayHintCache {
|
||||||
hints: HashMap<ExcerptId, Arc<RwLock<CachedExcerptHints>>>,
|
// TODO kb consider weak handles
|
||||||
|
project: ModelHandle<Project>,
|
||||||
|
pub(super) hints: HashMap<ExcerptId, Arc<RwLock<CachedExcerptHints>>>,
|
||||||
allowed_hint_kinds: HashSet<Option<InlayHintKind>>,
|
allowed_hint_kinds: HashSet<Option<InlayHintKind>>,
|
||||||
version: usize,
|
pub(super) version: usize,
|
||||||
pub(super) enabled: bool,
|
pub(super) enabled: bool,
|
||||||
update_tasks: HashMap<ExcerptId, TasksForRanges>,
|
update_tasks: HashMap<ExcerptId, TasksForRanges>,
|
||||||
lsp_request_limiter: Arc<Semaphore>,
|
lsp_request_limiter: Arc<Semaphore>,
|
||||||
|
@ -236,9 +238,13 @@ impl TasksForRanges {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Entity for InlayHintCache {
|
||||||
|
type Event = ();
|
||||||
|
}
|
||||||
impl InlayHintCache {
|
impl InlayHintCache {
|
||||||
pub fn new(inlay_hint_settings: InlayHintSettings) -> Self {
|
pub fn new(project: ModelHandle<Project>, inlay_hint_settings: InlayHintSettings) -> Self {
|
||||||
Self {
|
Self {
|
||||||
|
project,
|
||||||
allowed_hint_kinds: inlay_hint_settings.enabled_inlay_hint_kinds(),
|
allowed_hint_kinds: inlay_hint_settings.enabled_inlay_hint_kinds(),
|
||||||
enabled: inlay_hint_settings.enabled,
|
enabled: inlay_hint_settings.enabled,
|
||||||
hints: HashMap::default(),
|
hints: HashMap::default(),
|
||||||
|
@ -304,6 +310,7 @@ impl InlayHintCache {
|
||||||
reason: &'static str,
|
reason: &'static str,
|
||||||
excerpts_to_query: HashMap<ExcerptId, (ModelHandle<Buffer>, Global, Range<usize>)>,
|
excerpts_to_query: HashMap<ExcerptId, (ModelHandle<Buffer>, Global, Range<usize>)>,
|
||||||
invalidate: InvalidationStrategy,
|
invalidate: InvalidationStrategy,
|
||||||
|
cache: ModelHandle<Self>,
|
||||||
cx: &mut ViewContext<Editor>,
|
cx: &mut ViewContext<Editor>,
|
||||||
) -> Option<InlaySplice> {
|
) -> Option<InlaySplice> {
|
||||||
if !self.enabled {
|
if !self.enabled {
|
||||||
|
@ -336,6 +343,7 @@ impl InlayHintCache {
|
||||||
excerpts_to_query,
|
excerpts_to_query,
|
||||||
invalidate,
|
invalidate,
|
||||||
cache_version,
|
cache_version,
|
||||||
|
cache,
|
||||||
cx,
|
cx,
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
@ -538,7 +546,7 @@ impl InlayHintCache {
|
||||||
.read(cx)
|
.read(cx)
|
||||||
.buffer(buffer_id)
|
.buffer(buffer_id)
|
||||||
.and_then(|buffer| {
|
.and_then(|buffer| {
|
||||||
let project = editor.project.as_ref()?;
|
let project = self.project;
|
||||||
Some(project.update(cx, |project, cx| {
|
Some(project.update(cx, |project, cx| {
|
||||||
project.resolve_inlay_hint(
|
project.resolve_inlay_hint(
|
||||||
hint_to_resolve,
|
hint_to_resolve,
|
||||||
|
@ -582,6 +590,7 @@ fn spawn_new_update_tasks(
|
||||||
excerpts_to_query: HashMap<ExcerptId, (ModelHandle<Buffer>, Global, Range<usize>)>,
|
excerpts_to_query: HashMap<ExcerptId, (ModelHandle<Buffer>, Global, Range<usize>)>,
|
||||||
invalidate: InvalidationStrategy,
|
invalidate: InvalidationStrategy,
|
||||||
update_cache_version: usize,
|
update_cache_version: usize,
|
||||||
|
cache: ModelHandle<InlayHintCache>,
|
||||||
cx: &mut ViewContext<'_, '_, Editor>,
|
cx: &mut ViewContext<'_, '_, Editor>,
|
||||||
) {
|
) {
|
||||||
let visible_hints = Arc::new(editor.visible_inlay_hints(cx));
|
let visible_hints = Arc::new(editor.visible_inlay_hints(cx));
|
||||||
|
@ -601,7 +610,8 @@ fn spawn_new_update_tasks(
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
let cached_excerpt_hints = editor.inlay_hint_cache.hints.get(&excerpt_id).cloned();
|
let cached_excerpt_hints =
|
||||||
|
cache.update(cx, |cache, _| cache.hints.get(&excerpt_id).cloned());
|
||||||
if let Some(cached_excerpt_hints) = &cached_excerpt_hints {
|
if let Some(cached_excerpt_hints) = &cached_excerpt_hints {
|
||||||
let cached_excerpt_hints = cached_excerpt_hints.read();
|
let cached_excerpt_hints = cached_excerpt_hints.read();
|
||||||
let cached_buffer_version = &cached_excerpt_hints.buffer_version;
|
let cached_buffer_version = &cached_excerpt_hints.buffer_version;
|
||||||
|
@ -636,35 +646,39 @@ fn spawn_new_update_tasks(
|
||||||
reason,
|
reason,
|
||||||
};
|
};
|
||||||
|
|
||||||
let new_update_task = |query_ranges| {
|
let cache_handle = cache.clone();
|
||||||
new_update_task(
|
cache.update(cx, |cache, _| {
|
||||||
query,
|
let new_update_task = |query_ranges| {
|
||||||
query_ranges,
|
new_update_task(
|
||||||
multi_buffer_snapshot,
|
query,
|
||||||
buffer_snapshot.clone(),
|
|
||||||
Arc::clone(&visible_hints),
|
|
||||||
cached_excerpt_hints,
|
|
||||||
Arc::clone(&editor.inlay_hint_cache.lsp_request_limiter),
|
|
||||||
cx,
|
|
||||||
)
|
|
||||||
};
|
|
||||||
|
|
||||||
match editor.inlay_hint_cache.update_tasks.entry(excerpt_id) {
|
|
||||||
hash_map::Entry::Occupied(mut o) => {
|
|
||||||
o.get_mut().update_cached_tasks(
|
|
||||||
&buffer_snapshot,
|
|
||||||
query_ranges,
|
query_ranges,
|
||||||
invalidate,
|
multi_buffer_snapshot,
|
||||||
new_update_task,
|
buffer_snapshot.clone(),
|
||||||
);
|
Arc::clone(&visible_hints),
|
||||||
|
cached_excerpt_hints,
|
||||||
|
Arc::clone(&cache.lsp_request_limiter),
|
||||||
|
cache_handle,
|
||||||
|
cx,
|
||||||
|
)
|
||||||
|
};
|
||||||
|
|
||||||
|
match cache.update_tasks.entry(excerpt_id) {
|
||||||
|
hash_map::Entry::Occupied(mut o) => {
|
||||||
|
o.get_mut().update_cached_tasks(
|
||||||
|
&buffer_snapshot,
|
||||||
|
query_ranges,
|
||||||
|
invalidate,
|
||||||
|
new_update_task,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
hash_map::Entry::Vacant(v) => {
|
||||||
|
v.insert(TasksForRanges::new(
|
||||||
|
query_ranges.clone(),
|
||||||
|
new_update_task(query_ranges),
|
||||||
|
));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
hash_map::Entry::Vacant(v) => {
|
})
|
||||||
v.insert(TasksForRanges::new(
|
|
||||||
query_ranges.clone(),
|
|
||||||
new_update_task(query_ranges),
|
|
||||||
));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -760,6 +774,7 @@ fn new_update_task(
|
||||||
visible_hints: Arc<Vec<Inlay>>,
|
visible_hints: Arc<Vec<Inlay>>,
|
||||||
cached_excerpt_hints: Option<Arc<RwLock<CachedExcerptHints>>>,
|
cached_excerpt_hints: Option<Arc<RwLock<CachedExcerptHints>>>,
|
||||||
lsp_request_limiter: Arc<Semaphore>,
|
lsp_request_limiter: Arc<Semaphore>,
|
||||||
|
cache: ModelHandle<InlayHintCache>,
|
||||||
cx: &mut ViewContext<'_, '_, Editor>,
|
cx: &mut ViewContext<'_, '_, Editor>,
|
||||||
) -> Task<()> {
|
) -> Task<()> {
|
||||||
cx.spawn(|editor, mut cx| async move {
|
cx.spawn(|editor, mut cx| async move {
|
||||||
|
@ -775,6 +790,7 @@ fn new_update_task(
|
||||||
invalidate,
|
invalidate,
|
||||||
range,
|
range,
|
||||||
Arc::clone(&lsp_request_limiter),
|
Arc::clone(&lsp_request_limiter),
|
||||||
|
cache,
|
||||||
closure_cx.clone(),
|
closure_cx.clone(),
|
||||||
)
|
)
|
||||||
};
|
};
|
||||||
|
@ -795,13 +811,9 @@ fn new_update_task(
|
||||||
|
|
||||||
let mut query_range_failed = |range: &Range<language::Anchor>, e: anyhow::Error| {
|
let mut query_range_failed = |range: &Range<language::Anchor>, e: anyhow::Error| {
|
||||||
log::error!("inlay hint update task for range {range:?} failed: {e:#}");
|
log::error!("inlay hint update task for range {range:?} failed: {e:#}");
|
||||||
editor
|
cache
|
||||||
.update(&mut cx, |editor, _| {
|
.update(&mut cx, |cache, _| {
|
||||||
if let Some(task_ranges) = editor
|
if let Some(task_ranges) = cache.update_tasks.get_mut(&query.excerpt_id) {
|
||||||
.inlay_hint_cache
|
|
||||||
.update_tasks
|
|
||||||
.get_mut(&query.excerpt_id)
|
|
||||||
{
|
|
||||||
task_ranges.invalidate_range(&buffer_snapshot, &range);
|
task_ranges.invalidate_range(&buffer_snapshot, &range);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
@ -846,6 +858,7 @@ async fn fetch_and_update_hints(
|
||||||
invalidate: bool,
|
invalidate: bool,
|
||||||
fetch_range: Range<language::Anchor>,
|
fetch_range: Range<language::Anchor>,
|
||||||
lsp_request_limiter: Arc<Semaphore>,
|
lsp_request_limiter: Arc<Semaphore>,
|
||||||
|
cache: ModelHandle<InlayHintCache>,
|
||||||
mut cx: gpui::AsyncAppContext,
|
mut cx: gpui::AsyncAppContext,
|
||||||
) -> anyhow::Result<()> {
|
) -> anyhow::Result<()> {
|
||||||
let (lsp_request_guard, got_throttled) = if query.invalidate.should_invalidate() {
|
let (lsp_request_guard, got_throttled) = if query.invalidate.should_invalidate() {
|
||||||
|
@ -880,13 +893,14 @@ async fn fetch_and_update_hints(
|
||||||
};
|
};
|
||||||
if query_not_around_visible_range {
|
if query_not_around_visible_range {
|
||||||
log::trace!("Fetching inlay hints for range {fetch_range_to_log:?} got throttled and fell off the current visible range, skipping.");
|
log::trace!("Fetching inlay hints for range {fetch_range_to_log:?} got throttled and fell off the current visible range, skipping.");
|
||||||
if let Some(task_ranges) = editor
|
cache.update(cx, |cache, _| {
|
||||||
.inlay_hint_cache
|
if let Some(task_ranges) = cache
|
||||||
.update_tasks
|
.update_tasks
|
||||||
.get_mut(&query.excerpt_id)
|
.get_mut(&query.excerpt_id)
|
||||||
{
|
{
|
||||||
task_ranges.invalidate_range(&buffer_snapshot, &fetch_range);
|
task_ranges.invalidate_range(&buffer_snapshot, &fetch_range);
|
||||||
}
|
}
|
||||||
|
});
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -895,7 +909,7 @@ async fn fetch_and_update_hints(
|
||||||
.read(cx)
|
.read(cx)
|
||||||
.buffer(query.buffer_id)
|
.buffer(query.buffer_id)
|
||||||
.and_then(|buffer| {
|
.and_then(|buffer| {
|
||||||
let project = editor.project.as_ref()?;
|
let project = cache.read(cx).project;
|
||||||
Some(project.update(cx, |project, cx| {
|
Some(project.update(cx, |project, cx| {
|
||||||
project.inlay_hints(buffer, fetch_range.clone(), cx)
|
project.inlay_hints(buffer, fetch_range.clone(), cx)
|
||||||
}))
|
}))
|
||||||
|
@ -957,6 +971,7 @@ async fn fetch_and_update_hints(
|
||||||
invalidate,
|
invalidate,
|
||||||
buffer_snapshot,
|
buffer_snapshot,
|
||||||
multi_buffer_snapshot,
|
multi_buffer_snapshot,
|
||||||
|
cache,
|
||||||
cx,
|
cx,
|
||||||
);
|
);
|
||||||
})
|
})
|
||||||
|
@ -1071,13 +1086,11 @@ fn apply_hint_update(
|
||||||
invalidate: bool,
|
invalidate: bool,
|
||||||
buffer_snapshot: BufferSnapshot,
|
buffer_snapshot: BufferSnapshot,
|
||||||
multi_buffer_snapshot: MultiBufferSnapshot,
|
multi_buffer_snapshot: MultiBufferSnapshot,
|
||||||
|
cache: ModelHandle<InlayHintsCache>,
|
||||||
cx: &mut ViewContext<'_, '_, Editor>,
|
cx: &mut ViewContext<'_, '_, Editor>,
|
||||||
) {
|
) {
|
||||||
let cached_excerpt_hints = editor
|
cache.update(cx, |cache, cx| {
|
||||||
.inlay_hint_cache
|
let cached_excerpt_hints = cache.hints.entry(new_update.excerpt_id).or_insert_with(|| {
|
||||||
.hints
|
|
||||||
.entry(new_update.excerpt_id)
|
|
||||||
.or_insert_with(|| {
|
|
||||||
Arc::new(RwLock::new(CachedExcerptHints {
|
Arc::new(RwLock::new(CachedExcerptHints {
|
||||||
version: query.cache_version,
|
version: query.cache_version,
|
||||||
buffer_version: buffer_snapshot.version().clone(),
|
buffer_version: buffer_snapshot.version().clone(),
|
||||||
|
@ -1086,112 +1099,111 @@ fn apply_hint_update(
|
||||||
hints_by_id: HashMap::default(),
|
hints_by_id: HashMap::default(),
|
||||||
}))
|
}))
|
||||||
});
|
});
|
||||||
let mut cached_excerpt_hints = cached_excerpt_hints.write();
|
let mut cached_excerpt_hints = cached_excerpt_hints.write();
|
||||||
match query.cache_version.cmp(&cached_excerpt_hints.version) {
|
match query.cache_version.cmp(&cached_excerpt_hints.version) {
|
||||||
cmp::Ordering::Less => return,
|
cmp::Ordering::Less => return,
|
||||||
cmp::Ordering::Greater | cmp::Ordering::Equal => {
|
cmp::Ordering::Greater | cmp::Ordering::Equal => {
|
||||||
cached_excerpt_hints.version = query.cache_version;
|
cached_excerpt_hints.version = query.cache_version;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
let mut cached_inlays_changed = !new_update.remove_from_cache.is_empty();
|
let mut cached_inlays_changed = !new_update.remove_from_cache.is_empty();
|
||||||
cached_excerpt_hints
|
cached_excerpt_hints
|
||||||
.ordered_hints
|
|
||||||
.retain(|hint_id| !new_update.remove_from_cache.contains(hint_id));
|
|
||||||
cached_excerpt_hints
|
|
||||||
.hints_by_id
|
|
||||||
.retain(|hint_id, _| !new_update.remove_from_cache.contains(hint_id));
|
|
||||||
let mut splice = InlaySplice {
|
|
||||||
to_remove: new_update.remove_from_visible,
|
|
||||||
to_insert: Vec::new(),
|
|
||||||
};
|
|
||||||
for new_hint in new_update.add_to_cache {
|
|
||||||
let insert_position = match cached_excerpt_hints
|
|
||||||
.ordered_hints
|
.ordered_hints
|
||||||
.binary_search_by(|probe| {
|
.retain(|hint_id| !new_update.remove_from_cache.contains(hint_id));
|
||||||
cached_excerpt_hints.hints_by_id[probe]
|
cached_excerpt_hints
|
||||||
.position
|
.hints_by_id
|
||||||
.cmp(&new_hint.position, &buffer_snapshot)
|
.retain(|hint_id, _| !new_update.remove_from_cache.contains(hint_id));
|
||||||
}) {
|
let mut splice = InlaySplice {
|
||||||
Ok(i) => {
|
to_remove: new_update.remove_from_visible,
|
||||||
let mut insert_position = Some(i);
|
to_insert: Vec::new(),
|
||||||
for id in &cached_excerpt_hints.ordered_hints[i..] {
|
|
||||||
let cached_hint = &cached_excerpt_hints.hints_by_id[id];
|
|
||||||
if new_hint
|
|
||||||
.position
|
|
||||||
.cmp(&cached_hint.position, &buffer_snapshot)
|
|
||||||
.is_gt()
|
|
||||||
{
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
if cached_hint.text() == new_hint.text() {
|
|
||||||
insert_position = None;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
insert_position
|
|
||||||
}
|
|
||||||
Err(i) => Some(i),
|
|
||||||
};
|
};
|
||||||
|
for new_hint in new_update.add_to_cache {
|
||||||
|
let insert_position =
|
||||||
|
match cached_excerpt_hints
|
||||||
|
.ordered_hints
|
||||||
|
.binary_search_by(|probe| {
|
||||||
|
cached_excerpt_hints.hints_by_id[probe]
|
||||||
|
.position
|
||||||
|
.cmp(&new_hint.position, &buffer_snapshot)
|
||||||
|
}) {
|
||||||
|
Ok(i) => {
|
||||||
|
let mut insert_position = Some(i);
|
||||||
|
for id in &cached_excerpt_hints.ordered_hints[i..] {
|
||||||
|
let cached_hint = &cached_excerpt_hints.hints_by_id[id];
|
||||||
|
if new_hint
|
||||||
|
.position
|
||||||
|
.cmp(&cached_hint.position, &buffer_snapshot)
|
||||||
|
.is_gt()
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if cached_hint.text() == new_hint.text() {
|
||||||
|
insert_position = None;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
insert_position
|
||||||
|
}
|
||||||
|
Err(i) => Some(i),
|
||||||
|
};
|
||||||
|
|
||||||
if let Some(insert_position) = insert_position {
|
if let Some(insert_position) = insert_position {
|
||||||
let new_inlay_id = post_inc(&mut editor.next_inlay_id);
|
let new_inlay_id = post_inc(&mut editor.next_inlay_id);
|
||||||
if editor
|
if cache.allowed_hint_kinds.contains(&new_hint.kind) {
|
||||||
.inlay_hint_cache
|
let new_hint_position = multi_buffer_snapshot
|
||||||
.allowed_hint_kinds
|
.anchor_in_excerpt(query.excerpt_id, new_hint.position);
|
||||||
.contains(&new_hint.kind)
|
splice
|
||||||
{
|
.to_insert
|
||||||
let new_hint_position =
|
.push(Inlay::hint(new_inlay_id, new_hint_position, &new_hint));
|
||||||
multi_buffer_snapshot.anchor_in_excerpt(query.excerpt_id, new_hint.position);
|
}
|
||||||
splice
|
let new_id = InlayId::Hint(new_inlay_id);
|
||||||
.to_insert
|
cached_excerpt_hints.hints_by_id.insert(new_id, new_hint);
|
||||||
.push(Inlay::hint(new_inlay_id, new_hint_position, &new_hint));
|
cached_excerpt_hints
|
||||||
}
|
.ordered_hints
|
||||||
let new_id = InlayId::Hint(new_inlay_id);
|
.insert(insert_position, new_id);
|
||||||
cached_excerpt_hints.hints_by_id.insert(new_id, new_hint);
|
cached_inlays_changed = true;
|
||||||
cached_excerpt_hints
|
|
||||||
.ordered_hints
|
|
||||||
.insert(insert_position, new_id);
|
|
||||||
cached_inlays_changed = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
cached_excerpt_hints.buffer_version = buffer_snapshot.version().clone();
|
|
||||||
drop(cached_excerpt_hints);
|
|
||||||
|
|
||||||
if invalidate {
|
|
||||||
let mut outdated_excerpt_caches = HashSet::default();
|
|
||||||
for (excerpt_id, excerpt_hints) in &editor.inlay_hint_cache().hints {
|
|
||||||
let excerpt_hints = excerpt_hints.read();
|
|
||||||
if excerpt_hints.buffer_id == query.buffer_id
|
|
||||||
&& excerpt_id != &query.excerpt_id
|
|
||||||
&& buffer_snapshot
|
|
||||||
.version()
|
|
||||||
.changed_since(&excerpt_hints.buffer_version)
|
|
||||||
{
|
|
||||||
outdated_excerpt_caches.insert(*excerpt_id);
|
|
||||||
splice
|
|
||||||
.to_remove
|
|
||||||
.extend(excerpt_hints.ordered_hints.iter().copied());
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
cached_inlays_changed |= !outdated_excerpt_caches.is_empty();
|
cached_excerpt_hints.buffer_version = buffer_snapshot.version().clone();
|
||||||
editor
|
drop(cached_excerpt_hints);
|
||||||
.inlay_hint_cache
|
});
|
||||||
.hints
|
|
||||||
.retain(|excerpt_id, _| !outdated_excerpt_caches.contains(excerpt_id));
|
|
||||||
}
|
|
||||||
|
|
||||||
let InlaySplice {
|
cache.update(cx, |cache, _| {
|
||||||
to_remove,
|
if invalidate {
|
||||||
to_insert,
|
let mut outdated_excerpt_caches = HashSet::default();
|
||||||
} = splice;
|
for (excerpt_id, excerpt_hints) in &cache.hints {
|
||||||
let displayed_inlays_changed = !to_remove.is_empty() || !to_insert.is_empty();
|
let excerpt_hints = excerpt_hints.read();
|
||||||
if cached_inlays_changed || displayed_inlays_changed {
|
if excerpt_hints.buffer_id == query.buffer_id
|
||||||
editor.inlay_hint_cache.version += 1;
|
&& excerpt_id != &query.excerpt_id
|
||||||
}
|
&& buffer_snapshot
|
||||||
if displayed_inlays_changed {
|
.version()
|
||||||
editor.splice_inlay_hints(to_remove, to_insert, cx)
|
.changed_since(&excerpt_hints.buffer_version)
|
||||||
}
|
{
|
||||||
|
outdated_excerpt_caches.insert(*excerpt_id);
|
||||||
|
splice
|
||||||
|
.to_remove
|
||||||
|
.extend(excerpt_hints.ordered_hints.iter().copied());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
cached_inlays_changed |= !outdated_excerpt_caches.is_empty();
|
||||||
|
cache
|
||||||
|
.hints
|
||||||
|
.retain(|excerpt_id, _| !outdated_excerpt_caches.contains(excerpt_id));
|
||||||
|
}
|
||||||
|
|
||||||
|
let InlaySplice {
|
||||||
|
to_remove,
|
||||||
|
to_insert,
|
||||||
|
} = splice;
|
||||||
|
let displayed_inlays_changed = !to_remove.is_empty() || !to_insert.is_empty();
|
||||||
|
if cached_inlays_changed || displayed_inlays_changed {
|
||||||
|
cache.version += 1;
|
||||||
|
}
|
||||||
|
if displayed_inlays_changed {
|
||||||
|
editor.splice_inlay_hints(to_remove, to_insert, cx)
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
|
@ -1287,7 +1299,8 @@ pub mod tests {
|
||||||
"Cache should use editor settings to get the allowed hint kinds"
|
"Cache should use editor settings to get the allowed hint kinds"
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
inlay_cache.version, edits_made,
|
editor.inlay_hint_cache_version(),
|
||||||
|
edits_made,
|
||||||
"The editor update the cache version after every cache/view change"
|
"The editor update the cache version after every cache/view change"
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
@ -1312,7 +1325,8 @@ pub mod tests {
|
||||||
"Cache should use editor settings to get the allowed hint kinds"
|
"Cache should use editor settings to get the allowed hint kinds"
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
inlay_cache.version, edits_made,
|
editor.inlay_hint_cache_version(),
|
||||||
|
edits_made,
|
||||||
"The editor update the cache version after every cache/view change"
|
"The editor update the cache version after every cache/view change"
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
@ -1392,7 +1406,7 @@ pub mod tests {
|
||||||
);
|
);
|
||||||
assert_eq!(expected_hints, visible_hint_labels(editor, cx));
|
assert_eq!(expected_hints, visible_hint_labels(editor, cx));
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
editor.inlay_hint_cache().version,
|
editor.inlay_hint_cache_version(),
|
||||||
edits_made,
|
edits_made,
|
||||||
"The editor update the cache version after every cache/view change"
|
"The editor update the cache version after every cache/view change"
|
||||||
);
|
);
|
||||||
|
@ -1423,7 +1437,7 @@ pub mod tests {
|
||||||
);
|
);
|
||||||
assert_eq!(expected_hints, visible_hint_labels(editor, cx));
|
assert_eq!(expected_hints, visible_hint_labels(editor, cx));
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
editor.inlay_hint_cache().version,
|
editor.inlay_hint_cache_version(),
|
||||||
edits_made,
|
edits_made,
|
||||||
"Should not update the cache while the work task is running"
|
"Should not update the cache while the work task is running"
|
||||||
);
|
);
|
||||||
|
@ -1447,7 +1461,7 @@ pub mod tests {
|
||||||
);
|
);
|
||||||
assert_eq!(expected_hints, visible_hint_labels(editor, cx));
|
assert_eq!(expected_hints, visible_hint_labels(editor, cx));
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
editor.inlay_hint_cache().version,
|
editor.inlay_hint_cache_version(),
|
||||||
edits_made,
|
edits_made,
|
||||||
"Cache version should udpate once after the work task is done"
|
"Cache version should udpate once after the work task is done"
|
||||||
);
|
);
|
||||||
|
@ -1566,7 +1580,7 @@ pub mod tests {
|
||||||
);
|
);
|
||||||
assert_eq!(expected_hints, visible_hint_labels(editor, cx));
|
assert_eq!(expected_hints, visible_hint_labels(editor, cx));
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
editor.inlay_hint_cache().version,
|
editor.inlay_hint_cache_version(),
|
||||||
1,
|
1,
|
||||||
"Rust editor update the cache version after every cache/view change"
|
"Rust editor update the cache version after every cache/view change"
|
||||||
);
|
);
|
||||||
|
@ -1623,7 +1637,7 @@ pub mod tests {
|
||||||
"Markdown editor should have a separate verison, repeating Rust editor rules"
|
"Markdown editor should have a separate verison, repeating Rust editor rules"
|
||||||
);
|
);
|
||||||
assert_eq!(expected_hints, visible_hint_labels(editor, cx));
|
assert_eq!(expected_hints, visible_hint_labels(editor, cx));
|
||||||
assert_eq!(editor.inlay_hint_cache().version, 1);
|
assert_eq!(editor.inlay_hint_cache_version(), 1);
|
||||||
});
|
});
|
||||||
|
|
||||||
rs_editor.update(cx, |editor, cx| {
|
rs_editor.update(cx, |editor, cx| {
|
||||||
|
@ -1640,7 +1654,7 @@ pub mod tests {
|
||||||
);
|
);
|
||||||
assert_eq!(expected_hints, visible_hint_labels(editor, cx));
|
assert_eq!(expected_hints, visible_hint_labels(editor, cx));
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
editor.inlay_hint_cache().version,
|
editor.inlay_hint_cache_version(),
|
||||||
2,
|
2,
|
||||||
"Every time hint cache changes, cache version should be incremented"
|
"Every time hint cache changes, cache version should be incremented"
|
||||||
);
|
);
|
||||||
|
@ -1653,7 +1667,7 @@ pub mod tests {
|
||||||
"Markdown editor should not be affected by Rust editor changes"
|
"Markdown editor should not be affected by Rust editor changes"
|
||||||
);
|
);
|
||||||
assert_eq!(expected_hints, visible_hint_labels(editor, cx));
|
assert_eq!(expected_hints, visible_hint_labels(editor, cx));
|
||||||
assert_eq!(editor.inlay_hint_cache().version, 1);
|
assert_eq!(editor.inlay_hint_cache_version(), 1);
|
||||||
});
|
});
|
||||||
|
|
||||||
md_editor.update(cx, |editor, cx| {
|
md_editor.update(cx, |editor, cx| {
|
||||||
|
@ -1669,7 +1683,7 @@ pub mod tests {
|
||||||
"Rust editor should not be affected by Markdown editor changes"
|
"Rust editor should not be affected by Markdown editor changes"
|
||||||
);
|
);
|
||||||
assert_eq!(expected_hints, visible_hint_labels(editor, cx));
|
assert_eq!(expected_hints, visible_hint_labels(editor, cx));
|
||||||
assert_eq!(editor.inlay_hint_cache().version, 2);
|
assert_eq!(editor.inlay_hint_cache_version(), 2);
|
||||||
});
|
});
|
||||||
rs_editor.update(cx, |editor, cx| {
|
rs_editor.update(cx, |editor, cx| {
|
||||||
let expected_hints = vec!["1".to_string()];
|
let expected_hints = vec!["1".to_string()];
|
||||||
|
@ -1679,7 +1693,7 @@ pub mod tests {
|
||||||
"Markdown editor should also change independently"
|
"Markdown editor should also change independently"
|
||||||
);
|
);
|
||||||
assert_eq!(expected_hints, visible_hint_labels(editor, cx));
|
assert_eq!(expected_hints, visible_hint_labels(editor, cx));
|
||||||
assert_eq!(editor.inlay_hint_cache().version, 2);
|
assert_eq!(editor.inlay_hint_cache_version(), 2);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1801,7 +1815,7 @@ pub mod tests {
|
||||||
visible_hint_labels(editor, cx)
|
visible_hint_labels(editor, cx)
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
editor.inlay_hint_cache().version,
|
editor.inlay_hint_cache_version(),
|
||||||
edits_made,
|
edits_made,
|
||||||
"Should not update cache version due to new loaded hints being the same"
|
"Should not update cache version due to new loaded hints being the same"
|
||||||
);
|
);
|
||||||
|
@ -1936,7 +1950,7 @@ pub mod tests {
|
||||||
assert!(cached_hint_labels(editor).is_empty());
|
assert!(cached_hint_labels(editor).is_empty());
|
||||||
assert!(visible_hint_labels(editor, cx).is_empty());
|
assert!(visible_hint_labels(editor, cx).is_empty());
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
editor.inlay_hint_cache().version, edits_made,
|
editor.inlay_hint_cache_version(), edits_made,
|
||||||
"The editor should not update the cache version after /refresh query without updates"
|
"The editor should not update the cache version after /refresh query without updates"
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
@ -2007,7 +2021,7 @@ pub mod tests {
|
||||||
vec!["parameter hint".to_string()],
|
vec!["parameter hint".to_string()],
|
||||||
visible_hint_labels(editor, cx),
|
visible_hint_labels(editor, cx),
|
||||||
);
|
);
|
||||||
assert_eq!(editor.inlay_hint_cache().version, edits_made);
|
assert_eq!(editor.inlay_hint_cache_version(), edits_made);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2086,7 +2100,7 @@ pub mod tests {
|
||||||
);
|
);
|
||||||
assert_eq!(expected_hints, visible_hint_labels(editor, cx));
|
assert_eq!(expected_hints, visible_hint_labels(editor, cx));
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
editor.inlay_hint_cache().version, 1,
|
editor.inlay_hint_cache_version(), 1,
|
||||||
"Only one update should be registered in the cache after all cancellations"
|
"Only one update should be registered in the cache after all cancellations"
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
@ -2131,7 +2145,7 @@ pub mod tests {
|
||||||
);
|
);
|
||||||
assert_eq!(expected_hints, visible_hint_labels(editor, cx));
|
assert_eq!(expected_hints, visible_hint_labels(editor, cx));
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
editor.inlay_hint_cache().version,
|
editor.inlay_hint_cache_version(),
|
||||||
2,
|
2,
|
||||||
"Should update the cache version once more, for the new change"
|
"Should update the cache version once more, for the new change"
|
||||||
);
|
);
|
||||||
|
@ -2301,7 +2315,7 @@ pub mod tests {
|
||||||
);
|
);
|
||||||
assert_eq!(expected_hints, visible_hint_labels(editor, cx), "Should display only hints from the visible range");
|
assert_eq!(expected_hints, visible_hint_labels(editor, cx), "Should display only hints from the visible range");
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
editor.inlay_hint_cache().version, requests_count,
|
editor.inlay_hint_cache_version(), requests_count,
|
||||||
"LSP queries should've bumped the cache version"
|
"LSP queries should've bumped the cache version"
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
@ -2363,7 +2377,7 @@ pub mod tests {
|
||||||
);
|
);
|
||||||
assert_eq!(expected_hints, visible_hint_labels(editor, cx));
|
assert_eq!(expected_hints, visible_hint_labels(editor, cx));
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
editor.inlay_hint_cache().version,
|
editor.inlay_hint_cache_version(),
|
||||||
lsp_requests,
|
lsp_requests,
|
||||||
"Should update the cache for every LSP response with hints added"
|
"Should update the cache for every LSP response with hints added"
|
||||||
);
|
);
|
||||||
|
@ -2427,7 +2441,7 @@ pub mod tests {
|
||||||
assert_eq!(expected_hints, cached_hint_labels(editor),
|
assert_eq!(expected_hints, cached_hint_labels(editor),
|
||||||
"Should have hints from the new LSP response after the edit");
|
"Should have hints from the new LSP response after the edit");
|
||||||
assert_eq!(expected_hints, visible_hint_labels(editor, cx));
|
assert_eq!(expected_hints, visible_hint_labels(editor, cx));
|
||||||
assert_eq!(editor.inlay_hint_cache().version, lsp_requests, "Should update the cache for every LSP response with hints added");
|
assert_eq!(editor.inlay_hint_cache_version(), lsp_requests, "Should update the cache for every LSP response with hints added");
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2650,7 +2664,7 @@ pub mod tests {
|
||||||
"When scroll is at the edge of a multibuffer, its visible excerpts only should be queried for inlay hints"
|
"When scroll is at the edge of a multibuffer, its visible excerpts only should be queried for inlay hints"
|
||||||
);
|
);
|
||||||
assert_eq!(expected_hints, visible_hint_labels(editor, cx));
|
assert_eq!(expected_hints, visible_hint_labels(editor, cx));
|
||||||
assert_eq!(editor.inlay_hint_cache().version, expected_hints.len(), "Every visible excerpt hints should bump the verison");
|
assert_eq!(editor.inlay_hint_cache_version(), expected_hints.len(), "Every visible excerpt hints should bump the verison");
|
||||||
});
|
});
|
||||||
|
|
||||||
editor.update(cx, |editor, cx| {
|
editor.update(cx, |editor, cx| {
|
||||||
|
@ -2680,7 +2694,7 @@ pub mod tests {
|
||||||
assert_eq!(expected_hints, cached_hint_labels(editor),
|
assert_eq!(expected_hints, cached_hint_labels(editor),
|
||||||
"With more scrolls of the multibuffer, more hints should be added into the cache and nothing invalidated without edits");
|
"With more scrolls of the multibuffer, more hints should be added into the cache and nothing invalidated without edits");
|
||||||
assert_eq!(expected_hints, visible_hint_labels(editor, cx));
|
assert_eq!(expected_hints, visible_hint_labels(editor, cx));
|
||||||
assert_eq!(editor.inlay_hint_cache().version, expected_hints.len(),
|
assert_eq!(editor.inlay_hint_cache_version(), expected_hints.len(),
|
||||||
"Due to every excerpt having one hint, we update cache per new excerpt scrolled");
|
"Due to every excerpt having one hint, we update cache per new excerpt scrolled");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -2711,7 +2725,7 @@ pub mod tests {
|
||||||
assert_eq!(expected_hints, cached_hint_labels(editor),
|
assert_eq!(expected_hints, cached_hint_labels(editor),
|
||||||
"After multibuffer was scrolled to the end, all hints for all excerpts should be fetched");
|
"After multibuffer was scrolled to the end, all hints for all excerpts should be fetched");
|
||||||
assert_eq!(expected_hints, visible_hint_labels(editor, cx));
|
assert_eq!(expected_hints, visible_hint_labels(editor, cx));
|
||||||
assert_eq!(editor.inlay_hint_cache().version, expected_hints.len());
|
assert_eq!(editor.inlay_hint_cache_version(), expected_hints.len());
|
||||||
expected_hints.len()
|
expected_hints.len()
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -2739,7 +2753,7 @@ pub mod tests {
|
||||||
assert_eq!(expected_hints, cached_hint_labels(editor),
|
assert_eq!(expected_hints, cached_hint_labels(editor),
|
||||||
"After multibuffer was scrolled to the end, further scrolls up should not bring more hints");
|
"After multibuffer was scrolled to the end, further scrolls up should not bring more hints");
|
||||||
assert_eq!(expected_hints, visible_hint_labels(editor, cx));
|
assert_eq!(expected_hints, visible_hint_labels(editor, cx));
|
||||||
assert_eq!(editor.inlay_hint_cache().version, last_scroll_update_version, "No updates should happen during scrolling already scolled buffer");
|
assert_eq!(editor.inlay_hint_cache_version(), last_scroll_update_version, "No updates should happen during scrolling already scolled buffer");
|
||||||
});
|
});
|
||||||
|
|
||||||
editor_edited.store(true, Ordering::Release);
|
editor_edited.store(true, Ordering::Release);
|
||||||
|
@ -2769,7 +2783,7 @@ all hints should be invalidated and requeried for all of its visible excerpts"
|
||||||
);
|
);
|
||||||
assert_eq!(expected_hints, visible_hint_labels(editor, cx));
|
assert_eq!(expected_hints, visible_hint_labels(editor, cx));
|
||||||
|
|
||||||
let current_cache_version = editor.inlay_hint_cache().version;
|
let current_cache_version = editor.inlay_hint_cache_version();
|
||||||
let minimum_expected_version = last_scroll_update_version + expected_hints.len();
|
let minimum_expected_version = last_scroll_update_version + expected_hints.len();
|
||||||
assert!(
|
assert!(
|
||||||
current_cache_version == minimum_expected_version || current_cache_version == minimum_expected_version + 1,
|
current_cache_version == minimum_expected_version || current_cache_version == minimum_expected_version + 1,
|
||||||
|
@ -2953,7 +2967,7 @@ all hints should be invalidated and requeried for all of its visible excerpts"
|
||||||
"All hints are disabled and should not be shown despite being present in the cache"
|
"All hints are disabled and should not be shown despite being present in the cache"
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
editor.inlay_hint_cache().version,
|
editor.inlay_hint_cache_version(),
|
||||||
2,
|
2,
|
||||||
"Cache should update once per excerpt query"
|
"Cache should update once per excerpt query"
|
||||||
);
|
);
|
||||||
|
@ -2976,7 +2990,7 @@ all hints should be invalidated and requeried for all of its visible excerpts"
|
||||||
"All hints are disabled and should not be shown despite being present in the cache"
|
"All hints are disabled and should not be shown despite being present in the cache"
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
editor.inlay_hint_cache().version,
|
editor.inlay_hint_cache_version(),
|
||||||
3,
|
3,
|
||||||
"Excerpt removal should trigger a cache update"
|
"Excerpt removal should trigger a cache update"
|
||||||
);
|
);
|
||||||
|
@ -3004,7 +3018,7 @@ all hints should be invalidated and requeried for all of its visible excerpts"
|
||||||
"Settings change should make cached hints visible"
|
"Settings change should make cached hints visible"
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
editor.inlay_hint_cache().version,
|
editor.inlay_hint_cache_version(),
|
||||||
4,
|
4,
|
||||||
"Settings change should trigger a cache update"
|
"Settings change should trigger a cache update"
|
||||||
);
|
);
|
||||||
|
@ -3114,7 +3128,7 @@ all hints should be invalidated and requeried for all of its visible excerpts"
|
||||||
let expected_hints = vec!["1".to_string()];
|
let expected_hints = vec!["1".to_string()];
|
||||||
assert_eq!(expected_hints, cached_hint_labels(editor));
|
assert_eq!(expected_hints, cached_hint_labels(editor));
|
||||||
assert_eq!(expected_hints, visible_hint_labels(editor, cx));
|
assert_eq!(expected_hints, visible_hint_labels(editor, cx));
|
||||||
assert_eq!(editor.inlay_hint_cache().version, 1);
|
assert_eq!(editor.inlay_hint_cache_version(), 1);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -3171,7 +3185,7 @@ all hints should be invalidated and requeried for all of its visible excerpts"
|
||||||
);
|
);
|
||||||
assert_eq!(expected_hints, visible_hint_labels(editor, cx));
|
assert_eq!(expected_hints, visible_hint_labels(editor, cx));
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
editor.inlay_hint_cache().version,
|
editor.inlay_hint_cache_version(),
|
||||||
1,
|
1,
|
||||||
"First toggle should be cache's first update"
|
"First toggle should be cache's first update"
|
||||||
);
|
);
|
||||||
|
@ -3187,7 +3201,7 @@ all hints should be invalidated and requeried for all of its visible excerpts"
|
||||||
"Should clear hints after 2nd toggle"
|
"Should clear hints after 2nd toggle"
|
||||||
);
|
);
|
||||||
assert!(visible_hint_labels(editor, cx).is_empty());
|
assert!(visible_hint_labels(editor, cx).is_empty());
|
||||||
assert_eq!(editor.inlay_hint_cache().version, 2);
|
assert_eq!(editor.inlay_hint_cache_version(), 2);
|
||||||
});
|
});
|
||||||
|
|
||||||
update_test_language_settings(cx, |settings| {
|
update_test_language_settings(cx, |settings| {
|
||||||
|
@ -3207,7 +3221,7 @@ all hints should be invalidated and requeried for all of its visible excerpts"
|
||||||
"Should query LSP hints for the 2nd time after enabling hints in settings"
|
"Should query LSP hints for the 2nd time after enabling hints in settings"
|
||||||
);
|
);
|
||||||
assert_eq!(expected_hints, visible_hint_labels(editor, cx));
|
assert_eq!(expected_hints, visible_hint_labels(editor, cx));
|
||||||
assert_eq!(editor.inlay_hint_cache().version, 3);
|
assert_eq!(editor.inlay_hint_cache_version(), 3);
|
||||||
});
|
});
|
||||||
|
|
||||||
editor.update(cx, |editor, cx| {
|
editor.update(cx, |editor, cx| {
|
||||||
|
@ -3220,7 +3234,7 @@ all hints should be invalidated and requeried for all of its visible excerpts"
|
||||||
"Should clear hints after enabling in settings and a 3rd toggle"
|
"Should clear hints after enabling in settings and a 3rd toggle"
|
||||||
);
|
);
|
||||||
assert!(visible_hint_labels(editor, cx).is_empty());
|
assert!(visible_hint_labels(editor, cx).is_empty());
|
||||||
assert_eq!(editor.inlay_hint_cache().version, 4);
|
assert_eq!(editor.inlay_hint_cache_version(), 4);
|
||||||
});
|
});
|
||||||
|
|
||||||
editor.update(cx, |editor, cx| {
|
editor.update(cx, |editor, cx| {
|
||||||
|
@ -3235,7 +3249,7 @@ all hints should be invalidated and requeried for all of its visible excerpts"
|
||||||
"Should query LSP hints for the 3rd time after enabling hints in settings and toggling them back on"
|
"Should query LSP hints for the 3rd time after enabling hints in settings and toggling them back on"
|
||||||
);
|
);
|
||||||
assert_eq!(expected_hints, visible_hint_labels(editor, cx));
|
assert_eq!(expected_hints, visible_hint_labels(editor, cx));
|
||||||
assert_eq!(editor.inlay_hint_cache().version, 5);
|
assert_eq!(editor.inlay_hint_cache_version(), 5);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -3318,7 +3332,7 @@ all hints should be invalidated and requeried for all of its visible excerpts"
|
||||||
editor.update(cx, |editor, cx| {
|
editor.update(cx, |editor, cx| {
|
||||||
assert!(cached_hint_labels(editor).is_empty());
|
assert!(cached_hint_labels(editor).is_empty());
|
||||||
assert!(visible_hint_labels(editor, cx).is_empty());
|
assert!(visible_hint_labels(editor, cx).is_empty());
|
||||||
assert_eq!(editor.inlay_hint_cache().version, 0);
|
assert_eq!(editor.inlay_hint_cache_version(), 0);
|
||||||
});
|
});
|
||||||
|
|
||||||
("/a/main.rs", editor, fake_server)
|
("/a/main.rs", editor, fake_server)
|
||||||
|
|
|
@ -3,7 +3,7 @@ use crate::{
|
||||||
movement::surrounding_word, persistence::DB, scroll::ScrollAnchor, Anchor, Autoscroll, Editor,
|
movement::surrounding_word, persistence::DB, scroll::ScrollAnchor, Anchor, Autoscroll, Editor,
|
||||||
Event, ExcerptId, ExcerptRange, MultiBuffer, MultiBufferSnapshot, NavigationData, ToPoint as _,
|
Event, ExcerptId, ExcerptRange, MultiBuffer, MultiBufferSnapshot, NavigationData, ToPoint as _,
|
||||||
};
|
};
|
||||||
use anyhow::{Context, Result};
|
use anyhow::{anyhow, Context, Result};
|
||||||
use collections::HashSet;
|
use collections::HashSet;
|
||||||
use futures::future::try_join_all;
|
use futures::future::try_join_all;
|
||||||
use gpui::{
|
use gpui::{
|
||||||
|
@ -652,7 +652,9 @@ impl Item for Editor {
|
||||||
cx: &mut ViewContext<Self>,
|
cx: &mut ViewContext<Self>,
|
||||||
) -> Task<Result<()>> {
|
) -> Task<Result<()>> {
|
||||||
self.report_editor_event("save", None, cx);
|
self.report_editor_event("save", None, cx);
|
||||||
let format = self.perform_format(project.clone(), FormatTrigger::Save, cx);
|
let Some(format) = self.perform_format(FormatTrigger::Save, cx) else {
|
||||||
|
return Task::ready(Err(anyhow!("Editor does not have a formatter attached")));
|
||||||
|
};
|
||||||
let buffers = self.buffer().clone().read(cx).all_buffers();
|
let buffers = self.buffer().clone().read(cx).all_buffers();
|
||||||
cx.spawn(|_, mut cx| async move {
|
cx.spawn(|_, mut cx| async move {
|
||||||
format.await?;
|
format.await?;
|
||||||
|
|
|
@ -178,6 +178,9 @@ pub fn update_inlay_link_and_hover_points(
|
||||||
let mut go_to_definition_updated = false;
|
let mut go_to_definition_updated = false;
|
||||||
let mut hover_updated = false;
|
let mut hover_updated = false;
|
||||||
if let Some(hovered_offset) = hovered_offset {
|
if let Some(hovered_offset) = hovered_offset {
|
||||||
|
let Some(inlay_hint_cache) = editor.inlay_hints.clone() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
let buffer_snapshot = editor.buffer().read(cx).snapshot(cx);
|
let buffer_snapshot = editor.buffer().read(cx).snapshot(cx);
|
||||||
let previous_valid_anchor = buffer_snapshot.anchor_at(
|
let previous_valid_anchor = buffer_snapshot.anchor_at(
|
||||||
point_for_position.previous_valid.to_point(snapshot),
|
point_for_position.previous_valid.to_point(snapshot),
|
||||||
|
@ -202,82 +205,84 @@ pub fn update_inlay_link_and_hover_points(
|
||||||
})
|
})
|
||||||
.max_by_key(|hint| hint.id)
|
.max_by_key(|hint| hint.id)
|
||||||
{
|
{
|
||||||
let inlay_hint_cache = editor.inlay_hint_cache();
|
|
||||||
let excerpt_id = previous_valid_anchor.excerpt_id;
|
let excerpt_id = previous_valid_anchor.excerpt_id;
|
||||||
if let Some(cached_hint) = inlay_hint_cache.hint_by_id(excerpt_id, hovered_hint.id) {
|
inlay_hint_cache.update(cx, |inlay_hint_cache, cx| {
|
||||||
match cached_hint.resolve_state {
|
if let Some(cached_hint) = inlay_hint_cache.hint_by_id(excerpt_id, hovered_hint.id)
|
||||||
ResolveState::CanResolve(_, _) => {
|
{
|
||||||
if let Some(buffer_id) = previous_valid_anchor.buffer_id {
|
match cached_hint.resolve_state {
|
||||||
inlay_hint_cache.spawn_hint_resolve(
|
ResolveState::CanResolve(_, _) => {
|
||||||
buffer_id,
|
if let Some(buffer_id) = previous_valid_anchor.buffer_id {
|
||||||
excerpt_id,
|
inlay_hint_cache.spawn_hint_resolve(
|
||||||
hovered_hint.id,
|
buffer_id,
|
||||||
cx,
|
excerpt_id,
|
||||||
);
|
hovered_hint.id,
|
||||||
}
|
cx,
|
||||||
}
|
);
|
||||||
ResolveState::Resolved => {
|
|
||||||
let mut extra_shift_left = 0;
|
|
||||||
let mut extra_shift_right = 0;
|
|
||||||
if cached_hint.padding_left {
|
|
||||||
extra_shift_left += 1;
|
|
||||||
extra_shift_right += 1;
|
|
||||||
}
|
|
||||||
if cached_hint.padding_right {
|
|
||||||
extra_shift_right += 1;
|
|
||||||
}
|
|
||||||
match cached_hint.label {
|
|
||||||
project::InlayHintLabel::String(_) => {
|
|
||||||
if let Some(tooltip) = cached_hint.tooltip {
|
|
||||||
hover_popover::hover_at_inlay(
|
|
||||||
editor,
|
|
||||||
InlayHover {
|
|
||||||
excerpt: excerpt_id,
|
|
||||||
tooltip: match tooltip {
|
|
||||||
InlayHintTooltip::String(text) => HoverBlock {
|
|
||||||
text,
|
|
||||||
kind: HoverBlockKind::PlainText,
|
|
||||||
},
|
|
||||||
InlayHintTooltip::MarkupContent(content) => {
|
|
||||||
HoverBlock {
|
|
||||||
text: content.value,
|
|
||||||
kind: content.kind,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
range: InlayHighlight {
|
|
||||||
inlay: hovered_hint.id,
|
|
||||||
inlay_position: hovered_hint.position,
|
|
||||||
range: extra_shift_left
|
|
||||||
..hovered_hint.text.len() + extra_shift_right,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
cx,
|
|
||||||
);
|
|
||||||
hover_updated = true;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
project::InlayHintLabel::LabelParts(label_parts) => {
|
}
|
||||||
let hint_start =
|
ResolveState::Resolved => {
|
||||||
snapshot.anchor_to_inlay_offset(hovered_hint.position);
|
let mut extra_shift_left = 0;
|
||||||
if let Some((hovered_hint_part, part_range)) =
|
let mut extra_shift_right = 0;
|
||||||
hover_popover::find_hovered_hint_part(
|
if cached_hint.padding_left {
|
||||||
label_parts,
|
extra_shift_left += 1;
|
||||||
hint_start,
|
extra_shift_right += 1;
|
||||||
hovered_offset,
|
}
|
||||||
)
|
if cached_hint.padding_right {
|
||||||
{
|
extra_shift_right += 1;
|
||||||
let highlight_start =
|
}
|
||||||
(part_range.start - hint_start).0 + extra_shift_left;
|
match cached_hint.label {
|
||||||
let highlight_end =
|
project::InlayHintLabel::String(_) => {
|
||||||
(part_range.end - hint_start).0 + extra_shift_right;
|
if let Some(tooltip) = cached_hint.tooltip {
|
||||||
let highlight = InlayHighlight {
|
|
||||||
inlay: hovered_hint.id,
|
|
||||||
inlay_position: hovered_hint.position,
|
|
||||||
range: highlight_start..highlight_end,
|
|
||||||
};
|
|
||||||
if let Some(tooltip) = hovered_hint_part.tooltip {
|
|
||||||
hover_popover::hover_at_inlay(
|
hover_popover::hover_at_inlay(
|
||||||
|
editor,
|
||||||
|
InlayHover {
|
||||||
|
excerpt: excerpt_id,
|
||||||
|
tooltip: match tooltip {
|
||||||
|
InlayHintTooltip::String(text) => HoverBlock {
|
||||||
|
text,
|
||||||
|
kind: HoverBlockKind::PlainText,
|
||||||
|
},
|
||||||
|
InlayHintTooltip::MarkupContent(content) => {
|
||||||
|
HoverBlock {
|
||||||
|
text: content.value,
|
||||||
|
kind: content.kind,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
range: InlayHighlight {
|
||||||
|
inlay: hovered_hint.id,
|
||||||
|
inlay_position: hovered_hint.position,
|
||||||
|
range: extra_shift_left
|
||||||
|
..hovered_hint.text.len()
|
||||||
|
+ extra_shift_right,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
cx,
|
||||||
|
);
|
||||||
|
hover_updated = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
project::InlayHintLabel::LabelParts(label_parts) => {
|
||||||
|
let hint_start =
|
||||||
|
snapshot.anchor_to_inlay_offset(hovered_hint.position);
|
||||||
|
if let Some((hovered_hint_part, part_range)) =
|
||||||
|
hover_popover::find_hovered_hint_part(
|
||||||
|
label_parts,
|
||||||
|
hint_start,
|
||||||
|
hovered_offset,
|
||||||
|
)
|
||||||
|
{
|
||||||
|
let highlight_start =
|
||||||
|
(part_range.start - hint_start).0 + extra_shift_left;
|
||||||
|
let highlight_end =
|
||||||
|
(part_range.end - hint_start).0 + extra_shift_right;
|
||||||
|
let highlight = InlayHighlight {
|
||||||
|
inlay: hovered_hint.id,
|
||||||
|
inlay_position: hovered_hint.position,
|
||||||
|
range: highlight_start..highlight_end,
|
||||||
|
};
|
||||||
|
if let Some(tooltip) = hovered_hint_part.tooltip {
|
||||||
|
hover_popover::hover_at_inlay(
|
||||||
editor,
|
editor,
|
||||||
InlayHover {
|
InlayHover {
|
||||||
excerpt: excerpt_id,
|
excerpt: excerpt_id,
|
||||||
|
@ -299,31 +304,32 @@ pub fn update_inlay_link_and_hover_points(
|
||||||
},
|
},
|
||||||
cx,
|
cx,
|
||||||
);
|
);
|
||||||
hover_updated = true;
|
hover_updated = true;
|
||||||
}
|
}
|
||||||
if let Some((language_server_id, location)) =
|
if let Some((language_server_id, location)) =
|
||||||
hovered_hint_part.location
|
hovered_hint_part.location
|
||||||
{
|
{
|
||||||
go_to_definition_updated = true;
|
go_to_definition_updated = true;
|
||||||
update_go_to_definition_link(
|
update_go_to_definition_link(
|
||||||
editor,
|
editor,
|
||||||
Some(GoToDefinitionTrigger::InlayHint(
|
Some(GoToDefinitionTrigger::InlayHint(
|
||||||
highlight,
|
highlight,
|
||||||
location,
|
location,
|
||||||
language_server_id,
|
language_server_id,
|
||||||
)),
|
)),
|
||||||
cmd_held,
|
cmd_held,
|
||||||
shift_held,
|
shift_held,
|
||||||
cx,
|
cx,
|
||||||
);
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
};
|
}
|
||||||
|
ResolveState::Resolving => {}
|
||||||
}
|
}
|
||||||
ResolveState::Resolving => {}
|
|
||||||
}
|
}
|
||||||
}
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -353,7 +359,7 @@ pub fn show_link_definition(
|
||||||
hide_link_definition(editor, cx);
|
hide_link_definition(editor, cx);
|
||||||
}
|
}
|
||||||
|
|
||||||
if editor.pending_rename.is_some() {
|
if editor.has_pending_rename(cx) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue