mirror of
https://github.com/zed-industries/zed.git
synced 2024-12-26 10:40:54 +00:00
Merge pull request #1078 from zed-industries/lsp-hover
LSP Hover Information
This commit is contained in:
commit
9d5111e86a
27 changed files with 1854 additions and 989 deletions
1514
Cargo.lock
generated
1514
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
|
@ -99,6 +99,7 @@
|
|||
"context": "Editor && vim_operator == g",
|
||||
"bindings": {
|
||||
"g": "vim::StartOfDocument",
|
||||
"h": "editor::Hover",
|
||||
"escape": [
|
||||
"vim::SwitchMode",
|
||||
"Normal"
|
||||
|
|
|
@ -2281,6 +2281,104 @@ async fn test_document_highlights(cx_a: &mut TestAppContext, cx_b: &mut TestAppC
|
|||
});
|
||||
}
|
||||
|
||||
#[gpui::test(iterations = 10)]
|
||||
async fn test_lsp_hover(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
|
||||
cx_a.foreground().forbid_parking();
|
||||
let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
|
||||
let client_a = server.create_client(cx_a, "user_a").await;
|
||||
let client_b = server.create_client(cx_b, "user_b").await;
|
||||
server
|
||||
.make_contacts(vec![(&client_a, cx_a), (&client_b, cx_b)])
|
||||
.await;
|
||||
|
||||
client_a
|
||||
.fs
|
||||
.insert_tree(
|
||||
"/root-1",
|
||||
json!({
|
||||
"main.rs": "use std::collections::HashMap;",
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
|
||||
// Set up a fake language server.
|
||||
let mut language = Language::new(
|
||||
LanguageConfig {
|
||||
name: "Rust".into(),
|
||||
path_suffixes: vec!["rs".to_string()],
|
||||
..Default::default()
|
||||
},
|
||||
Some(tree_sitter_rust::language()),
|
||||
);
|
||||
let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default());
|
||||
client_a.language_registry.add(Arc::new(language));
|
||||
|
||||
let (project_a, worktree_id) = client_a.build_local_project("/root-1", cx_a).await;
|
||||
let project_b = client_b.build_remote_project(&project_a, cx_a, cx_b).await;
|
||||
|
||||
// Open the file as the guest
|
||||
let buffer_b = cx_b
|
||||
.background()
|
||||
.spawn(project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx)))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Request hover information as the guest.
|
||||
let fake_language_server = fake_language_servers.next().await.unwrap();
|
||||
fake_language_server.handle_request::<lsp::request::HoverRequest, _, _>(
|
||||
|params, _| async move {
|
||||
assert_eq!(
|
||||
params
|
||||
.text_document_position_params
|
||||
.text_document
|
||||
.uri
|
||||
.as_str(),
|
||||
"file:///root-1/main.rs"
|
||||
);
|
||||
assert_eq!(
|
||||
params.text_document_position_params.position,
|
||||
lsp::Position::new(0, 22)
|
||||
);
|
||||
Ok(Some(lsp::Hover {
|
||||
contents: lsp::HoverContents::Array(vec![
|
||||
lsp::MarkedString::String("Test hover content.".to_string()),
|
||||
lsp::MarkedString::LanguageString(lsp::LanguageString {
|
||||
language: "Rust".to_string(),
|
||||
value: "let foo = 42;".to_string(),
|
||||
}),
|
||||
]),
|
||||
range: Some(lsp::Range::new(
|
||||
lsp::Position::new(0, 22),
|
||||
lsp::Position::new(0, 29),
|
||||
)),
|
||||
}))
|
||||
},
|
||||
);
|
||||
|
||||
let hover_info = project_b
|
||||
.update(cx_b, |p, cx| p.hover(&buffer_b, 22, cx))
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
buffer_b.read_with(cx_b, |buffer, _| {
|
||||
let snapshot = buffer.snapshot();
|
||||
assert_eq!(hover_info.range.unwrap().to_offset(&snapshot), 22..29);
|
||||
assert_eq!(
|
||||
hover_info.contents,
|
||||
vec![
|
||||
project::HoverBlock {
|
||||
text: "Test hover content.".to_string(),
|
||||
language: None,
|
||||
},
|
||||
project::HoverBlock {
|
||||
text: "let foo = 42;".to_string(),
|
||||
language: Some("Rust".to_string()),
|
||||
}
|
||||
]
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test(iterations = 10)]
|
||||
async fn test_project_symbols(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
|
||||
cx_a.foreground().forbid_parking();
|
||||
|
|
|
@ -150,6 +150,7 @@ impl Server {
|
|||
.add_message_handler(Server::start_language_server)
|
||||
.add_message_handler(Server::update_language_server)
|
||||
.add_message_handler(Server::update_diagnostic_summary)
|
||||
.add_request_handler(Server::forward_project_request::<proto::GetHover>)
|
||||
.add_request_handler(Server::forward_project_request::<proto::GetDefinition>)
|
||||
.add_request_handler(Server::forward_project_request::<proto::GetReferences>)
|
||||
.add_request_handler(Server::forward_project_request::<proto::SearchProject>)
|
||||
|
|
|
@ -25,7 +25,7 @@ use gpui::{
|
|||
geometry::vector::{vec2f, Vector2F},
|
||||
impl_actions, impl_internal_actions,
|
||||
platform::CursorStyle,
|
||||
text_layout, AppContext, AsyncAppContext, ClipboardItem, Element, ElementBox, Entity,
|
||||
text_layout, AppContext, AsyncAppContext, Axis, ClipboardItem, Element, ElementBox, Entity,
|
||||
ModelHandle, MutableAppContext, RenderContext, Task, View, ViewContext, ViewHandle,
|
||||
WeakViewHandle,
|
||||
};
|
||||
|
@ -39,7 +39,7 @@ pub use multi_buffer::{
|
|||
Anchor, AnchorRangeExt, ExcerptId, MultiBuffer, MultiBufferSnapshot, ToOffset, ToPoint,
|
||||
};
|
||||
use ordered_float::OrderedFloat;
|
||||
use project::{Project, ProjectTransaction};
|
||||
use project::{HoverBlock, Project, ProjectTransaction};
|
||||
use selections_collection::{resolve_multiple, MutableSelectionsCollection, SelectionsCollection};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use settings::Settings;
|
||||
|
@ -80,6 +80,11 @@ pub struct Scroll(pub Vector2F);
|
|||
#[derive(Clone, PartialEq)]
|
||||
pub struct Select(pub SelectPhase);
|
||||
|
||||
#[derive(Clone, PartialEq)]
|
||||
pub struct HoverAt {
|
||||
point: Option<DisplayPoint>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Deserialize, PartialEq)]
|
||||
pub struct Input(pub String);
|
||||
|
||||
|
@ -113,6 +118,11 @@ pub struct ConfirmCodeAction {
|
|||
pub item_ix: Option<usize>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Default)]
|
||||
pub struct GoToDefinitionAt {
|
||||
pub location: Option<DisplayPoint>,
|
||||
}
|
||||
|
||||
actions!(
|
||||
editor,
|
||||
[
|
||||
|
@ -173,10 +183,10 @@ actions!(
|
|||
ToggleComments,
|
||||
SelectLargerSyntaxNode,
|
||||
SelectSmallerSyntaxNode,
|
||||
GoToDefinition,
|
||||
MoveToEnclosingBracket,
|
||||
UndoSelection,
|
||||
RedoSelection,
|
||||
GoToDefinition,
|
||||
FindAllReferences,
|
||||
Rename,
|
||||
ConfirmRename,
|
||||
|
@ -188,6 +198,7 @@ actions!(
|
|||
ShowCompletions,
|
||||
OpenExcerpts,
|
||||
RestartLanguageServer,
|
||||
Hover,
|
||||
]
|
||||
);
|
||||
|
||||
|
@ -204,7 +215,7 @@ impl_actions!(
|
|||
]
|
||||
);
|
||||
|
||||
impl_internal_actions!(editor, [Scroll, Select]);
|
||||
impl_internal_actions!(editor, [Scroll, Select, HoverAt]);
|
||||
|
||||
enum DocumentHighlightRead {}
|
||||
enum DocumentHighlightWrite {}
|
||||
|
@ -291,6 +302,8 @@ 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::restart_language_server);
|
||||
cx.add_async_action(Editor::confirm_completion);
|
||||
|
@ -408,6 +421,7 @@ 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,
|
||||
|
@ -415,6 +429,40 @@ pub struct Editor {
|
|||
keymap_context_layers: BTreeMap<TypeId, gpui::keymap::Context>,
|
||||
input_enabled: bool,
|
||||
leader_replica_id: Option<u16>,
|
||||
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 {
|
||||
|
@ -841,6 +889,67 @@ 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>,
|
||||
|
@ -998,6 +1107,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,
|
||||
|
@ -1006,6 +1116,11 @@ 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(),
|
||||
},
|
||||
};
|
||||
this.end_selection(cx);
|
||||
|
||||
|
@ -1391,6 +1506,8 @@ impl Editor {
|
|||
}
|
||||
}
|
||||
|
||||
self.hide_hover(cx);
|
||||
|
||||
if old_cursor_position.to_display_point(&display_map).row()
|
||||
!= new_cursor_position.to_display_point(&display_map).row()
|
||||
{
|
||||
|
@ -1752,6 +1869,10 @@ impl Editor {
|
|||
return;
|
||||
}
|
||||
|
||||
if self.hide_hover(cx) {
|
||||
return;
|
||||
}
|
||||
|
||||
if self.hide_context_menu(cx).is_some() {
|
||||
return;
|
||||
}
|
||||
|
@ -2380,6 +2501,179 @@ 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>,
|
||||
|
@ -2626,6 +2920,10 @@ 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();
|
||||
|
@ -4706,7 +5004,6 @@ impl Editor {
|
|||
// Position the selection in the rename editor so that it matches the current selection.
|
||||
this.show_local_selections = false;
|
||||
let rename_editor = cx.add_view(|cx| {
|
||||
println!("Rename editor created.");
|
||||
let mut editor = Editor::single_line(None, cx);
|
||||
if let Some(old_highlight_id) = old_highlight_id {
|
||||
editor.override_text_style =
|
||||
|
|
|
@ -5,7 +5,7 @@ use super::{
|
|||
};
|
||||
use crate::{
|
||||
display_map::{DisplaySnapshot, TransformBlock},
|
||||
EditorStyle,
|
||||
EditorStyle, HoverAt,
|
||||
};
|
||||
use clock::ReplicaId;
|
||||
use collections::{BTreeMap, HashMap};
|
||||
|
@ -102,6 +102,7 @@ impl EditorElement {
|
|||
fn mouse_down(
|
||||
&self,
|
||||
position: Vector2F,
|
||||
_: bool,
|
||||
alt: bool,
|
||||
shift: bool,
|
||||
mut click_count: usize,
|
||||
|
@ -121,7 +122,7 @@ impl EditorElement {
|
|||
if shift && alt {
|
||||
cx.dispatch_action(Select(SelectPhase::BeginColumnar {
|
||||
position,
|
||||
overshoot,
|
||||
overshoot: overshoot.column(),
|
||||
}));
|
||||
} else if shift {
|
||||
cx.dispatch_action(Select(SelectPhase::Extend {
|
||||
|
@ -190,7 +191,7 @@ impl EditorElement {
|
|||
|
||||
cx.dispatch_action(Select(SelectPhase::Update {
|
||||
position,
|
||||
overshoot,
|
||||
overshoot: overshoot.column(),
|
||||
scroll_position: (snapshot.scroll_position() + scroll_delta)
|
||||
.clamp(Vector2F::zero(), layout.scroll_max),
|
||||
}));
|
||||
|
@ -349,6 +350,7 @@ impl EditorElement {
|
|||
bounds: RectF,
|
||||
visible_bounds: RectF,
|
||||
layout: &mut LayoutState,
|
||||
paint: &mut PaintState,
|
||||
cx: &mut PaintContext,
|
||||
) {
|
||||
let view = self.view(cx.app);
|
||||
|
@ -490,7 +492,7 @@ impl EditorElement {
|
|||
let mut list_origin = content_origin + vec2f(x, y);
|
||||
let list_height = context_menu.size().y();
|
||||
|
||||
if list_origin.y() + list_height > bounds.lower_left().y() {
|
||||
if list_origin.y() + list_height > bounds.max_y() {
|
||||
list_origin.set_y(list_origin.y() - layout.line_height - list_height);
|
||||
}
|
||||
|
||||
|
@ -503,6 +505,38 @@ impl EditorElement {
|
|||
cx.scene.pop_stacking_context();
|
||||
}
|
||||
|
||||
if let Some((position, hover_popover)) = layout.hover.as_mut() {
|
||||
cx.scene.push_stacking_context(None);
|
||||
|
||||
// This is safe because we check on layout whether the required row is available
|
||||
let hovered_row_layout = &layout.line_layouts[(position.row() - start_row) as usize];
|
||||
let size = hover_popover.size();
|
||||
let x = hovered_row_layout.x_for_index(position.column() as usize) - scroll_left;
|
||||
let y = position.row() as f32 * layout.line_height - scroll_top - size.y();
|
||||
let mut popover_origin = content_origin + vec2f(x, y);
|
||||
|
||||
if popover_origin.y() < 0.0 {
|
||||
popover_origin.set_y(popover_origin.y() + layout.line_height + size.y());
|
||||
}
|
||||
|
||||
let x_out_of_bounds = bounds.max_x() - (popover_origin.x() + size.x());
|
||||
if x_out_of_bounds < 0.0 {
|
||||
popover_origin.set_x(popover_origin.x() + x_out_of_bounds);
|
||||
}
|
||||
|
||||
hover_popover.paint(
|
||||
popover_origin,
|
||||
RectF::from_points(Vector2F::zero(), vec2f(f32::MAX, f32::MAX)), // Let content bleed outside of editor
|
||||
cx,
|
||||
);
|
||||
|
||||
paint.hover_bounds = Some(
|
||||
RectF::new(popover_origin, hover_popover.size()).dilate(Vector2F::new(0., 5.)),
|
||||
);
|
||||
|
||||
cx.scene.pop_stacking_context();
|
||||
}
|
||||
|
||||
cx.scene.pop_layer();
|
||||
}
|
||||
|
||||
|
@ -1076,6 +1110,7 @@ impl Element for EditorElement {
|
|||
|
||||
let mut context_menu = None;
|
||||
let mut code_actions_indicator = None;
|
||||
let mut hover = None;
|
||||
cx.render(&self.view.upgrade(cx).unwrap(), |view, cx| {
|
||||
let newest_selection_head = view
|
||||
.selections
|
||||
|
@ -1083,8 +1118,8 @@ impl Element for EditorElement {
|
|||
.head()
|
||||
.to_display_point(&snapshot);
|
||||
|
||||
let style = view.style(cx);
|
||||
if (start_row..end_row).contains(&newest_selection_head.row()) {
|
||||
let style = view.style(cx);
|
||||
if view.context_menu_visible() {
|
||||
context_menu =
|
||||
view.render_context_menu(newest_selection_head, style.clone(), cx);
|
||||
|
@ -1094,6 +1129,17 @@ impl Element for EditorElement {
|
|||
.render_code_actions_indicator(&style, cx)
|
||||
.map(|indicator| (newest_selection_head.row(), indicator));
|
||||
}
|
||||
|
||||
hover = view.hover_popover().and_then(|hover| {
|
||||
let (point, rendered) = hover.render(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));
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
});
|
||||
});
|
||||
|
||||
if let Some((_, context_menu)) = context_menu.as_mut() {
|
||||
|
@ -1116,6 +1162,19 @@ impl Element for EditorElement {
|
|||
);
|
||||
}
|
||||
|
||||
if let Some((_, hover)) = hover.as_mut() {
|
||||
hover.layout(
|
||||
SizeConstraint {
|
||||
min: Vector2F::zero(),
|
||||
max: vec2f(
|
||||
(120. * em_width).min(size.x()),
|
||||
(size.y() - line_height) * 1. / 2.,
|
||||
),
|
||||
},
|
||||
cx,
|
||||
);
|
||||
}
|
||||
|
||||
let blocks = self.layout_blocks(
|
||||
start_row..end_row,
|
||||
&snapshot,
|
||||
|
@ -1152,6 +1211,7 @@ impl Element for EditorElement {
|
|||
selections,
|
||||
context_menu,
|
||||
code_actions_indicator,
|
||||
hover,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
@ -1171,11 +1231,18 @@ impl Element for EditorElement {
|
|||
layout.text_size,
|
||||
);
|
||||
|
||||
let mut paint_state = PaintState {
|
||||
bounds,
|
||||
gutter_bounds,
|
||||
text_bounds,
|
||||
hover_bounds: None,
|
||||
};
|
||||
|
||||
self.paint_background(gutter_bounds, text_bounds, layout, cx);
|
||||
if layout.gutter_size.x() > 0. {
|
||||
self.paint_gutter(gutter_bounds, visible_bounds, layout, cx);
|
||||
}
|
||||
self.paint_text(text_bounds, visible_bounds, layout, cx);
|
||||
self.paint_text(text_bounds, visible_bounds, layout, &mut paint_state, cx);
|
||||
|
||||
if !layout.blocks.is_empty() {
|
||||
cx.scene.push_layer(Some(bounds));
|
||||
|
@ -1185,11 +1252,7 @@ impl Element for EditorElement {
|
|||
|
||||
cx.scene.pop_layer();
|
||||
|
||||
PaintState {
|
||||
bounds,
|
||||
gutter_bounds,
|
||||
text_bounds,
|
||||
}
|
||||
paint_state
|
||||
}
|
||||
|
||||
fn dispatch_event(
|
||||
|
@ -1213,6 +1276,12 @@ impl Element for EditorElement {
|
|||
}
|
||||
}
|
||||
|
||||
if let Some((_, hover)) = &mut layout.hover {
|
||||
if hover.dispatch_event(event, cx) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
for (_, block) in &mut layout.blocks {
|
||||
if block.dispatch_event(event, cx) {
|
||||
return true;
|
||||
|
@ -1222,11 +1291,21 @@ impl Element for EditorElement {
|
|||
match event {
|
||||
Event::LeftMouseDown {
|
||||
position,
|
||||
cmd,
|
||||
alt,
|
||||
shift,
|
||||
click_count,
|
||||
..
|
||||
} => self.mouse_down(*position, *alt, *shift, *click_count, layout, paint, cx),
|
||||
} => self.mouse_down(
|
||||
*position,
|
||||
*cmd,
|
||||
*alt,
|
||||
*shift,
|
||||
*click_count,
|
||||
layout,
|
||||
paint,
|
||||
cx,
|
||||
),
|
||||
Event::LeftMouseUp { position, .. } => self.mouse_up(*position, cx),
|
||||
Event::LeftMouseDragged { position } => {
|
||||
self.mouse_dragged(*position, layout, paint, cx)
|
||||
|
@ -1237,6 +1316,29 @@ impl Element for EditorElement {
|
|||
precise,
|
||||
} => self.scroll(*position, *delta, *precise, layout, paint, cx),
|
||||
Event::KeyDown { input, .. } => self.key_down(input.as_deref(), cx),
|
||||
Event::MouseMoved { position, .. } => {
|
||||
if paint
|
||||
.hover_bounds
|
||||
.map_or(false, |hover_bounds| hover_bounds.contains_point(*position))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
let point = if paint.text_bounds.contains_point(*position) {
|
||||
let (point, overshoot) =
|
||||
paint.point_for_position(&self.snapshot(cx), layout, *position);
|
||||
if overshoot.is_zero() {
|
||||
Some(point)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
cx.dispatch_action(HoverAt { point });
|
||||
true
|
||||
}
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
@ -1275,6 +1377,7 @@ pub struct LayoutState {
|
|||
selections: Vec<(ReplicaId, Vec<SelectionLayout>)>,
|
||||
context_menu: Option<(DisplayPoint, ElementBox)>,
|
||||
code_actions_indicator: Option<(u32, ElementBox)>,
|
||||
hover: Option<(DisplayPoint, ElementBox)>,
|
||||
}
|
||||
|
||||
fn layout_line(
|
||||
|
@ -1312,19 +1415,24 @@ pub struct PaintState {
|
|||
bounds: RectF,
|
||||
gutter_bounds: RectF,
|
||||
text_bounds: RectF,
|
||||
hover_bounds: Option<RectF>,
|
||||
}
|
||||
|
||||
impl PaintState {
|
||||
/// Returns two display points. The first is the nearest valid
|
||||
/// position in the current buffer and the second is the distance to the
|
||||
/// nearest valid position if there was overshoot.
|
||||
fn point_for_position(
|
||||
&self,
|
||||
snapshot: &EditorSnapshot,
|
||||
layout: &LayoutState,
|
||||
position: Vector2F,
|
||||
) -> (DisplayPoint, u32) {
|
||||
) -> (DisplayPoint, DisplayPoint) {
|
||||
let scroll_position = snapshot.scroll_position();
|
||||
let position = position - self.text_bounds.origin();
|
||||
let y = position.y().max(0.0).min(layout.size.y());
|
||||
let row = ((y / layout.line_height) + scroll_position.y()) as u32;
|
||||
let row_overshoot = row.saturating_sub(snapshot.max_point().row());
|
||||
let row = cmp::min(row, snapshot.max_point().row());
|
||||
let line = &layout.line_layouts[(row - scroll_position.y() as u32) as usize];
|
||||
let x = position.x() + (scroll_position.x() * layout.em_width);
|
||||
|
@ -1336,9 +1444,12 @@ impl PaintState {
|
|||
} else {
|
||||
0
|
||||
};
|
||||
let overshoot = (0f32.max(x - line.width()) / layout.em_advance) as u32;
|
||||
let column_overshoot = (0f32.max(x - line.width()) / layout.em_advance) as u32;
|
||||
|
||||
(DisplayPoint::new(row, column), overshoot)
|
||||
(
|
||||
DisplayPoint::new(row, column),
|
||||
DisplayPoint::new(row_overshoot, column_overshoot),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -195,6 +195,14 @@ impl SelectionsCollection {
|
|||
resolve(self.newest_anchor(), &self.buffer(cx))
|
||||
}
|
||||
|
||||
pub fn newest_display(&self, cx: &mut MutableAppContext) -> Selection<DisplayPoint> {
|
||||
let display_map = self.display_map(cx);
|
||||
let selection = self
|
||||
.newest_anchor()
|
||||
.map(|point| point.to_display_point(&display_map));
|
||||
selection
|
||||
}
|
||||
|
||||
pub fn oldest_anchor(&self) -> &Selection<Anchor> {
|
||||
self.disjoint
|
||||
.iter()
|
||||
|
|
|
@ -319,6 +319,17 @@ impl Element for Flex {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !handled {
|
||||
if let &Event::MouseMoved { position, .. } = event {
|
||||
// If this is a scrollable flex, and the mouse is over it, eat the scroll event to prevent
|
||||
// propogating it to the element below.
|
||||
if self.scroll_state.is_some() && bounds.contains_point(position) {
|
||||
handled = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
handled
|
||||
}
|
||||
|
||||
|
|
|
@ -160,7 +160,12 @@ impl Presenter {
|
|||
|
||||
if cx.window_is_active(self.window_id) {
|
||||
if let Some(event) = self.last_mouse_moved_event.clone() {
|
||||
self.dispatch_event(event, cx)
|
||||
let mut invalidated_views = Vec::new();
|
||||
self.handle_hover_events(&event, &mut invalidated_views, cx);
|
||||
|
||||
for view_id in invalidated_views {
|
||||
cx.notify_view(self.window_id, view_id);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
|
@ -222,8 +227,6 @@ impl Presenter {
|
|||
pub fn dispatch_event(&mut self, event: Event, cx: &mut MutableAppContext) {
|
||||
if let Some(root_view_id) = cx.root_view_id(self.window_id) {
|
||||
let mut invalidated_views = Vec::new();
|
||||
let mut hovered_regions = Vec::new();
|
||||
let mut unhovered_regions = Vec::new();
|
||||
let mut mouse_down_out_handlers = Vec::new();
|
||||
let mut mouse_down_region = None;
|
||||
let mut clicked_region = None;
|
||||
|
@ -288,46 +291,8 @@ impl Presenter {
|
|||
}
|
||||
}
|
||||
}
|
||||
Event::MouseMoved {
|
||||
position,
|
||||
left_mouse_down,
|
||||
} => {
|
||||
Event::MouseMoved { .. } => {
|
||||
self.last_mouse_moved_event = Some(event.clone());
|
||||
|
||||
if !left_mouse_down {
|
||||
let mut style_to_assign = CursorStyle::Arrow;
|
||||
for region in self.cursor_regions.iter().rev() {
|
||||
if region.bounds.contains_point(position) {
|
||||
style_to_assign = region.style;
|
||||
break;
|
||||
}
|
||||
}
|
||||
cx.platform().set_cursor_style(style_to_assign);
|
||||
|
||||
let mut hover_depth = None;
|
||||
for (region, depth) in self.mouse_regions.iter().rev() {
|
||||
if region.bounds.contains_point(position)
|
||||
&& hover_depth.map_or(true, |hover_depth| hover_depth == *depth)
|
||||
{
|
||||
hover_depth = Some(*depth);
|
||||
if let Some(region_id) = region.id() {
|
||||
if !self.hovered_region_ids.contains(®ion_id) {
|
||||
invalidated_views.push(region.view_id);
|
||||
hovered_regions.push((region.clone(), position));
|
||||
self.hovered_region_ids.insert(region_id);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if let Some(region_id) = region.id() {
|
||||
if self.hovered_region_ids.contains(®ion_id) {
|
||||
invalidated_views.push(region.view_id);
|
||||
unhovered_regions.push((region.clone(), position));
|
||||
self.hovered_region_ids.remove(®ion_id);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Event::LeftMouseDragged { position } => {
|
||||
if let Some((clicked_region, prev_drag_position)) = self
|
||||
|
@ -348,25 +313,8 @@ impl Presenter {
|
|||
_ => {}
|
||||
}
|
||||
|
||||
let mut event_cx = self.build_event_context(cx);
|
||||
let mut handled = false;
|
||||
for (unhovered_region, position) in unhovered_regions {
|
||||
handled = true;
|
||||
if let Some(hover_callback) = unhovered_region.hover {
|
||||
event_cx.with_current_view(unhovered_region.view_id, |event_cx| {
|
||||
hover_callback(position, false, event_cx);
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
for (hovered_region, position) in hovered_regions {
|
||||
handled = true;
|
||||
if let Some(hover_callback) = hovered_region.hover {
|
||||
event_cx.with_current_view(hovered_region.view_id, |event_cx| {
|
||||
hover_callback(position, true, event_cx);
|
||||
})
|
||||
}
|
||||
}
|
||||
let (mut handled, mut event_cx) =
|
||||
self.handle_hover_events(&event, &mut invalidated_views, cx);
|
||||
|
||||
for (handler, view_id, position) in mouse_down_out_handlers {
|
||||
event_cx.with_current_view(view_id, |event_cx| handler(position, event_cx))
|
||||
|
@ -439,6 +387,80 @@ impl Presenter {
|
|||
}
|
||||
}
|
||||
|
||||
fn handle_hover_events<'a>(
|
||||
&'a mut self,
|
||||
event: &Event,
|
||||
invalidated_views: &mut Vec<usize>,
|
||||
cx: &'a mut MutableAppContext,
|
||||
) -> (bool, EventContext<'a>) {
|
||||
let mut unhovered_regions = Vec::new();
|
||||
let mut hovered_regions = Vec::new();
|
||||
|
||||
if let Event::MouseMoved {
|
||||
position,
|
||||
left_mouse_down,
|
||||
} = event
|
||||
{
|
||||
if !left_mouse_down {
|
||||
let mut style_to_assign = CursorStyle::Arrow;
|
||||
for region in self.cursor_regions.iter().rev() {
|
||||
if region.bounds.contains_point(*position) {
|
||||
style_to_assign = region.style;
|
||||
break;
|
||||
}
|
||||
}
|
||||
cx.platform().set_cursor_style(style_to_assign);
|
||||
|
||||
let mut hover_depth = None;
|
||||
for (region, depth) in self.mouse_regions.iter().rev() {
|
||||
if region.bounds.contains_point(*position)
|
||||
&& hover_depth.map_or(true, |hover_depth| hover_depth == *depth)
|
||||
{
|
||||
hover_depth = Some(*depth);
|
||||
if let Some(region_id) = region.id() {
|
||||
if !self.hovered_region_ids.contains(®ion_id) {
|
||||
invalidated_views.push(region.view_id);
|
||||
hovered_regions.push((region.clone(), position));
|
||||
self.hovered_region_ids.insert(region_id);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if let Some(region_id) = region.id() {
|
||||
if self.hovered_region_ids.contains(®ion_id) {
|
||||
invalidated_views.push(region.view_id);
|
||||
unhovered_regions.push((region.clone(), position));
|
||||
self.hovered_region_ids.remove(®ion_id);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mut event_cx = self.build_event_context(cx);
|
||||
let mut handled = false;
|
||||
|
||||
for (unhovered_region, position) in unhovered_regions {
|
||||
handled = true;
|
||||
if let Some(hover_callback) = unhovered_region.hover {
|
||||
event_cx.with_current_view(unhovered_region.view_id, |event_cx| {
|
||||
hover_callback(*position, false, event_cx);
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
for (hovered_region, position) in hovered_regions {
|
||||
handled = true;
|
||||
if let Some(hover_callback) = hovered_region.hover {
|
||||
event_cx.with_current_view(hovered_region.view_id, |event_cx| {
|
||||
hover_callback(*position, true, event_cx);
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
(handled, event_cx)
|
||||
}
|
||||
|
||||
pub fn build_event_context<'a>(
|
||||
&'a mut self,
|
||||
cx: &'a mut MutableAppContext,
|
||||
|
|
|
@ -236,7 +236,7 @@ impl LanguageRegistry {
|
|||
self.languages
|
||||
.read()
|
||||
.iter()
|
||||
.find(|language| language.name().as_ref() == name)
|
||||
.find(|language| language.name().to_lowercase() == name.to_lowercase())
|
||||
.cloned()
|
||||
}
|
||||
|
||||
|
|
|
@ -296,6 +296,10 @@ impl LanguageServer {
|
|||
prepare_support: Some(true),
|
||||
..Default::default()
|
||||
}),
|
||||
hover: Some(HoverClientCapabilities {
|
||||
content_format: Some(vec![MarkupKind::Markdown]),
|
||||
..Default::default()
|
||||
}),
|
||||
..Default::default()
|
||||
}),
|
||||
experimental: Some(json!({
|
||||
|
|
|
@ -38,6 +38,7 @@ libc = "0.2"
|
|||
log = { version = "0.4.16", features = ["kv_unstable_serde"] }
|
||||
parking_lot = "0.11.1"
|
||||
postage = { version = "0.4.1", features = ["futures-traits"] }
|
||||
pulldown-cmark = { version = "0.9.1", default-features = false }
|
||||
rand = "0.8.3"
|
||||
regex = "1.5"
|
||||
serde = { version = "1.0", features = ["derive", "rc"] }
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
use crate::{DocumentHighlight, Location, Project, ProjectTransaction};
|
||||
use crate::{DocumentHighlight, Hover, HoverBlock, Location, Project, ProjectTransaction};
|
||||
use anyhow::{anyhow, Result};
|
||||
use async_trait::async_trait;
|
||||
use client::{proto, PeerId};
|
||||
|
@ -9,6 +9,7 @@ use language::{
|
|||
range_from_lsp, Anchor, Bias, Buffer, PointUtf16, ToPointUtf16,
|
||||
};
|
||||
use lsp::{DocumentHighlightKind, ServerCapabilities};
|
||||
use pulldown_cmark::{CodeBlockKind, Event, Options, Parser, Tag};
|
||||
use std::{cmp::Reverse, ops::Range, path::Path};
|
||||
|
||||
#[async_trait(?Send)]
|
||||
|
@ -80,6 +81,10 @@ pub(crate) struct GetDocumentHighlights {
|
|||
pub position: PointUtf16,
|
||||
}
|
||||
|
||||
pub(crate) struct GetHover {
|
||||
pub position: PointUtf16,
|
||||
}
|
||||
|
||||
#[async_trait(?Send)]
|
||||
impl LspCommand for PrepareRename {
|
||||
type Response = Option<Range<Anchor>>;
|
||||
|
@ -794,3 +799,220 @@ impl LspCommand for GetDocumentHighlights {
|
|||
message.buffer_id
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait(?Send)]
|
||||
impl LspCommand for GetHover {
|
||||
type Response = Option<Hover>;
|
||||
type LspRequest = lsp::request::HoverRequest;
|
||||
type ProtoRequest = proto::GetHover;
|
||||
|
||||
fn to_lsp(&self, path: &Path, _: &AppContext) -> lsp::HoverParams {
|
||||
lsp::HoverParams {
|
||||
text_document_position_params: lsp::TextDocumentPositionParams {
|
||||
text_document: lsp::TextDocumentIdentifier {
|
||||
uri: lsp::Url::from_file_path(path).unwrap(),
|
||||
},
|
||||
position: point_to_lsp(self.position),
|
||||
},
|
||||
work_done_progress_params: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
async fn response_from_lsp(
|
||||
self,
|
||||
message: Option<lsp::Hover>,
|
||||
_: ModelHandle<Project>,
|
||||
buffer: ModelHandle<Buffer>,
|
||||
mut cx: AsyncAppContext,
|
||||
) -> Result<Self::Response> {
|
||||
Ok(message.and_then(|hover| {
|
||||
let range = hover.range.map(|range| {
|
||||
cx.read(|cx| {
|
||||
let buffer = buffer.read(cx);
|
||||
let token_start =
|
||||
buffer.clip_point_utf16(point_from_lsp(range.start), Bias::Left);
|
||||
let token_end = buffer.clip_point_utf16(point_from_lsp(range.end), Bias::Left);
|
||||
buffer.anchor_after(token_start)..buffer.anchor_before(token_end)
|
||||
})
|
||||
});
|
||||
|
||||
let contents = cx.read(|_| match hover.contents {
|
||||
lsp::HoverContents::Scalar(marked_string) => {
|
||||
HoverBlock::try_new(marked_string).map(|contents| vec![contents])
|
||||
}
|
||||
lsp::HoverContents::Array(marked_strings) => {
|
||||
let content: Vec<HoverBlock> = marked_strings
|
||||
.into_iter()
|
||||
.filter_map(|marked_string| HoverBlock::try_new(marked_string))
|
||||
.collect();
|
||||
if content.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(content)
|
||||
}
|
||||
}
|
||||
lsp::HoverContents::Markup(markup_content) => {
|
||||
let mut contents = Vec::new();
|
||||
let mut language = None;
|
||||
let mut current_text = String::new();
|
||||
for event in Parser::new_ext(&markup_content.value, Options::all()) {
|
||||
match event {
|
||||
Event::SoftBreak => {
|
||||
current_text.push(' ');
|
||||
}
|
||||
Event::Text(text) | Event::Code(text) => {
|
||||
current_text.push_str(&text.to_string());
|
||||
}
|
||||
Event::Start(Tag::CodeBlock(CodeBlockKind::Fenced(new_language))) => {
|
||||
if !current_text.is_empty() {
|
||||
let text = std::mem::replace(&mut current_text, String::new());
|
||||
contents.push(HoverBlock { text, language });
|
||||
}
|
||||
|
||||
language = if new_language.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(new_language.to_string())
|
||||
};
|
||||
}
|
||||
Event::End(Tag::CodeBlock(_))
|
||||
| Event::End(Tag::Paragraph)
|
||||
| Event::End(Tag::Heading(_, _, _))
|
||||
| Event::End(Tag::BlockQuote)
|
||||
| Event::HardBreak => {
|
||||
if !current_text.is_empty() {
|
||||
let text = std::mem::replace(&mut current_text, String::new());
|
||||
contents.push(HoverBlock { text, language });
|
||||
current_text.clear();
|
||||
}
|
||||
language = None;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
if !current_text.is_empty() {
|
||||
contents.push(HoverBlock {
|
||||
text: current_text,
|
||||
language,
|
||||
});
|
||||
}
|
||||
|
||||
if contents.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(contents)
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
contents.map(|contents| Hover { contents, range })
|
||||
}))
|
||||
}
|
||||
|
||||
fn to_proto(&self, project_id: u64, buffer: &Buffer) -> Self::ProtoRequest {
|
||||
proto::GetHover {
|
||||
project_id,
|
||||
buffer_id: buffer.remote_id(),
|
||||
position: Some(language::proto::serialize_anchor(
|
||||
&buffer.anchor_before(self.position),
|
||||
)),
|
||||
version: serialize_version(&buffer.version),
|
||||
}
|
||||
}
|
||||
|
||||
async fn from_proto(
|
||||
message: Self::ProtoRequest,
|
||||
_: ModelHandle<Project>,
|
||||
buffer: ModelHandle<Buffer>,
|
||||
mut cx: AsyncAppContext,
|
||||
) -> Result<Self> {
|
||||
let position = message
|
||||
.position
|
||||
.and_then(deserialize_anchor)
|
||||
.ok_or_else(|| anyhow!("invalid position"))?;
|
||||
buffer
|
||||
.update(&mut cx, |buffer, _| {
|
||||
buffer.wait_for_version(deserialize_version(message.version))
|
||||
})
|
||||
.await;
|
||||
Ok(Self {
|
||||
position: buffer.read_with(&cx, |buffer, _| position.to_point_utf16(buffer)),
|
||||
})
|
||||
}
|
||||
|
||||
fn response_to_proto(
|
||||
response: Self::Response,
|
||||
_: &mut Project,
|
||||
_: PeerId,
|
||||
_: &clock::Global,
|
||||
_: &AppContext,
|
||||
) -> proto::GetHoverResponse {
|
||||
if let Some(response) = response {
|
||||
let (start, end) = if let Some(range) = response.range {
|
||||
(
|
||||
Some(language::proto::serialize_anchor(&range.start)),
|
||||
Some(language::proto::serialize_anchor(&range.end)),
|
||||
)
|
||||
} else {
|
||||
(None, None)
|
||||
};
|
||||
|
||||
let contents = response
|
||||
.contents
|
||||
.into_iter()
|
||||
.map(|block| proto::HoverBlock {
|
||||
text: block.text,
|
||||
language: block.language,
|
||||
})
|
||||
.collect();
|
||||
|
||||
proto::GetHoverResponse {
|
||||
start,
|
||||
end,
|
||||
contents,
|
||||
}
|
||||
} else {
|
||||
proto::GetHoverResponse {
|
||||
start: None,
|
||||
end: None,
|
||||
contents: Vec::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn response_from_proto(
|
||||
self,
|
||||
message: proto::GetHoverResponse,
|
||||
_: ModelHandle<Project>,
|
||||
_: 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))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let contents: Vec<_> = message
|
||||
.contents
|
||||
.into_iter()
|
||||
.map(|block| HoverBlock {
|
||||
text: block.text,
|
||||
language: block.language,
|
||||
})
|
||||
.collect();
|
||||
|
||||
Ok(if contents.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(Hover { contents, range })
|
||||
})
|
||||
}
|
||||
|
||||
fn buffer_id_from_proto(message: &Self::ProtoRequest) -> u64 {
|
||||
message.buffer_id
|
||||
}
|
||||
}
|
||||
|
|
|
@ -23,7 +23,10 @@ use language::{
|
|||
LanguageRegistry, LanguageServerName, LocalFile, LspAdapter, OffsetRangeExt, Operation, Patch,
|
||||
PointUtf16, TextBufferSnapshot, ToOffset, ToPointUtf16, Transaction,
|
||||
};
|
||||
use lsp::{DiagnosticSeverity, DiagnosticTag, DocumentHighlightKind, LanguageServer};
|
||||
use lsp::{
|
||||
DiagnosticSeverity, DiagnosticTag, DocumentHighlightKind, LanguageServer, LanguageString,
|
||||
MarkedString,
|
||||
};
|
||||
use lsp_command::*;
|
||||
use parking_lot::Mutex;
|
||||
use postage::stream::Stream;
|
||||
|
@ -223,6 +226,38 @@ pub struct Symbol {
|
|||
pub signature: [u8; 32],
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct HoverBlock {
|
||||
pub text: String,
|
||||
pub language: Option<String>,
|
||||
}
|
||||
|
||||
impl HoverBlock {
|
||||
fn try_new(marked_string: MarkedString) -> Option<Self> {
|
||||
let result = match marked_string {
|
||||
MarkedString::LanguageString(LanguageString { language, value }) => HoverBlock {
|
||||
text: value,
|
||||
language: Some(language),
|
||||
},
|
||||
MarkedString::String(text) => HoverBlock {
|
||||
text,
|
||||
language: None,
|
||||
},
|
||||
};
|
||||
if result.text.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(result)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Hover {
|
||||
pub contents: Vec<HoverBlock>,
|
||||
pub range: Option<Range<language::Anchor>>,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct ProjectTransaction(pub HashMap<ModelHandle<Buffer>, language::Transaction>);
|
||||
|
||||
|
@ -314,6 +349,7 @@ impl Project {
|
|||
client.add_model_request_handler(Self::handle_format_buffers);
|
||||
client.add_model_request_handler(Self::handle_get_code_actions);
|
||||
client.add_model_request_handler(Self::handle_get_completions);
|
||||
client.add_model_request_handler(Self::handle_lsp_command::<GetHover>);
|
||||
client.add_model_request_handler(Self::handle_lsp_command::<GetDefinition>);
|
||||
client.add_model_request_handler(Self::handle_lsp_command::<GetDocumentHighlights>);
|
||||
client.add_model_request_handler(Self::handle_lsp_command::<GetReferences>);
|
||||
|
@ -2912,6 +2948,16 @@ impl Project {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn hover<T: ToPointUtf16>(
|
||||
&self,
|
||||
buffer: &ModelHandle<Buffer>,
|
||||
position: T,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) -> Task<Result<Option<Hover>>> {
|
||||
let position = position.to_point_utf16(buffer.read(cx));
|
||||
self.request_lsp(buffer.clone(), GetHover { position }, cx)
|
||||
}
|
||||
|
||||
pub fn completions<T: ToPointUtf16>(
|
||||
&self,
|
||||
source_buffer_handle: &ModelHandle<Buffer>,
|
||||
|
|
|
@ -66,41 +66,43 @@ message Envelope {
|
|||
ApplyCompletionAdditionalEditsResponse apply_completion_additional_edits_response = 56;
|
||||
GetCodeActions get_code_actions = 57;
|
||||
GetCodeActionsResponse get_code_actions_response = 58;
|
||||
ApplyCodeAction apply_code_action = 59;
|
||||
ApplyCodeActionResponse apply_code_action_response = 60;
|
||||
PrepareRename prepare_rename = 61;
|
||||
PrepareRenameResponse prepare_rename_response = 62;
|
||||
PerformRename perform_rename = 63;
|
||||
PerformRenameResponse perform_rename_response = 64;
|
||||
SearchProject search_project = 65;
|
||||
SearchProjectResponse search_project_response = 66;
|
||||
GetHover get_hover = 59;
|
||||
GetHoverResponse get_hover_response = 60;
|
||||
ApplyCodeAction apply_code_action = 61;
|
||||
ApplyCodeActionResponse apply_code_action_response = 62;
|
||||
PrepareRename prepare_rename = 63;
|
||||
PrepareRenameResponse prepare_rename_response = 64;
|
||||
PerformRename perform_rename = 65;
|
||||
PerformRenameResponse perform_rename_response = 66;
|
||||
SearchProject search_project = 67;
|
||||
SearchProjectResponse search_project_response = 68;
|
||||
|
||||
GetChannels get_channels = 67;
|
||||
GetChannelsResponse get_channels_response = 68;
|
||||
JoinChannel join_channel = 69;
|
||||
JoinChannelResponse join_channel_response = 70;
|
||||
LeaveChannel leave_channel = 71;
|
||||
SendChannelMessage send_channel_message = 72;
|
||||
SendChannelMessageResponse send_channel_message_response = 73;
|
||||
ChannelMessageSent channel_message_sent = 74;
|
||||
GetChannelMessages get_channel_messages = 75;
|
||||
GetChannelMessagesResponse get_channel_messages_response = 76;
|
||||
GetChannels get_channels = 69;
|
||||
GetChannelsResponse get_channels_response = 70;
|
||||
JoinChannel join_channel = 71;
|
||||
JoinChannelResponse join_channel_response = 72;
|
||||
LeaveChannel leave_channel = 73;
|
||||
SendChannelMessage send_channel_message = 74;
|
||||
SendChannelMessageResponse send_channel_message_response = 75;
|
||||
ChannelMessageSent channel_message_sent = 76;
|
||||
GetChannelMessages get_channel_messages = 77;
|
||||
GetChannelMessagesResponse get_channel_messages_response = 78;
|
||||
|
||||
UpdateContacts update_contacts = 77;
|
||||
UpdateInviteInfo update_invite_info = 78;
|
||||
ShowContacts show_contacts = 79;
|
||||
UpdateContacts update_contacts = 79;
|
||||
UpdateInviteInfo update_invite_info = 80;
|
||||
ShowContacts show_contacts = 81;
|
||||
|
||||
GetUsers get_users = 80;
|
||||
FuzzySearchUsers fuzzy_search_users = 81;
|
||||
UsersResponse users_response = 82;
|
||||
RequestContact request_contact = 83;
|
||||
RespondToContactRequest respond_to_contact_request = 84;
|
||||
RemoveContact remove_contact = 85;
|
||||
GetUsers get_users = 82;
|
||||
FuzzySearchUsers fuzzy_search_users = 83;
|
||||
UsersResponse users_response = 84;
|
||||
RequestContact request_contact = 85;
|
||||
RespondToContactRequest respond_to_contact_request = 86;
|
||||
RemoveContact remove_contact = 87;
|
||||
|
||||
Follow follow = 86;
|
||||
FollowResponse follow_response = 87;
|
||||
UpdateFollowers update_followers = 88;
|
||||
Unfollow unfollow = 89;
|
||||
Follow follow = 88;
|
||||
FollowResponse follow_response = 89;
|
||||
UpdateFollowers update_followers = 90;
|
||||
Unfollow unfollow = 91;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -426,6 +428,24 @@ message GetCodeActionsResponse {
|
|||
repeated VectorClockEntry version = 2;
|
||||
}
|
||||
|
||||
message GetHover {
|
||||
uint64 project_id = 1;
|
||||
uint64 buffer_id = 2;
|
||||
Anchor position = 3;
|
||||
repeated VectorClockEntry version = 5;
|
||||
}
|
||||
|
||||
message GetHoverResponse {
|
||||
optional Anchor start = 1;
|
||||
optional Anchor end = 2;
|
||||
repeated HoverBlock contents = 3;
|
||||
}
|
||||
|
||||
message HoverBlock {
|
||||
string text = 1;
|
||||
optional string language = 2;
|
||||
}
|
||||
|
||||
message ApplyCodeAction {
|
||||
uint64 project_id = 1;
|
||||
uint64 buffer_id = 2;
|
||||
|
|
|
@ -99,6 +99,8 @@ messages!(
|
|||
(GetChannelsResponse, Foreground),
|
||||
(GetCodeActions, Background),
|
||||
(GetCodeActionsResponse, Background),
|
||||
(GetHover, Background),
|
||||
(GetHoverResponse, Background),
|
||||
(GetCompletions, Background),
|
||||
(GetCompletionsResponse, Background),
|
||||
(GetDefinition, Background),
|
||||
|
@ -175,6 +177,7 @@ request_messages!(
|
|||
(GetChannelMessages, GetChannelMessagesResponse),
|
||||
(GetChannels, GetChannelsResponse),
|
||||
(GetCodeActions, GetCodeActionsResponse),
|
||||
(GetHover, GetHoverResponse),
|
||||
(GetCompletions, GetCompletionsResponse),
|
||||
(GetDefinition, GetDefinitionResponse),
|
||||
(GetDocumentHighlights, GetDocumentHighlightsResponse),
|
||||
|
@ -221,6 +224,7 @@ entity_messages!(
|
|||
GetCompletions,
|
||||
GetDefinition,
|
||||
GetDocumentHighlights,
|
||||
GetHover,
|
||||
GetReferences,
|
||||
GetProjectSymbols,
|
||||
JoinProject,
|
||||
|
|
|
@ -452,6 +452,7 @@ pub struct Editor {
|
|||
pub autocomplete: AutocompleteStyle,
|
||||
pub code_actions_indicator: Color,
|
||||
pub unnecessary_code_fade: f32,
|
||||
pub hover_popover: HoverPopover,
|
||||
}
|
||||
|
||||
#[derive(Clone, Deserialize, Default)]
|
||||
|
@ -630,3 +631,11 @@ impl<'de> Deserialize<'de> for SyntaxTheme {
|
|||
Ok(result)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Deserialize, Default)]
|
||||
pub struct HoverPopover {
|
||||
pub container: ContainerStyle,
|
||||
pub block_style: ContainerStyle,
|
||||
pub prose: TextStyle,
|
||||
pub highlight: Color,
|
||||
}
|
||||
|
|
|
@ -4,9 +4,10 @@ import {
|
|||
backgroundColor,
|
||||
border,
|
||||
player,
|
||||
shadow,
|
||||
modalShadow,
|
||||
text,
|
||||
TextColor
|
||||
TextColor,
|
||||
popoverShadow
|
||||
} from "./components";
|
||||
|
||||
export default function chatPanel(theme: Theme) {
|
||||
|
@ -69,7 +70,7 @@ export default function chatPanel(theme: Theme) {
|
|||
cornerRadius: 6,
|
||||
padding: 4,
|
||||
border: border(theme, "primary"),
|
||||
shadow: shadow(theme),
|
||||
shadow: popoverShadow(theme),
|
||||
},
|
||||
},
|
||||
signInPrompt: text(theme, "sans", "secondary", { underline: true }),
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import chroma from "chroma-js";
|
||||
import { isIPv4 } from "net";
|
||||
import Theme, { BackgroundColorSet } from "../themes/common/theme";
|
||||
import { fontFamilies, fontSizes, FontWeight } from "../tokens";
|
||||
import { Color } from "../utils/color";
|
||||
|
@ -84,10 +85,18 @@ export function backgroundColor(
|
|||
return theme.backgroundColor[name][state || "base"].value;
|
||||
}
|
||||
|
||||
export function shadow(theme: Theme) {
|
||||
export function modalShadow(theme: Theme) {
|
||||
return {
|
||||
blur: 16,
|
||||
color: chroma("black").alpha(theme.shadowAlpha.value).hex(),
|
||||
color: theme.shadow.value,
|
||||
offset: [0, 2],
|
||||
};
|
||||
}
|
||||
|
||||
export function popoverShadow(theme: Theme) {
|
||||
return {
|
||||
blur: 4,
|
||||
color: theme.shadow.value,
|
||||
offset: [1, 2],
|
||||
};
|
||||
}
|
||||
|
|
|
@ -1,12 +1,12 @@
|
|||
import Theme from "../themes/common/theme";
|
||||
import { backgroundColor, border, borderColor, shadow, text } from "./components";
|
||||
import { backgroundColor, border, borderColor, popoverShadow, text } from "./components";
|
||||
|
||||
export default function contextMenu(theme: Theme) {
|
||||
return {
|
||||
background: backgroundColor(theme, 300, "base"),
|
||||
cornerRadius: 6,
|
||||
padding: 6,
|
||||
shadow: shadow(theme),
|
||||
shadow: popoverShadow(theme),
|
||||
border: border(theme, "primary"),
|
||||
item: {
|
||||
padding: { left: 4, right: 4, top: 2, bottom: 2 },
|
||||
|
|
|
@ -4,9 +4,11 @@ import {
|
|||
border,
|
||||
iconColor,
|
||||
player,
|
||||
popoverShadow,
|
||||
text,
|
||||
TextColor
|
||||
} from "./components";
|
||||
import hoverPopover from "./hoverPopover";
|
||||
|
||||
export default function editor(theme: Theme) {
|
||||
const autocompleteItem = {
|
||||
|
@ -80,6 +82,7 @@ export default function editor(theme: Theme) {
|
|||
cornerRadius: 8,
|
||||
padding: 4,
|
||||
border: border(theme, "secondary"),
|
||||
shadow: popoverShadow(theme),
|
||||
item: autocompleteItem,
|
||||
hoveredItem: {
|
||||
...autocompleteItem,
|
||||
|
@ -143,6 +146,7 @@ export default function editor(theme: Theme) {
|
|||
invalidHintDiagnostic: diagnostic(theme, "muted"),
|
||||
invalidInformationDiagnostic: diagnostic(theme, "muted"),
|
||||
invalidWarningDiagnostic: diagnostic(theme, "muted"),
|
||||
hover_popover: hoverPopover(theme),
|
||||
syntax,
|
||||
};
|
||||
}
|
||||
|
|
27
styles/src/styleTree/hoverPopover.ts
Normal file
27
styles/src/styleTree/hoverPopover.ts
Normal file
|
@ -0,0 +1,27 @@
|
|||
import Theme from "../themes/common/theme";
|
||||
import { backgroundColor, border, popoverShadow, text } from "./components";
|
||||
|
||||
export default function HoverPopover(theme: Theme) {
|
||||
return {
|
||||
container: {
|
||||
background: backgroundColor(theme, "on500"),
|
||||
cornerRadius: 8,
|
||||
padding: {
|
||||
left: 8,
|
||||
right: 8,
|
||||
top: 4,
|
||||
bottom: 4
|
||||
},
|
||||
shadow: popoverShadow(theme),
|
||||
border: border(theme, "primary"),
|
||||
margin: {
|
||||
left: -8,
|
||||
},
|
||||
},
|
||||
block_style: {
|
||||
padding: { top: 4 },
|
||||
},
|
||||
prose: text(theme, "sans", "primary", { "size": "sm" }),
|
||||
highlight: theme.editor.highlight.occurrence.value,
|
||||
}
|
||||
}
|
|
@ -1,5 +1,5 @@
|
|||
import Theme from "../themes/common/theme";
|
||||
import { backgroundColor, border, player, shadow, text } from "./components";
|
||||
import { backgroundColor, border, player, modalShadow, text } from "./components";
|
||||
|
||||
export default function picker(theme: Theme) {
|
||||
return {
|
||||
|
@ -48,6 +48,6 @@ export default function picker(theme: Theme) {
|
|||
top: 7,
|
||||
},
|
||||
},
|
||||
shadow: shadow(theme),
|
||||
shadow: modalShadow(theme),
|
||||
};
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import Theme from "../themes/common/theme";
|
||||
import { backgroundColor, border, shadow, text } from "./components";
|
||||
import { backgroundColor, border, popoverShadow, text } from "./components";
|
||||
|
||||
export default function tooltip(theme: Theme) {
|
||||
return {
|
||||
|
@ -7,7 +7,7 @@ export default function tooltip(theme: Theme) {
|
|||
border: border(theme, "secondary"),
|
||||
padding: { top: 4, bottom: 4, left: 8, right: 8 },
|
||||
margin: { top: 6, left: 6 },
|
||||
shadow: shadow(theme),
|
||||
shadow: popoverShadow(theme),
|
||||
cornerRadius: 6,
|
||||
text: text(theme, "sans", "secondary", { size: "xs", weight: "bold" }),
|
||||
keystroke: {
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import Theme from "../themes/common/theme";
|
||||
import { withOpacity } from "../utils/color";
|
||||
import { backgroundColor, border, iconColor, shadow, text } from "./components";
|
||||
import { backgroundColor, border, iconColor, modalShadow, text } from "./components";
|
||||
import statusBar from "./statusBar";
|
||||
|
||||
export function workspaceBackground(theme: Theme) {
|
||||
|
@ -164,7 +164,7 @@ export default function workspace(theme: Theme) {
|
|||
cornerRadius: 6,
|
||||
padding: 12,
|
||||
border: border(theme, "primary"),
|
||||
shadow: shadow(theme),
|
||||
shadow: modalShadow(theme),
|
||||
},
|
||||
notifications: {
|
||||
width: 380,
|
||||
|
|
|
@ -14,7 +14,6 @@ export function createTheme(
|
|||
name: string,
|
||||
isLight: boolean,
|
||||
ramps: { [rampName: string]: Scale },
|
||||
blend?: number
|
||||
): Theme {
|
||||
if (isLight) {
|
||||
for (var rampName in ramps) {
|
||||
|
@ -25,100 +24,99 @@ export function createTheme(
|
|||
ramps.neutral = ramps.neutral.domain([0, 7]);
|
||||
}
|
||||
|
||||
if (blend === undefined) {
|
||||
blend = isLight ? 0.12 : 0.24;
|
||||
}
|
||||
let blend = isLight ? 0.12 : 0.24;
|
||||
|
||||
function rampColor(ramp: Scale, index: number): ColorToken {
|
||||
function sample(ramp: Scale, index: number): ColorToken {
|
||||
return color(ramp(index).hex());
|
||||
}
|
||||
const darkest = color(ramps.neutral(isLight ? 7 : 0).hex());
|
||||
|
||||
const backgroundColor = {
|
||||
// Title bar
|
||||
100: {
|
||||
base: rampColor(ramps.neutral, 1.25),
|
||||
hovered: rampColor(ramps.neutral, 1.5),
|
||||
active: rampColor(ramps.neutral, 1.75),
|
||||
base: sample(ramps.neutral, 1.25),
|
||||
hovered: sample(ramps.neutral, 1.5),
|
||||
active: sample(ramps.neutral, 1.75),
|
||||
},
|
||||
// Midground (panels, etc)
|
||||
300: {
|
||||
base: rampColor(ramps.neutral, 1),
|
||||
hovered: rampColor(ramps.neutral, 1.25),
|
||||
active: rampColor(ramps.neutral, 1.5),
|
||||
base: sample(ramps.neutral, 1),
|
||||
hovered: sample(ramps.neutral, 1.25),
|
||||
active: sample(ramps.neutral, 1.5),
|
||||
},
|
||||
// Editor
|
||||
500: {
|
||||
base: rampColor(ramps.neutral, 0),
|
||||
hovered: rampColor(ramps.neutral, 0.25),
|
||||
active: rampColor(ramps.neutral, 0.5),
|
||||
base: sample(ramps.neutral, 0),
|
||||
hovered: sample(ramps.neutral, 0.25),
|
||||
active: sample(ramps.neutral, 0.5),
|
||||
},
|
||||
on300: {
|
||||
base: rampColor(ramps.neutral, 0),
|
||||
hovered: rampColor(ramps.neutral, 0.25),
|
||||
active: rampColor(ramps.neutral, 0.5),
|
||||
base: sample(ramps.neutral, 0),
|
||||
hovered: sample(ramps.neutral, 0.25),
|
||||
active: sample(ramps.neutral, 0.5),
|
||||
},
|
||||
on500: {
|
||||
base: rampColor(ramps.neutral, 1.25),
|
||||
hovered: rampColor(ramps.neutral, 1.5),
|
||||
active: rampColor(ramps.neutral, 1.75),
|
||||
base: sample(ramps.neutral, 1.25),
|
||||
hovered: sample(ramps.neutral, 1.5),
|
||||
active: sample(ramps.neutral, 1.75),
|
||||
},
|
||||
ok: {
|
||||
base: withOpacity(rampColor(ramps.green, 0.5), 0.15),
|
||||
hovered: withOpacity(rampColor(ramps.green, 0.5), 0.2),
|
||||
active: withOpacity(rampColor(ramps.green, 0.5), 0.25),
|
||||
base: withOpacity(sample(ramps.green, 0.5), 0.15),
|
||||
hovered: withOpacity(sample(ramps.green, 0.5), 0.2),
|
||||
active: withOpacity(sample(ramps.green, 0.5), 0.25),
|
||||
},
|
||||
error: {
|
||||
base: withOpacity(rampColor(ramps.red, 0.5), 0.15),
|
||||
hovered: withOpacity(rampColor(ramps.red, 0.5), 0.2),
|
||||
active: withOpacity(rampColor(ramps.red, 0.5), 0.25),
|
||||
base: withOpacity(sample(ramps.red, 0.5), 0.15),
|
||||
hovered: withOpacity(sample(ramps.red, 0.5), 0.2),
|
||||
active: withOpacity(sample(ramps.red, 0.5), 0.25),
|
||||
},
|
||||
warning: {
|
||||
base: withOpacity(rampColor(ramps.yellow, 0.5), 0.15),
|
||||
hovered: withOpacity(rampColor(ramps.yellow, 0.5), 0.2),
|
||||
active: withOpacity(rampColor(ramps.yellow, 0.5), 0.25),
|
||||
base: withOpacity(sample(ramps.yellow, 0.5), 0.15),
|
||||
hovered: withOpacity(sample(ramps.yellow, 0.5), 0.2),
|
||||
active: withOpacity(sample(ramps.yellow, 0.5), 0.25),
|
||||
},
|
||||
info: {
|
||||
base: withOpacity(rampColor(ramps.blue, 0.5), 0.15),
|
||||
hovered: withOpacity(rampColor(ramps.blue, 0.5), 0.2),
|
||||
active: withOpacity(rampColor(ramps.blue, 0.5), 0.25),
|
||||
base: withOpacity(sample(ramps.blue, 0.5), 0.15),
|
||||
hovered: withOpacity(sample(ramps.blue, 0.5), 0.2),
|
||||
active: withOpacity(sample(ramps.blue, 0.5), 0.25),
|
||||
},
|
||||
};
|
||||
|
||||
const borderColor = {
|
||||
primary: rampColor(ramps.neutral, isLight ? 1.5 : 0),
|
||||
secondary: rampColor(ramps.neutral, isLight ? 1.25 : 1),
|
||||
muted: rampColor(ramps.neutral, isLight ? 1 : 3),
|
||||
active: rampColor(ramps.neutral, isLight ? 4 : 3),
|
||||
onMedia: withOpacity(rampColor(ramps.neutral, 0), 0.1),
|
||||
ok: withOpacity(rampColor(ramps.green, 0.5), 0.15),
|
||||
error: withOpacity(rampColor(ramps.red, 0.5), 0.15),
|
||||
warning: withOpacity(rampColor(ramps.yellow, 0.5), 0.15),
|
||||
info: withOpacity(rampColor(ramps.blue, 0.5), 0.15),
|
||||
primary: sample(ramps.neutral, isLight ? 1.5 : 0),
|
||||
secondary: sample(ramps.neutral, isLight ? 1.25 : 1),
|
||||
muted: sample(ramps.neutral, isLight ? 1 : 3),
|
||||
active: sample(ramps.neutral, isLight ? 4 : 3),
|
||||
onMedia: withOpacity(darkest, 0.1),
|
||||
ok: withOpacity(sample(ramps.green, 0.5), 0.15),
|
||||
error: withOpacity(sample(ramps.red, 0.5), 0.15),
|
||||
warning: withOpacity(sample(ramps.yellow, 0.5), 0.15),
|
||||
info: withOpacity(sample(ramps.blue, 0.5), 0.15),
|
||||
};
|
||||
|
||||
const textColor = {
|
||||
primary: rampColor(ramps.neutral, 6),
|
||||
secondary: rampColor(ramps.neutral, 5),
|
||||
muted: rampColor(ramps.neutral, 5),
|
||||
placeholder: rampColor(ramps.neutral, 4),
|
||||
active: rampColor(ramps.neutral, 7),
|
||||
feature: rampColor(ramps.blue, 0.5),
|
||||
ok: rampColor(ramps.green, 0.5),
|
||||
error: rampColor(ramps.red, 0.5),
|
||||
warning: rampColor(ramps.yellow, 0.5),
|
||||
info: rampColor(ramps.blue, 0.5),
|
||||
onMedia: rampColor(ramps.neutral, isLight ? 0 : 7),
|
||||
primary: sample(ramps.neutral, 6),
|
||||
secondary: sample(ramps.neutral, 5),
|
||||
muted: sample(ramps.neutral, 5),
|
||||
placeholder: sample(ramps.neutral, 4),
|
||||
active: sample(ramps.neutral, 7),
|
||||
feature: sample(ramps.blue, 0.5),
|
||||
ok: sample(ramps.green, 0.5),
|
||||
error: sample(ramps.red, 0.5),
|
||||
warning: sample(ramps.yellow, 0.5),
|
||||
info: sample(ramps.blue, 0.5),
|
||||
onMedia: darkest,
|
||||
};
|
||||
|
||||
const player = {
|
||||
1: buildPlayer(rampColor(ramps.blue, 0.5)),
|
||||
2: buildPlayer(rampColor(ramps.green, 0.5)),
|
||||
3: buildPlayer(rampColor(ramps.magenta, 0.5)),
|
||||
4: buildPlayer(rampColor(ramps.orange, 0.5)),
|
||||
5: buildPlayer(rampColor(ramps.violet, 0.5)),
|
||||
6: buildPlayer(rampColor(ramps.cyan, 0.5)),
|
||||
7: buildPlayer(rampColor(ramps.red, 0.5)),
|
||||
8: buildPlayer(rampColor(ramps.yellow, 0.5)),
|
||||
1: buildPlayer(sample(ramps.blue, 0.5)),
|
||||
2: buildPlayer(sample(ramps.green, 0.5)),
|
||||
3: buildPlayer(sample(ramps.magenta, 0.5)),
|
||||
4: buildPlayer(sample(ramps.orange, 0.5)),
|
||||
5: buildPlayer(sample(ramps.violet, 0.5)),
|
||||
6: buildPlayer(sample(ramps.cyan, 0.5)),
|
||||
7: buildPlayer(sample(ramps.red, 0.5)),
|
||||
8: buildPlayer(sample(ramps.yellow, 0.5)),
|
||||
};
|
||||
|
||||
const editor = {
|
||||
|
@ -126,16 +124,16 @@ export function createTheme(
|
|||
indent_guide: borderColor.muted,
|
||||
indent_guide_active: borderColor.secondary,
|
||||
line: {
|
||||
active: rampColor(ramps.neutral, 1),
|
||||
highlighted: rampColor(ramps.neutral, 1.25), // TODO: Where is this used?
|
||||
active: sample(ramps.neutral, 1),
|
||||
highlighted: sample(ramps.neutral, 1.25), // TODO: Where is this used?
|
||||
},
|
||||
highlight: {
|
||||
selection: player[1].selectionColor,
|
||||
occurrence: withOpacity(rampColor(ramps.neutral, 3.5), blend),
|
||||
activeOccurrence: withOpacity(rampColor(ramps.neutral, 3.5), blend * 2), // TODO: Not hooked up - https://github.com/zed-industries/zed/issues/751
|
||||
occurrence: withOpacity(sample(ramps.neutral, 3.5), blend),
|
||||
activeOccurrence: withOpacity(sample(ramps.neutral, 3.5), blend * 2), // TODO: Not hooked up - https://github.com/zed-industries/zed/issues/751
|
||||
matchingBracket: backgroundColor[500].active, // TODO: Not hooked up
|
||||
match: rampColor(ramps.violet, 0.15),
|
||||
activeMatch: withOpacity(rampColor(ramps.violet, 0.4), blend * 2), // TODO: Not hooked up - https://github.com/zed-industries/zed/issues/751
|
||||
match: sample(ramps.violet, 0.15),
|
||||
activeMatch: withOpacity(sample(ramps.violet, 0.4), blend * 2), // TODO: Not hooked up - https://github.com/zed-industries/zed/issues/751
|
||||
related: backgroundColor[500].hovered,
|
||||
},
|
||||
gutter: {
|
||||
|
@ -146,59 +144,59 @@ export function createTheme(
|
|||
|
||||
const syntax: Syntax = {
|
||||
primary: {
|
||||
color: rampColor(ramps.neutral, 7),
|
||||
color: sample(ramps.neutral, 7),
|
||||
weight: fontWeights.normal,
|
||||
},
|
||||
comment: {
|
||||
color: rampColor(ramps.neutral, 5),
|
||||
color: sample(ramps.neutral, 5),
|
||||
weight: fontWeights.normal,
|
||||
},
|
||||
punctuation: {
|
||||
color: rampColor(ramps.neutral, 6),
|
||||
color: sample(ramps.neutral, 6),
|
||||
weight: fontWeights.normal,
|
||||
},
|
||||
constant: {
|
||||
color: rampColor(ramps.neutral, 4),
|
||||
color: sample(ramps.neutral, 4),
|
||||
weight: fontWeights.normal,
|
||||
},
|
||||
keyword: {
|
||||
color: rampColor(ramps.blue, 0.5),
|
||||
color: sample(ramps.blue, 0.5),
|
||||
weight: fontWeights.normal,
|
||||
},
|
||||
function: {
|
||||
color: rampColor(ramps.yellow, 0.5),
|
||||
color: sample(ramps.yellow, 0.5),
|
||||
weight: fontWeights.normal,
|
||||
},
|
||||
type: {
|
||||
color: rampColor(ramps.cyan, 0.5),
|
||||
color: sample(ramps.cyan, 0.5),
|
||||
weight: fontWeights.normal,
|
||||
},
|
||||
variant: {
|
||||
color: rampColor(ramps.blue, 0.5),
|
||||
color: sample(ramps.blue, 0.5),
|
||||
weight: fontWeights.normal,
|
||||
},
|
||||
property: {
|
||||
color: rampColor(ramps.blue, 0.5),
|
||||
color: sample(ramps.blue, 0.5),
|
||||
weight: fontWeights.normal,
|
||||
},
|
||||
enum: {
|
||||
color: rampColor(ramps.orange, 0.5),
|
||||
color: sample(ramps.orange, 0.5),
|
||||
weight: fontWeights.normal,
|
||||
},
|
||||
operator: {
|
||||
color: rampColor(ramps.orange, 0.5),
|
||||
color: sample(ramps.orange, 0.5),
|
||||
weight: fontWeights.normal,
|
||||
},
|
||||
string: {
|
||||
color: rampColor(ramps.orange, 0.5),
|
||||
color: sample(ramps.orange, 0.5),
|
||||
weight: fontWeights.normal,
|
||||
},
|
||||
number: {
|
||||
color: rampColor(ramps.green, 0.5),
|
||||
color: sample(ramps.green, 0.5),
|
||||
weight: fontWeights.normal,
|
||||
},
|
||||
boolean: {
|
||||
color: rampColor(ramps.green, 0.5),
|
||||
color: sample(ramps.green, 0.5),
|
||||
weight: fontWeights.normal,
|
||||
},
|
||||
predictive: {
|
||||
|
@ -206,7 +204,7 @@ export function createTheme(
|
|||
weight: fontWeights.normal,
|
||||
},
|
||||
title: {
|
||||
color: rampColor(ramps.yellow, 0.5),
|
||||
color: sample(ramps.yellow, 0.5),
|
||||
weight: fontWeights.bold,
|
||||
},
|
||||
emphasis: {
|
||||
|
@ -218,21 +216,20 @@ export function createTheme(
|
|||
weight: fontWeights.bold,
|
||||
},
|
||||
linkUri: {
|
||||
color: rampColor(ramps.green, 0.5),
|
||||
color: sample(ramps.green, 0.5),
|
||||
weight: fontWeights.normal,
|
||||
underline: true,
|
||||
},
|
||||
linkText: {
|
||||
color: rampColor(ramps.orange, 0.5),
|
||||
color: sample(ramps.orange, 0.5),
|
||||
weight: fontWeights.normal,
|
||||
italic: true,
|
||||
},
|
||||
};
|
||||
|
||||
const shadowAlpha: NumberToken = {
|
||||
value: blend,
|
||||
type: "number",
|
||||
};
|
||||
const shadow = withOpacity(
|
||||
color(ramps.neutral(isLight ? 7 : 0).darken().hex()),
|
||||
blend);
|
||||
|
||||
return {
|
||||
name,
|
||||
|
@ -243,6 +240,6 @@ export function createTheme(
|
|||
editor,
|
||||
syntax,
|
||||
player,
|
||||
shadowAlpha,
|
||||
shadow,
|
||||
};
|
||||
}
|
||||
|
|
|
@ -153,6 +153,6 @@ export default interface Theme {
|
|||
6: Player;
|
||||
7: Player;
|
||||
8: Player;
|
||||
};
|
||||
shadowAlpha: NumberToken;
|
||||
},
|
||||
shadow: ColorToken;
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue