Merge pull request #1144 from zed-industries/hover-fixes
Hover fixes. Addresses delay issues with current hover implementation, shrinks the hover popover, and tweaks the display of markdown data
This commit is contained in:
@@ -16,6 +16,7 @@ test-support = [
|
||||
"project/test-support",
|
||||
"util/test-support",
|
||||
"workspace/test-support",
|
||||
"tree-sitter-rust"
|
||||
]
|
||||
|
||||
[dependencies]
|
||||
@@ -48,6 +49,7 @@ rand = { version = "0.8.3", optional = true }
|
||||
serde = { version = "1.0", features = ["derive", "rc"] }
|
||||
smallvec = { version = "1.6", features = ["union"] }
|
||||
smol = "1.2"
|
||||
tree-sitter-rust = { version = "*", optional = true }
|
||||
|
||||
[dev-dependencies]
|
||||
text = { path = "../text", features = ["test-support"] }
|
||||
|
||||
@@ -193,9 +193,9 @@ impl DisplayMap {
|
||||
self.text_highlights.remove(&Some(type_id))
|
||||
}
|
||||
|
||||
pub fn set_font(&self, font_id: FontId, font_size: f32, cx: &mut ModelContext<Self>) {
|
||||
pub fn set_font(&self, font_id: FontId, font_size: f32, cx: &mut ModelContext<Self>) -> bool {
|
||||
self.wrap_map
|
||||
.update(cx, |map, cx| map.set_font(font_id, font_size, cx));
|
||||
.update(cx, |map, cx| map.set_font(font_id, font_size, cx))
|
||||
}
|
||||
|
||||
pub fn set_wrap_width(&self, width: Option<f32>, cx: &mut ModelContext<Self>) -> bool {
|
||||
|
||||
@@ -121,10 +121,18 @@ impl WrapMap {
|
||||
(self.snapshot.clone(), mem::take(&mut self.edits_since_sync))
|
||||
}
|
||||
|
||||
pub fn set_font(&mut self, font_id: FontId, font_size: f32, cx: &mut ModelContext<Self>) {
|
||||
pub fn set_font(
|
||||
&mut self,
|
||||
font_id: FontId,
|
||||
font_size: f32,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) -> bool {
|
||||
if (font_id, font_size) != self.font {
|
||||
self.font = (font_id, font_size);
|
||||
self.rewrap(cx)
|
||||
self.rewrap(cx);
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+223
-301
@@ -1,5 +1,6 @@
|
||||
pub mod display_map;
|
||||
mod element;
|
||||
mod hover_popover;
|
||||
pub mod items;
|
||||
pub mod movement;
|
||||
mod multi_buffer;
|
||||
@@ -25,10 +26,11 @@ use gpui::{
|
||||
geometry::vector::{vec2f, Vector2F},
|
||||
impl_actions, impl_internal_actions,
|
||||
platform::CursorStyle,
|
||||
text_layout, AppContext, AsyncAppContext, Axis, ClipboardItem, Element, ElementBox, Entity,
|
||||
text_layout, AppContext, AsyncAppContext, ClipboardItem, Element, ElementBox, Entity,
|
||||
ModelHandle, MutableAppContext, RenderContext, Task, View, ViewContext, ViewHandle,
|
||||
WeakViewHandle,
|
||||
};
|
||||
use hover_popover::{hide_hover, HoverState};
|
||||
pub use language::{char_kind, CharKind};
|
||||
use language::{
|
||||
BracketPair, Buffer, CodeAction, CodeLabel, Completion, Diagnostic, DiagnosticSeverity,
|
||||
@@ -41,7 +43,7 @@ pub use multi_buffer::{
|
||||
ToPoint,
|
||||
};
|
||||
use ordered_float::OrderedFloat;
|
||||
use project::{HoverBlock, Project, ProjectPath, ProjectTransaction};
|
||||
use project::{Project, ProjectPath, ProjectTransaction};
|
||||
use selections_collection::{resolve_multiple, MutableSelectionsCollection, SelectionsCollection};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use settings::Settings;
|
||||
@@ -82,11 +84,6 @@ pub struct Scroll(pub Vector2F);
|
||||
#[derive(Clone, PartialEq)]
|
||||
pub struct Select(pub SelectPhase);
|
||||
|
||||
#[derive(Clone, PartialEq)]
|
||||
pub struct HoverAt {
|
||||
point: Option<DisplayPoint>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct Jump {
|
||||
path: ProjectPath,
|
||||
@@ -127,11 +124,6 @@ pub struct ConfirmCodeAction {
|
||||
pub item_ix: Option<usize>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Default)]
|
||||
pub struct GoToDefinitionAt {
|
||||
pub location: Option<DisplayPoint>,
|
||||
}
|
||||
|
||||
actions!(
|
||||
editor,
|
||||
[
|
||||
@@ -224,7 +216,7 @@ impl_actions!(
|
||||
]
|
||||
);
|
||||
|
||||
impl_internal_actions!(editor, [Scroll, Select, HoverAt, Jump]);
|
||||
impl_internal_actions!(editor, [Scroll, Select, Jump]);
|
||||
|
||||
enum DocumentHighlightRead {}
|
||||
enum DocumentHighlightWrite {}
|
||||
@@ -311,8 +303,6 @@ pub fn init(cx: &mut MutableAppContext) {
|
||||
cx.add_action(Editor::fold_selected_ranges);
|
||||
cx.add_action(Editor::show_completions);
|
||||
cx.add_action(Editor::toggle_code_actions);
|
||||
cx.add_action(Editor::hover);
|
||||
cx.add_action(Editor::hover_at);
|
||||
cx.add_action(Editor::open_excerpts);
|
||||
cx.add_action(Editor::jump);
|
||||
cx.add_action(Editor::restart_language_server);
|
||||
@@ -322,6 +312,8 @@ pub fn init(cx: &mut MutableAppContext) {
|
||||
cx.add_async_action(Editor::confirm_rename);
|
||||
cx.add_async_action(Editor::find_all_references);
|
||||
|
||||
hover_popover::init(cx);
|
||||
|
||||
workspace::register_project_item::<Editor>(cx);
|
||||
workspace::register_followable_item::<Editor>(cx);
|
||||
}
|
||||
@@ -431,7 +423,6 @@ pub struct Editor {
|
||||
next_completion_id: CompletionId,
|
||||
available_code_actions: Option<(ModelHandle<Buffer>, Arc<[CodeAction]>)>,
|
||||
code_actions_task: Option<Task<()>>,
|
||||
hover_task: Option<Task<Option<()>>>,
|
||||
document_highlights_task: Option<Task<()>>,
|
||||
pending_rename: Option<RenameState>,
|
||||
searchable: bool,
|
||||
@@ -442,39 +433,6 @@ pub struct Editor {
|
||||
hover_state: HoverState,
|
||||
}
|
||||
|
||||
/// Keeps track of the state of the [`HoverPopover`].
|
||||
/// Times out the initial delay and the grace period.
|
||||
pub struct HoverState {
|
||||
popover: Option<HoverPopover>,
|
||||
last_hover: std::time::Instant,
|
||||
start_grace: std::time::Instant,
|
||||
}
|
||||
|
||||
impl HoverState {
|
||||
/// Takes whether the cursor is currently hovering over a symbol,
|
||||
/// and returns a tuple containing whether there was a recent hover,
|
||||
/// and whether the hover is still in the grace period.
|
||||
pub fn determine_state(&mut self, hovering: bool) -> (bool, bool) {
|
||||
// NOTE: We use some sane defaults, but it might be
|
||||
// nice to make these values configurable.
|
||||
let recent_hover = self.last_hover.elapsed() < std::time::Duration::from_millis(500);
|
||||
if !hovering {
|
||||
self.last_hover = std::time::Instant::now();
|
||||
}
|
||||
|
||||
let in_grace = self.start_grace.elapsed() < std::time::Duration::from_millis(250);
|
||||
if hovering && !recent_hover {
|
||||
self.start_grace = std::time::Instant::now();
|
||||
}
|
||||
|
||||
return (recent_hover, in_grace);
|
||||
}
|
||||
|
||||
pub fn close(&mut self) {
|
||||
self.popover.take();
|
||||
}
|
||||
}
|
||||
|
||||
pub struct EditorSnapshot {
|
||||
pub mode: EditorMode,
|
||||
pub display_snapshot: DisplaySnapshot,
|
||||
@@ -899,67 +857,6 @@ impl CodeActionsMenu {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub(crate) struct HoverPopover {
|
||||
pub project: ModelHandle<Project>,
|
||||
pub hover_point: DisplayPoint,
|
||||
pub range: Range<DisplayPoint>,
|
||||
pub contents: Vec<HoverBlock>,
|
||||
}
|
||||
|
||||
impl HoverPopover {
|
||||
fn render(
|
||||
&self,
|
||||
style: EditorStyle,
|
||||
cx: &mut RenderContext<Editor>,
|
||||
) -> (DisplayPoint, ElementBox) {
|
||||
let element = MouseEventHandler::new::<HoverPopover, _, _>(0, cx, |_, cx| {
|
||||
let mut flex = Flex::new(Axis::Vertical).scrollable::<HoverBlock, _>(1, None, cx);
|
||||
flex.extend(self.contents.iter().map(|content| {
|
||||
let project = self.project.read(cx);
|
||||
if let Some(language) = content
|
||||
.language
|
||||
.clone()
|
||||
.and_then(|language| project.languages().get_language(&language))
|
||||
{
|
||||
let runs = language
|
||||
.highlight_text(&content.text.as_str().into(), 0..content.text.len());
|
||||
|
||||
Text::new(content.text.clone(), style.text.clone())
|
||||
.with_soft_wrap(true)
|
||||
.with_highlights(
|
||||
runs.iter()
|
||||
.filter_map(|(range, id)| {
|
||||
id.style(style.theme.syntax.as_ref())
|
||||
.map(|style| (range.clone(), style))
|
||||
})
|
||||
.collect(),
|
||||
)
|
||||
.boxed()
|
||||
} else {
|
||||
Text::new(content.text.clone(), style.hover_popover.prose.clone())
|
||||
.with_soft_wrap(true)
|
||||
.contained()
|
||||
.with_style(style.hover_popover.block_style)
|
||||
.boxed()
|
||||
}
|
||||
}));
|
||||
flex.contained()
|
||||
.with_style(style.hover_popover.container)
|
||||
.boxed()
|
||||
})
|
||||
.with_cursor_style(CursorStyle::Arrow)
|
||||
.with_padding(Padding {
|
||||
bottom: 5.,
|
||||
top: 5.,
|
||||
..Default::default()
|
||||
})
|
||||
.boxed();
|
||||
|
||||
(self.range.start, element)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct ActiveDiagnosticGroup {
|
||||
primary_range: Range<Anchor>,
|
||||
@@ -1117,7 +1014,7 @@ impl Editor {
|
||||
next_completion_id: 0,
|
||||
available_code_actions: Default::default(),
|
||||
code_actions_task: Default::default(),
|
||||
hover_task: Default::default(),
|
||||
|
||||
document_highlights_task: Default::default(),
|
||||
pending_rename: Default::default(),
|
||||
searchable: true,
|
||||
@@ -1126,11 +1023,7 @@ impl Editor {
|
||||
keymap_context_layers: Default::default(),
|
||||
input_enabled: true,
|
||||
leader_replica_id: None,
|
||||
hover_state: HoverState {
|
||||
popover: None,
|
||||
last_hover: std::time::Instant::now(),
|
||||
start_grace: std::time::Instant::now(),
|
||||
},
|
||||
hover_state: Default::default(),
|
||||
};
|
||||
this.end_selection(cx);
|
||||
|
||||
@@ -1253,6 +1146,8 @@ impl Editor {
|
||||
}
|
||||
|
||||
self.autoscroll_request.take();
|
||||
hide_hover(self, cx);
|
||||
|
||||
cx.emit(Event::ScrollPositionChanged { local });
|
||||
cx.notify();
|
||||
}
|
||||
@@ -1516,7 +1411,7 @@ impl Editor {
|
||||
}
|
||||
}
|
||||
|
||||
self.hide_hover(cx);
|
||||
hide_hover(self, cx);
|
||||
|
||||
if old_cursor_position.to_display_point(&display_map).row()
|
||||
!= new_cursor_position.to_display_point(&display_map).row()
|
||||
@@ -1879,7 +1774,7 @@ impl Editor {
|
||||
return;
|
||||
}
|
||||
|
||||
if self.hide_hover(cx) {
|
||||
if hide_hover(self, cx) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -2510,179 +2405,6 @@ impl Editor {
|
||||
}))
|
||||
}
|
||||
|
||||
/// Bindable action which uses the most recent selection head to trigger a hover
|
||||
fn hover(&mut self, _: &Hover, cx: &mut ViewContext<Self>) {
|
||||
let head = self.selections.newest_display(cx).head();
|
||||
self.show_hover(head, true, cx);
|
||||
}
|
||||
|
||||
/// The internal hover action dispatches between `show_hover` or `hide_hover`
|
||||
/// depending on whether a point to hover over is provided.
|
||||
fn hover_at(&mut self, action: &HoverAt, cx: &mut ViewContext<Self>) {
|
||||
if let Some(point) = action.point {
|
||||
self.show_hover(point, false, cx);
|
||||
} else {
|
||||
self.hide_hover(cx);
|
||||
}
|
||||
}
|
||||
|
||||
/// Hides the type information popup.
|
||||
/// Triggered by the `Hover` action when the cursor is not over a symbol or when the
|
||||
/// selecitons changed.
|
||||
fn hide_hover(&mut self, cx: &mut ViewContext<Self>) -> bool {
|
||||
// consistently keep track of state to make handoff smooth
|
||||
self.hover_state.determine_state(false);
|
||||
|
||||
let mut did_hide = false;
|
||||
|
||||
// only notify the context once
|
||||
if self.hover_state.popover.is_some() {
|
||||
self.hover_state.popover = None;
|
||||
did_hide = true;
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
self.clear_background_highlights::<HoverState>(cx);
|
||||
|
||||
self.hover_task = None;
|
||||
|
||||
did_hide
|
||||
}
|
||||
|
||||
/// Queries the LSP and shows type info and documentation
|
||||
/// about the symbol the mouse is currently hovering over.
|
||||
/// Triggered by the `Hover` action when the cursor may be over a symbol.
|
||||
fn show_hover(
|
||||
&mut self,
|
||||
point: DisplayPoint,
|
||||
ignore_timeout: bool,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
if self.pending_rename.is_some() {
|
||||
return;
|
||||
}
|
||||
|
||||
if let Some(hover) = &self.hover_state.popover {
|
||||
if hover.hover_point == point {
|
||||
// Hover triggered from same location as last time. Don't show again.
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
let snapshot = self.snapshot(cx);
|
||||
let (buffer, buffer_position) = if let Some(output) = self
|
||||
.buffer
|
||||
.read(cx)
|
||||
.text_anchor_for_position(point.to_point(&snapshot.display_snapshot), cx)
|
||||
{
|
||||
output
|
||||
} else {
|
||||
return;
|
||||
};
|
||||
|
||||
let project = if let Some(project) = self.project.clone() {
|
||||
project
|
||||
} else {
|
||||
return;
|
||||
};
|
||||
|
||||
// query the LSP for hover info
|
||||
let hover_request = project.update(cx, |project, cx| {
|
||||
project.hover(&buffer, buffer_position.clone(), cx)
|
||||
});
|
||||
|
||||
let buffer_snapshot = buffer.read(cx).snapshot();
|
||||
|
||||
let task = cx.spawn_weak(|this, mut cx| {
|
||||
async move {
|
||||
// Construct new hover popover from hover request
|
||||
let hover_popover = hover_request.await.ok().flatten().and_then(|hover_result| {
|
||||
if hover_result.contents.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let range = if let Some(range) = hover_result.range {
|
||||
let offset_range = range.to_offset(&buffer_snapshot);
|
||||
if !offset_range
|
||||
.contains(&point.to_offset(&snapshot.display_snapshot, Bias::Left))
|
||||
{
|
||||
return None;
|
||||
}
|
||||
|
||||
offset_range
|
||||
.start
|
||||
.to_display_point(&snapshot.display_snapshot)
|
||||
..offset_range
|
||||
.end
|
||||
.to_display_point(&snapshot.display_snapshot)
|
||||
} else {
|
||||
point..point
|
||||
};
|
||||
|
||||
Some(HoverPopover {
|
||||
project: project.clone(),
|
||||
hover_point: point,
|
||||
range,
|
||||
contents: hover_result.contents,
|
||||
})
|
||||
});
|
||||
|
||||
if let Some(this) = this.upgrade(&cx) {
|
||||
this.update(&mut cx, |this, cx| {
|
||||
// this was trickier than expected, trying to do a couple things:
|
||||
//
|
||||
// 1. if you hover over a symbol, there should be a slight delay
|
||||
// before the popover shows
|
||||
// 2. if you move to another symbol when the popover is showing,
|
||||
// the popover should switch right away, and you should
|
||||
// not have to wait for it to come up again
|
||||
let (recent_hover, in_grace) =
|
||||
this.hover_state.determine_state(hover_popover.is_some());
|
||||
let smooth_handoff =
|
||||
this.hover_state.popover.is_some() && hover_popover.is_some();
|
||||
let visible = this.hover_state.popover.is_some() || hover_popover.is_some();
|
||||
|
||||
// `smooth_handoff` and `in_grace` determine whether to switch right away.
|
||||
// `recent_hover` will activate the handoff after the initial delay.
|
||||
// `ignore_timeout` is set when the user manually sent the hover action.
|
||||
if (ignore_timeout || smooth_handoff || !recent_hover || in_grace)
|
||||
&& visible
|
||||
{
|
||||
// Highlight the selected symbol using a background highlight
|
||||
if let Some(display_range) =
|
||||
hover_popover.as_ref().map(|popover| popover.range.clone())
|
||||
{
|
||||
let start = snapshot.display_snapshot.buffer_snapshot.anchor_after(
|
||||
display_range
|
||||
.start
|
||||
.to_offset(&snapshot.display_snapshot, Bias::Right),
|
||||
);
|
||||
let end = snapshot.display_snapshot.buffer_snapshot.anchor_before(
|
||||
display_range
|
||||
.end
|
||||
.to_offset(&snapshot.display_snapshot, Bias::Left),
|
||||
);
|
||||
|
||||
this.highlight_background::<HoverState>(
|
||||
vec![start..end],
|
||||
|theme| theme.editor.hover_popover.highlight,
|
||||
cx,
|
||||
);
|
||||
}
|
||||
|
||||
this.hover_state.popover = hover_popover;
|
||||
cx.notify();
|
||||
}
|
||||
});
|
||||
}
|
||||
Ok::<_, anyhow::Error>(())
|
||||
}
|
||||
.log_err()
|
||||
});
|
||||
|
||||
self.hover_task = Some(task);
|
||||
}
|
||||
|
||||
async fn open_project_transaction(
|
||||
this: ViewHandle<Editor>,
|
||||
workspace: ViewHandle<Workspace>,
|
||||
@@ -2708,7 +2430,7 @@ impl Editor {
|
||||
.read(cx)
|
||||
.excerpt_containing(editor.selections.newest_anchor().head(), cx)
|
||||
});
|
||||
if let Some((excerpted_buffer, excerpt_range)) = excerpt {
|
||||
if let Some((_, excerpted_buffer, excerpt_range)) = excerpt {
|
||||
if excerpted_buffer == *buffer {
|
||||
let snapshot = buffer.read_with(&cx, |buffer, _| buffer.snapshot());
|
||||
let excerpt_range = excerpt_range.to_offset(&snapshot);
|
||||
@@ -2929,10 +2651,6 @@ impl Editor {
|
||||
.map(|menu| menu.render(cursor_position, style, cx))
|
||||
}
|
||||
|
||||
pub(crate) fn hover_popover(&self) -> Option<HoverPopover> {
|
||||
self.hover_state.popover.clone()
|
||||
}
|
||||
|
||||
fn show_context_menu(&mut self, menu: ContextMenu, cx: &mut ViewContext<Self>) {
|
||||
if !matches!(menu, ContextMenu::Completions(_)) {
|
||||
self.completion_tasks.clear();
|
||||
@@ -5970,9 +5688,22 @@ impl Entity for Editor {
|
||||
impl View for Editor {
|
||||
fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
|
||||
let style = self.style(cx);
|
||||
self.display_map.update(cx, |map, cx| {
|
||||
let font_changed = self.display_map.update(cx, |map, cx| {
|
||||
map.set_font(style.text.font_id, style.text.font_size, cx)
|
||||
});
|
||||
|
||||
// If the
|
||||
if font_changed {
|
||||
let handle = self.handle.clone();
|
||||
cx.defer(move |cx| {
|
||||
if let Some(editor) = handle.upgrade(cx) {
|
||||
editor.update(cx, |editor, cx| {
|
||||
hide_hover(editor, cx);
|
||||
})
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
EditorElement::new(self.handle.clone(), style.clone(), self.cursor_shape).boxed()
|
||||
}
|
||||
|
||||
@@ -6416,11 +6147,16 @@ pub fn styled_runs_for_code_label<'a>(
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::test::{
|
||||
assert_text_with_selections, build_editor, select_ranges, EditorTestContext,
|
||||
use crate::{
|
||||
hover_popover::{hover, hover_at, HoverAt, HOVER_DELAY_MILLIS, HOVER_GRACE_MILLIS},
|
||||
test::{
|
||||
assert_text_with_selections, build_editor, select_ranges, EditorLspTestContext,
|
||||
EditorTestContext,
|
||||
},
|
||||
};
|
||||
|
||||
use super::*;
|
||||
use futures::StreamExt;
|
||||
use gpui::{
|
||||
geometry::rect::RectF,
|
||||
platform::{WindowBounds, WindowOptions},
|
||||
@@ -6428,9 +6164,8 @@ mod tests {
|
||||
use indoc::indoc;
|
||||
use language::{FakeLspAdapter, LanguageConfig};
|
||||
use lsp::FakeLanguageServer;
|
||||
use project::FakeFs;
|
||||
use project::{FakeFs, HoverBlock};
|
||||
use settings::LanguageOverride;
|
||||
use smol::stream::StreamExt;
|
||||
use std::{cell::RefCell, rc::Rc, time::Instant};
|
||||
use text::Point;
|
||||
use unindent::Unindent;
|
||||
@@ -9660,6 +9395,193 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_hover_popover(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = EditorLspTestContext::new_rust(
|
||||
lsp::ServerCapabilities {
|
||||
hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
|
||||
..Default::default()
|
||||
},
|
||||
cx,
|
||||
)
|
||||
.await;
|
||||
|
||||
// Basic hover delays and then pops without moving the mouse
|
||||
cx.set_state(indoc! {"
|
||||
fn |test()
|
||||
println!();"});
|
||||
let hover_point = cx.display_point(indoc! {"
|
||||
fn test()
|
||||
print|ln!();"});
|
||||
|
||||
cx.update_editor(|editor, cx| {
|
||||
hover_at(
|
||||
editor,
|
||||
&HoverAt {
|
||||
point: Some(hover_point),
|
||||
},
|
||||
cx,
|
||||
)
|
||||
});
|
||||
assert!(!cx.editor(|editor, _| editor.hover_state.visible()));
|
||||
|
||||
// After delay, hover should be visible.
|
||||
let symbol_range = cx.lsp_range(indoc! {"
|
||||
fn test()
|
||||
[println!]();"});
|
||||
let mut requests =
|
||||
cx.lsp
|
||||
.handle_request::<lsp::request::HoverRequest, _, _>(move |_, _| async move {
|
||||
Ok(Some(lsp::Hover {
|
||||
contents: lsp::HoverContents::Markup(lsp::MarkupContent {
|
||||
kind: lsp::MarkupKind::Markdown,
|
||||
value: indoc! {"
|
||||
# Some basic docs
|
||||
Some test documentation"}
|
||||
.to_string(),
|
||||
}),
|
||||
range: Some(symbol_range),
|
||||
}))
|
||||
});
|
||||
cx.foreground()
|
||||
.advance_clock(Duration::from_millis(HOVER_DELAY_MILLIS + 100));
|
||||
requests.next().await;
|
||||
|
||||
cx.editor(|editor, _| {
|
||||
assert!(editor.hover_state.visible());
|
||||
assert_eq!(
|
||||
editor.hover_state.popover.clone().unwrap().contents,
|
||||
vec![
|
||||
HoverBlock {
|
||||
text: "Some basic docs".to_string(),
|
||||
language: None
|
||||
},
|
||||
HoverBlock {
|
||||
text: "Some test documentation".to_string(),
|
||||
language: None
|
||||
}
|
||||
]
|
||||
)
|
||||
});
|
||||
|
||||
// Mouse moved with no hover response dismisses
|
||||
let hover_point = cx.display_point(indoc! {"
|
||||
fn te|st()
|
||||
println!();"});
|
||||
cx.update_editor(|editor, cx| {
|
||||
hover_at(
|
||||
editor,
|
||||
&HoverAt {
|
||||
point: Some(hover_point),
|
||||
},
|
||||
cx,
|
||||
)
|
||||
});
|
||||
cx.lsp
|
||||
.handle_request::<lsp::request::HoverRequest, _, _>(|_, _| async move { Ok(None) })
|
||||
.next()
|
||||
.await;
|
||||
cx.foreground().run_until_parked();
|
||||
cx.editor(|editor, _| {
|
||||
assert!(!editor.hover_state.visible());
|
||||
});
|
||||
cx.foreground()
|
||||
.advance_clock(Duration::from_millis(HOVER_GRACE_MILLIS + 100));
|
||||
|
||||
// Hover with keyboard has no delay
|
||||
cx.set_state(indoc! {"
|
||||
f|n test()
|
||||
println!();"});
|
||||
cx.update_editor(|editor, cx| hover(editor, &hover_popover::Hover, cx));
|
||||
let symbol_range = cx.lsp_range(indoc! {"
|
||||
[fn] test()
|
||||
println!();"});
|
||||
cx.lsp
|
||||
.handle_request::<lsp::request::HoverRequest, _, _>(move |_, _| async move {
|
||||
Ok(Some(lsp::Hover {
|
||||
contents: lsp::HoverContents::Markup(lsp::MarkupContent {
|
||||
kind: lsp::MarkupKind::Markdown,
|
||||
value: indoc! {"
|
||||
# Some other basic docs
|
||||
Some other test documentation"}
|
||||
.to_string(),
|
||||
}),
|
||||
range: Some(symbol_range),
|
||||
}))
|
||||
})
|
||||
.next()
|
||||
.await;
|
||||
cx.foreground().run_until_parked();
|
||||
cx.editor(|editor, _| {
|
||||
assert!(editor.hover_state.visible());
|
||||
assert_eq!(
|
||||
editor.hover_state.popover.clone().unwrap().contents,
|
||||
vec![
|
||||
HoverBlock {
|
||||
text: "Some other basic docs".to_string(),
|
||||
language: None
|
||||
},
|
||||
HoverBlock {
|
||||
text: "Some other test documentation".to_string(),
|
||||
language: None
|
||||
}
|
||||
]
|
||||
)
|
||||
});
|
||||
|
||||
// Open hover popover disables delay
|
||||
let hover_point = cx.display_point(indoc! {"
|
||||
fn test()
|
||||
print|ln!();"});
|
||||
cx.update_editor(|editor, cx| {
|
||||
hover_at(
|
||||
editor,
|
||||
&HoverAt {
|
||||
point: Some(hover_point),
|
||||
},
|
||||
cx,
|
||||
)
|
||||
});
|
||||
|
||||
let symbol_range = cx.lsp_range(indoc! {"
|
||||
fn test()
|
||||
[println!]();"});
|
||||
cx.lsp
|
||||
.handle_request::<lsp::request::HoverRequest, _, _>(move |_, _| async move {
|
||||
Ok(Some(lsp::Hover {
|
||||
contents: lsp::HoverContents::Markup(lsp::MarkupContent {
|
||||
kind: lsp::MarkupKind::Markdown,
|
||||
value: indoc! {"
|
||||
# Some third basic docs
|
||||
Some third test documentation"}
|
||||
.to_string(),
|
||||
}),
|
||||
range: Some(symbol_range),
|
||||
}))
|
||||
})
|
||||
.next()
|
||||
.await;
|
||||
cx.foreground().run_until_parked();
|
||||
// No delay as the popover is already visible
|
||||
|
||||
cx.editor(|editor, _| {
|
||||
assert!(editor.hover_state.visible());
|
||||
assert_eq!(
|
||||
editor.hover_state.popover.clone().unwrap().contents,
|
||||
vec![
|
||||
HoverBlock {
|
||||
text: "Some third basic docs".to_string(),
|
||||
language: None
|
||||
},
|
||||
HoverBlock {
|
||||
text: "Some third test documentation".to_string(),
|
||||
language: None
|
||||
}
|
||||
]
|
||||
)
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_toggle_comment(cx: &mut gpui::TestAppContext) {
|
||||
cx.update(|cx| cx.set_global(Settings::test(cx)));
|
||||
|
||||
@@ -3,9 +3,10 @@ use super::{
|
||||
Anchor, DisplayPoint, Editor, EditorMode, EditorSnapshot, Input, Scroll, Select, SelectPhase,
|
||||
SoftWrap, ToPoint, MAX_LINE_LEN,
|
||||
};
|
||||
use crate::hover_popover::HoverAt;
|
||||
use crate::{
|
||||
display_map::{DisplaySnapshot, TransformBlock},
|
||||
EditorStyle, HoverAt,
|
||||
EditorStyle,
|
||||
};
|
||||
use clock::ReplicaId;
|
||||
use collections::{BTreeMap, HashMap};
|
||||
@@ -1196,8 +1197,8 @@ impl Element for EditorElement {
|
||||
.map(|indicator| (newest_selection_head.row(), indicator));
|
||||
}
|
||||
|
||||
hover = view.hover_popover().and_then(|hover| {
|
||||
let (point, rendered) = hover.render(style.clone(), cx);
|
||||
hover = view.hover_state.popover.clone().and_then(|hover| {
|
||||
let (point, rendered) = hover.render(&snapshot, style.clone(), cx);
|
||||
if point.row() >= snapshot.scroll_position().y() as u32 {
|
||||
if line_layouts.len() > (point.row() - start_row) as usize {
|
||||
return Some((point, rendered));
|
||||
@@ -1233,8 +1234,12 @@ impl Element for EditorElement {
|
||||
SizeConstraint {
|
||||
min: Vector2F::zero(),
|
||||
max: vec2f(
|
||||
(120. * em_width).min(size.x()),
|
||||
(size.y() - line_height) * 1. / 2.,
|
||||
(120. * em_width) // Default size
|
||||
.min(size.x() / 2.) // Shrink to half of the editor width
|
||||
.max(20. * em_width), // Apply minimum width of 20 characters
|
||||
(16. * line_height) // Default size
|
||||
.min(size.y() / 2.) // Shrink to half of the editor height
|
||||
.max(4. * line_height), // Apply minimum height of 4 lines
|
||||
),
|
||||
},
|
||||
cx,
|
||||
|
||||
@@ -0,0 +1,329 @@
|
||||
use std::{
|
||||
ops::Range,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
|
||||
use gpui::{
|
||||
actions,
|
||||
elements::{Flex, MouseEventHandler, Padding, Text},
|
||||
impl_internal_actions,
|
||||
platform::CursorStyle,
|
||||
Axis, Element, ElementBox, ModelHandle, MutableAppContext, RenderContext, Task, ViewContext,
|
||||
};
|
||||
use language::Bias;
|
||||
use project::{HoverBlock, Project};
|
||||
use util::TryFutureExt;
|
||||
|
||||
use crate::{
|
||||
display_map::ToDisplayPoint, Anchor, AnchorRangeExt, DisplayPoint, Editor, EditorSnapshot,
|
||||
EditorStyle,
|
||||
};
|
||||
|
||||
pub const HOVER_DELAY_MILLIS: u64 = 500;
|
||||
pub const HOVER_REQUEST_DELAY_MILLIS: u64 = 250;
|
||||
pub const HOVER_GRACE_MILLIS: u64 = 250;
|
||||
|
||||
#[derive(Clone, PartialEq)]
|
||||
pub struct HoverAt {
|
||||
pub point: Option<DisplayPoint>,
|
||||
}
|
||||
|
||||
actions!(editor, [Hover]);
|
||||
impl_internal_actions!(editor, [HoverAt]);
|
||||
|
||||
pub fn init(cx: &mut MutableAppContext) {
|
||||
cx.add_action(hover);
|
||||
cx.add_action(hover_at);
|
||||
}
|
||||
|
||||
/// Bindable action which uses the most recent selection head to trigger a hover
|
||||
pub fn hover(editor: &mut Editor, _: &Hover, cx: &mut ViewContext<Editor>) {
|
||||
let head = editor.selections.newest_display(cx).head();
|
||||
show_hover(editor, head, true, cx);
|
||||
}
|
||||
|
||||
/// The internal hover action dispatches between `show_hover` or `hide_hover`
|
||||
/// depending on whether a point to hover over is provided.
|
||||
pub fn hover_at(editor: &mut Editor, action: &HoverAt, cx: &mut ViewContext<Editor>) {
|
||||
if let Some(point) = action.point {
|
||||
show_hover(editor, point, false, cx);
|
||||
} else {
|
||||
hide_hover(editor, cx);
|
||||
}
|
||||
}
|
||||
|
||||
/// Hides the type information popup.
|
||||
/// Triggered by the `Hover` action when the cursor is not over a symbol or when the
|
||||
/// selections changed.
|
||||
pub fn hide_hover(editor: &mut Editor, cx: &mut ViewContext<Editor>) -> bool {
|
||||
let mut did_hide = false;
|
||||
|
||||
// only notify the context once
|
||||
if editor.hover_state.popover.is_some() {
|
||||
editor.hover_state.popover = None;
|
||||
editor.hover_state.hidden_at = Some(cx.background().now());
|
||||
did_hide = true;
|
||||
cx.notify();
|
||||
}
|
||||
editor.hover_state.task = None;
|
||||
editor.hover_state.triggered_from = None;
|
||||
editor.hover_state.symbol_range = None;
|
||||
|
||||
editor.clear_background_highlights::<HoverState>(cx);
|
||||
|
||||
did_hide
|
||||
}
|
||||
|
||||
/// Queries the LSP and shows type info and documentation
|
||||
/// about the symbol the mouse is currently hovering over.
|
||||
/// Triggered by the `Hover` action when the cursor may be over a symbol.
|
||||
fn show_hover(
|
||||
editor: &mut Editor,
|
||||
point: DisplayPoint,
|
||||
ignore_timeout: bool,
|
||||
cx: &mut ViewContext<Editor>,
|
||||
) {
|
||||
if editor.pending_rename.is_some() {
|
||||
return;
|
||||
}
|
||||
|
||||
let snapshot = editor.snapshot(cx);
|
||||
let multibuffer_offset = point.to_offset(&snapshot.display_snapshot, Bias::Left);
|
||||
|
||||
let (buffer, buffer_position) = if let Some(output) = editor
|
||||
.buffer
|
||||
.read(cx)
|
||||
.text_anchor_for_position(multibuffer_offset, cx)
|
||||
{
|
||||
output
|
||||
} else {
|
||||
return;
|
||||
};
|
||||
|
||||
let excerpt_id = if let Some((excerpt_id, _, _)) = editor
|
||||
.buffer()
|
||||
.read(cx)
|
||||
.excerpt_containing(multibuffer_offset, cx)
|
||||
{
|
||||
excerpt_id
|
||||
} else {
|
||||
return;
|
||||
};
|
||||
|
||||
let project = if let Some(project) = editor.project.clone() {
|
||||
project
|
||||
} else {
|
||||
return;
|
||||
};
|
||||
|
||||
// We should only delay if the hover popover isn't visible, it wasn't recently hidden, and
|
||||
// the hover wasn't triggered from the keyboard
|
||||
let should_delay = editor.hover_state.popover.is_none() // Hover not visible currently
|
||||
&& editor
|
||||
.hover_state
|
||||
.hidden_at
|
||||
.map(|hidden| hidden.elapsed().as_millis() > HOVER_GRACE_MILLIS as u128)
|
||||
.unwrap_or(true) // Hover wasn't recently visible
|
||||
&& !ignore_timeout; // Hover was not triggered from keyboard
|
||||
|
||||
if should_delay {
|
||||
if let Some(range) = &editor.hover_state.symbol_range {
|
||||
if range
|
||||
.to_offset(&snapshot.buffer_snapshot)
|
||||
.contains(&multibuffer_offset)
|
||||
{
|
||||
// Hover triggered from same location as last time. Don't show again.
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Get input anchor
|
||||
let anchor = snapshot
|
||||
.buffer_snapshot
|
||||
.anchor_at(multibuffer_offset, Bias::Left);
|
||||
|
||||
// Don't request again if the location is the same as the previous request
|
||||
if let Some(triggered_from) = &editor.hover_state.triggered_from {
|
||||
if triggered_from
|
||||
.cmp(&anchor, &snapshot.buffer_snapshot)
|
||||
.is_eq()
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
let task = cx.spawn_weak(|this, mut cx| {
|
||||
async move {
|
||||
// If we need to delay, delay a set amount initially before making the lsp request
|
||||
let delay = if should_delay {
|
||||
// Construct delay task to wait for later
|
||||
let total_delay = Some(
|
||||
cx.background()
|
||||
.timer(Duration::from_millis(HOVER_DELAY_MILLIS)),
|
||||
);
|
||||
|
||||
cx.background()
|
||||
.timer(Duration::from_millis(HOVER_REQUEST_DELAY_MILLIS))
|
||||
.await;
|
||||
total_delay
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
// query the LSP for hover info
|
||||
let hover_request = cx.update(|cx| {
|
||||
project.update(cx, |project, cx| {
|
||||
project.hover(&buffer, buffer_position.clone(), cx)
|
||||
})
|
||||
});
|
||||
|
||||
// Construct new hover popover from hover request
|
||||
let hover_popover = hover_request.await.ok().flatten().and_then(|hover_result| {
|
||||
if hover_result.contents.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
// 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.clone()..anchor.clone()
|
||||
};
|
||||
|
||||
if let Some(this) = this.upgrade(&cx) {
|
||||
this.update(&mut cx, |this, _| {
|
||||
this.hover_state.symbol_range = Some(range.clone());
|
||||
});
|
||||
}
|
||||
|
||||
Some(HoverPopover {
|
||||
project: project.clone(),
|
||||
anchor: range.start.clone(),
|
||||
contents: hover_result.contents,
|
||||
})
|
||||
});
|
||||
|
||||
if let Some(delay) = delay {
|
||||
delay.await;
|
||||
}
|
||||
|
||||
if let Some(this) = this.upgrade(&cx) {
|
||||
this.update(&mut cx, |this, cx| {
|
||||
if hover_popover.is_some() {
|
||||
// Highlight the selected symbol using a background highlight
|
||||
if let Some(range) = this.hover_state.symbol_range.clone() {
|
||||
this.highlight_background::<HoverState>(
|
||||
vec![range],
|
||||
|theme| theme.editor.hover_popover.highlight,
|
||||
cx,
|
||||
);
|
||||
}
|
||||
this.hover_state.popover = hover_popover;
|
||||
cx.notify();
|
||||
} else {
|
||||
if this.hover_state.visible() {
|
||||
// Popover was visible, but now is hidden. Dismiss it
|
||||
hide_hover(this, cx);
|
||||
} else {
|
||||
// Clear selected symbol range for future requests
|
||||
this.hover_state.symbol_range = None;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
Ok::<_, anyhow::Error>(())
|
||||
}
|
||||
.log_err()
|
||||
});
|
||||
|
||||
editor.hover_state.task = Some(task);
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct HoverState {
|
||||
pub popover: Option<HoverPopover>,
|
||||
pub hidden_at: Option<Instant>,
|
||||
pub triggered_from: Option<Anchor>,
|
||||
pub symbol_range: Option<Range<Anchor>>,
|
||||
pub task: Option<Task<Option<()>>>,
|
||||
}
|
||||
|
||||
impl HoverState {
|
||||
pub fn visible(&self) -> bool {
|
||||
self.popover.is_some()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct HoverPopover {
|
||||
pub project: ModelHandle<Project>,
|
||||
pub anchor: Anchor,
|
||||
pub contents: Vec<HoverBlock>,
|
||||
}
|
||||
|
||||
impl HoverPopover {
|
||||
pub fn render(
|
||||
&self,
|
||||
snapshot: &EditorSnapshot,
|
||||
style: EditorStyle,
|
||||
cx: &mut RenderContext<Editor>,
|
||||
) -> (DisplayPoint, ElementBox) {
|
||||
let element = MouseEventHandler::new::<HoverPopover, _, _>(0, cx, |_, cx| {
|
||||
let mut flex = Flex::new(Axis::Vertical).scrollable::<HoverBlock, _>(1, None, cx);
|
||||
flex.extend(self.contents.iter().map(|content| {
|
||||
let project = self.project.read(cx);
|
||||
if let Some(language) = content
|
||||
.language
|
||||
.clone()
|
||||
.and_then(|language| project.languages().get_language(&language))
|
||||
{
|
||||
let runs = language
|
||||
.highlight_text(&content.text.as_str().into(), 0..content.text.len());
|
||||
|
||||
Text::new(content.text.clone(), style.text.clone())
|
||||
.with_soft_wrap(true)
|
||||
.with_highlights(
|
||||
runs.iter()
|
||||
.filter_map(|(range, id)| {
|
||||
id.style(style.theme.syntax.as_ref())
|
||||
.map(|style| (range.clone(), style))
|
||||
})
|
||||
.collect(),
|
||||
)
|
||||
.boxed()
|
||||
} else {
|
||||
let mut text_style = style.hover_popover.prose.clone();
|
||||
text_style.font_size = style.text.font_size;
|
||||
|
||||
Text::new(content.text.clone(), text_style)
|
||||
.with_soft_wrap(true)
|
||||
.contained()
|
||||
.with_style(style.hover_popover.block_style)
|
||||
.boxed()
|
||||
}
|
||||
}));
|
||||
flex.contained()
|
||||
.with_style(style.hover_popover.container)
|
||||
.boxed()
|
||||
})
|
||||
.with_cursor_style(CursorStyle::Arrow)
|
||||
.with_padding(Padding {
|
||||
bottom: 5.,
|
||||
top: 5.,
|
||||
..Default::default()
|
||||
})
|
||||
.boxed();
|
||||
|
||||
let display_point = self.anchor.to_display_point(&snapshot.display_snapshot);
|
||||
(display_point, element)
|
||||
}
|
||||
}
|
||||
@@ -937,7 +937,7 @@ impl MultiBuffer {
|
||||
&self,
|
||||
position: impl ToOffset,
|
||||
cx: &AppContext,
|
||||
) -> Option<(ModelHandle<Buffer>, Range<text::Anchor>)> {
|
||||
) -> Option<(ExcerptId, ModelHandle<Buffer>, Range<text::Anchor>)> {
|
||||
let snapshot = self.read(cx);
|
||||
let position = position.to_offset(&snapshot);
|
||||
|
||||
@@ -945,6 +945,7 @@ impl MultiBuffer {
|
||||
cursor.seek(&position, Bias::Right, &());
|
||||
cursor.item().map(|excerpt| {
|
||||
(
|
||||
excerpt.id.clone(),
|
||||
self.buffers
|
||||
.borrow()
|
||||
.get(&excerpt.buffer_id)
|
||||
|
||||
+147
-4
@@ -1,10 +1,15 @@
|
||||
use std::ops::{Deref, DerefMut, Range};
|
||||
use std::{
|
||||
ops::{Deref, DerefMut, Range},
|
||||
sync::Arc,
|
||||
};
|
||||
|
||||
use futures::StreamExt;
|
||||
use indoc::indoc;
|
||||
|
||||
use collections::BTreeMap;
|
||||
use gpui::{keymap::Keystroke, ModelHandle, ViewContext, ViewHandle};
|
||||
use language::Selection;
|
||||
use gpui::{keymap::Keystroke, AppContext, ModelHandle, ViewContext, ViewHandle};
|
||||
use language::{point_to_lsp, FakeLspAdapter, Language, LanguageConfig, Selection};
|
||||
use project::{FakeFs, Project};
|
||||
use settings::Settings;
|
||||
use util::{
|
||||
set_eq,
|
||||
@@ -13,7 +18,8 @@ use util::{
|
||||
|
||||
use crate::{
|
||||
display_map::{DisplayMap, DisplaySnapshot, ToDisplayPoint},
|
||||
Autoscroll, DisplayPoint, Editor, EditorMode, MultiBuffer,
|
||||
multi_buffer::ToPointUtf16,
|
||||
Autoscroll, DisplayPoint, Editor, EditorMode, MultiBuffer, ToPoint,
|
||||
};
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -102,6 +108,13 @@ impl<'a> EditorTestContext<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn editor<F, T>(&mut self, read: F) -> T
|
||||
where
|
||||
F: FnOnce(&Editor, &AppContext) -> T,
|
||||
{
|
||||
self.editor.read_with(self.cx, read)
|
||||
}
|
||||
|
||||
pub fn update_editor<F, T>(&mut self, update: F) -> T
|
||||
where
|
||||
F: FnOnce(&mut Editor, &mut ViewContext<Editor>) -> T,
|
||||
@@ -132,6 +145,14 @@ impl<'a> EditorTestContext<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn display_point(&mut self, cursor_location: &str) -> DisplayPoint {
|
||||
let (_, locations) = marked_text(cursor_location);
|
||||
let snapshot = self
|
||||
.editor
|
||||
.update(self.cx, |editor, cx| editor.snapshot(cx));
|
||||
locations[0].to_display_point(&snapshot.display_snapshot)
|
||||
}
|
||||
|
||||
// Sets the editor state via a marked string.
|
||||
// `|` characters represent empty selections
|
||||
// `[` to `}` represents a non empty selection with the head at `}`
|
||||
@@ -365,3 +386,125 @@ impl<'a> DerefMut for EditorTestContext<'a> {
|
||||
&mut self.cx
|
||||
}
|
||||
}
|
||||
|
||||
pub struct EditorLspTestContext<'a> {
|
||||
pub cx: EditorTestContext<'a>,
|
||||
pub lsp: lsp::FakeLanguageServer,
|
||||
}
|
||||
|
||||
impl<'a> EditorLspTestContext<'a> {
|
||||
pub async fn new(
|
||||
mut language: Language,
|
||||
capabilities: lsp::ServerCapabilities,
|
||||
cx: &'a mut gpui::TestAppContext,
|
||||
) -> EditorLspTestContext<'a> {
|
||||
let file_name = format!(
|
||||
"/file.{}",
|
||||
language
|
||||
.path_suffixes()
|
||||
.first()
|
||||
.unwrap_or(&"txt".to_string())
|
||||
);
|
||||
|
||||
let mut fake_servers = language.set_fake_lsp_adapter(FakeLspAdapter {
|
||||
capabilities,
|
||||
..Default::default()
|
||||
});
|
||||
|
||||
let fs = FakeFs::new(cx.background().clone());
|
||||
fs.insert_file(file_name.clone(), "".to_string()).await;
|
||||
|
||||
let project = Project::test(fs, [file_name.as_ref()], cx).await;
|
||||
project.update(cx, |project, _| project.languages().add(Arc::new(language)));
|
||||
let buffer = project
|
||||
.update(cx, |project, cx| project.open_local_buffer(file_name, cx))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let (window_id, editor) = cx.update(|cx| {
|
||||
cx.set_global(Settings::test(cx));
|
||||
crate::init(cx);
|
||||
|
||||
let (window_id, editor) = cx.add_window(Default::default(), |cx| {
|
||||
let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx));
|
||||
|
||||
Editor::new(EditorMode::Full, buffer, Some(project), None, None, cx)
|
||||
});
|
||||
|
||||
editor.update(cx, |_, cx| cx.focus_self());
|
||||
|
||||
(window_id, editor)
|
||||
});
|
||||
|
||||
let lsp = fake_servers.next().await.unwrap();
|
||||
|
||||
Self {
|
||||
cx: EditorTestContext {
|
||||
cx,
|
||||
window_id,
|
||||
editor,
|
||||
},
|
||||
lsp,
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn new_rust(
|
||||
capabilities: lsp::ServerCapabilities,
|
||||
cx: &'a mut gpui::TestAppContext,
|
||||
) -> EditorLspTestContext<'a> {
|
||||
let language = Language::new(
|
||||
LanguageConfig {
|
||||
name: "Rust".into(),
|
||||
path_suffixes: vec!["rs".to_string()],
|
||||
..Default::default()
|
||||
},
|
||||
Some(tree_sitter_rust::language()),
|
||||
);
|
||||
|
||||
Self::new(language, capabilities, cx).await
|
||||
}
|
||||
|
||||
// Constructs lsp range using a marked string with '[', ']' range delimiters
|
||||
pub fn lsp_range(&mut self, marked_text: &str) -> lsp::Range {
|
||||
let (unmarked, mut ranges) = marked_text_ranges_by(marked_text, vec![('[', ']').into()]);
|
||||
assert_eq!(unmarked, self.cx.buffer_text());
|
||||
let snapshot = self.update_editor(|editor, cx| editor.snapshot(cx));
|
||||
|
||||
let offset_range = ranges.remove(&('[', ']').into()).unwrap()[0].clone();
|
||||
let start_point = offset_range.start.to_point(&snapshot.buffer_snapshot);
|
||||
let end_point = offset_range.end.to_point(&snapshot.buffer_snapshot);
|
||||
self.editor(|editor, cx| {
|
||||
let buffer = editor.buffer().read(cx);
|
||||
let start = point_to_lsp(
|
||||
buffer
|
||||
.point_to_buffer_offset(start_point, cx)
|
||||
.unwrap()
|
||||
.1
|
||||
.to_point_utf16(&buffer.read(cx)),
|
||||
);
|
||||
let end = point_to_lsp(
|
||||
buffer
|
||||
.point_to_buffer_offset(end_point, cx)
|
||||
.unwrap()
|
||||
.1
|
||||
.to_point_utf16(&buffer.read(cx)),
|
||||
);
|
||||
|
||||
lsp::Range { start, end }
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Deref for EditorLspTestContext<'a> {
|
||||
type Target = EditorTestContext<'a>;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.cx
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> DerefMut for EditorLspTestContext<'a> {
|
||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||
&mut self.cx
|
||||
}
|
||||
}
|
||||
|
||||
@@ -330,6 +330,11 @@ impl Deterministic {
|
||||
Timer::Deterministic(DeterministicTimer { rx, id, state })
|
||||
}
|
||||
|
||||
pub fn now(&self) -> std::time::Instant {
|
||||
let state = self.state.lock();
|
||||
state.now.clone()
|
||||
}
|
||||
|
||||
pub fn advance_clock(&self, duration: Duration) {
|
||||
let new_now = self.state.lock().now + duration;
|
||||
loop {
|
||||
@@ -647,6 +652,14 @@ impl Background {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn now(&self) -> std::time::Instant {
|
||||
match self {
|
||||
Background::Production { .. } => std::time::Instant::now(),
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
Background::Deterministic { executor } => executor.now(),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub async fn simulate_random_delay(&self) {
|
||||
use rand::prelude::*;
|
||||
|
||||
@@ -571,6 +571,10 @@ impl Language {
|
||||
&self.config.brackets
|
||||
}
|
||||
|
||||
pub fn path_suffixes(&self) -> &[String] {
|
||||
&self.config.path_suffixes
|
||||
}
|
||||
|
||||
pub fn should_autoclose_before(&self, c: char) -> bool {
|
||||
c.is_whitespace() || self.config.autoclose_before.contains(c)
|
||||
}
|
||||
|
||||
@@ -988,7 +988,6 @@ impl LspCommand for GetHover {
|
||||
_: ModelHandle<Buffer>,
|
||||
_: AsyncAppContext,
|
||||
) -> Result<Self::Response> {
|
||||
println!("Response from proto");
|
||||
let range = if let (Some(start), Some(end)) = (message.start, message.end) {
|
||||
language::proto::deserialize_anchor(start)
|
||||
.and_then(|start| language::proto::deserialize_anchor(end).map(|end| start..end))
|
||||
|
||||
@@ -30,7 +30,7 @@ function themeTokens(theme: Theme) {
|
||||
boolean: theme.syntax.boolean.color,
|
||||
},
|
||||
player: theme.player,
|
||||
shadowAlpha: theme.shadowAlpha,
|
||||
shadow: theme.shadow,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user