Merge pull request #1237 from zed-industries/jump-to-definition

Mouse jump to definition
This commit is contained in:
Keith Simmons 2022-06-27 15:20:07 -07:00 committed by GitHub
commit bc82d98ae5
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
30 changed files with 1267 additions and 600 deletions

View file

@ -1980,13 +1980,13 @@ async fn test_definition(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
cx_b.read(|cx| { cx_b.read(|cx| {
assert_eq!(definitions_1.len(), 1); assert_eq!(definitions_1.len(), 1);
assert_eq!(project_b.read(cx).worktrees(cx).count(), 2); assert_eq!(project_b.read(cx).worktrees(cx).count(), 2);
let target_buffer = definitions_1[0].buffer.read(cx); let target_buffer = definitions_1[0].target.buffer.read(cx);
assert_eq!( assert_eq!(
target_buffer.text(), target_buffer.text(),
"const TWO: usize = 2;\nconst THREE: usize = 3;" "const TWO: usize = 2;\nconst THREE: usize = 3;"
); );
assert_eq!( assert_eq!(
definitions_1[0].range.to_point(target_buffer), definitions_1[0].target.range.to_point(target_buffer),
Point::new(0, 6)..Point::new(0, 9) Point::new(0, 6)..Point::new(0, 9)
); );
}); });
@ -2009,17 +2009,20 @@ async fn test_definition(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
cx_b.read(|cx| { cx_b.read(|cx| {
assert_eq!(definitions_2.len(), 1); assert_eq!(definitions_2.len(), 1);
assert_eq!(project_b.read(cx).worktrees(cx).count(), 2); assert_eq!(project_b.read(cx).worktrees(cx).count(), 2);
let target_buffer = definitions_2[0].buffer.read(cx); let target_buffer = definitions_2[0].target.buffer.read(cx);
assert_eq!( assert_eq!(
target_buffer.text(), target_buffer.text(),
"const TWO: usize = 2;\nconst THREE: usize = 3;" "const TWO: usize = 2;\nconst THREE: usize = 3;"
); );
assert_eq!( assert_eq!(
definitions_2[0].range.to_point(target_buffer), definitions_2[0].target.range.to_point(target_buffer),
Point::new(1, 6)..Point::new(1, 11) Point::new(1, 6)..Point::new(1, 11)
); );
}); });
assert_eq!(definitions_1[0].buffer, definitions_2[0].buffer); assert_eq!(
definitions_1[0].target.buffer,
definitions_2[0].target.buffer
);
} }
#[gpui::test(iterations = 10)] #[gpui::test(iterations = 10)]
@ -2554,7 +2557,7 @@ async fn test_open_buffer_while_getting_definition_pointing_to_it(
let buffer_b2 = buffer_b2.await.unwrap(); let buffer_b2 = buffer_b2.await.unwrap();
let definitions = definitions.await.unwrap(); let definitions = definitions.await.unwrap();
assert_eq!(definitions.len(), 1); assert_eq!(definitions.len(), 1);
assert_eq!(definitions[0].buffer, buffer_b2); assert_eq!(definitions[0].target.buffer, buffer_b2);
} }
#[gpui::test(iterations = 10)] #[gpui::test(iterations = 10)]
@ -5593,9 +5596,9 @@ impl TestClient {
log::info!("{}: detaching definitions request", guest_username); log::info!("{}: detaching definitions request", guest_username);
cx.update(|cx| definitions.detach_and_log_err(cx)); cx.update(|cx| definitions.detach_and_log_err(cx));
} else { } else {
client client.buffers.extend(
.buffers definitions.await?.into_iter().map(|loc| loc.target.buffer),
.extend(definitions.await?.into_iter().map(|loc| loc.buffer)); );
} }
} }
50..=54 => { 50..=54 => {

View file

@ -474,6 +474,14 @@ impl DisplaySnapshot {
pub fn longest_row(&self) -> u32 { pub fn longest_row(&self) -> u32 {
self.blocks_snapshot.longest_row() self.blocks_snapshot.longest_row()
} }
#[cfg(any(test, feature = "test-support"))]
pub fn highlight_ranges<Tag: ?Sized + 'static>(
&self,
) -> Option<Arc<(HighlightStyle, Vec<Range<Anchor>>)>> {
let type_id = TypeId::of::<Tag>();
self.text_highlights.get(&Some(type_id)).cloned()
}
} }
#[derive(Copy, Clone, Default, Eq, Ord, PartialOrd, PartialEq)] #[derive(Copy, Clone, Default, Eq, Ord, PartialOrd, PartialEq)]

View file

@ -2,6 +2,7 @@ pub mod display_map;
mod element; mod element;
mod hover_popover; mod hover_popover;
pub mod items; pub mod items;
mod link_go_to_definition;
pub mod movement; pub mod movement;
mod multi_buffer; mod multi_buffer;
pub mod selections_collection; pub mod selections_collection;
@ -37,13 +38,14 @@ use language::{
IndentKind, IndentSize, Language, OffsetRangeExt, Point, Selection, SelectionGoal, IndentKind, IndentSize, Language, OffsetRangeExt, Point, Selection, SelectionGoal,
TransactionId, TransactionId,
}; };
use link_go_to_definition::LinkGoToDefinitionState;
use multi_buffer::MultiBufferChunks; use multi_buffer::MultiBufferChunks;
pub use multi_buffer::{ pub use multi_buffer::{
Anchor, AnchorRangeExt, ExcerptId, ExcerptRange, MultiBuffer, MultiBufferSnapshot, ToOffset, Anchor, AnchorRangeExt, ExcerptId, ExcerptRange, MultiBuffer, MultiBufferSnapshot, ToOffset,
ToPoint, ToPoint,
}; };
use ordered_float::OrderedFloat; use ordered_float::OrderedFloat;
use project::{Project, ProjectPath, ProjectTransaction}; use project::{LocationLink, Project, ProjectPath, ProjectTransaction};
use selections_collection::{resolve_multiple, MutableSelectionsCollection, SelectionsCollection}; use selections_collection::{resolve_multiple, MutableSelectionsCollection, SelectionsCollection};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use settings::Settings; use settings::Settings;
@ -314,6 +316,7 @@ pub fn init(cx: &mut MutableAppContext) {
cx.add_async_action(Editor::find_all_references); cx.add_async_action(Editor::find_all_references);
hover_popover::init(cx); hover_popover::init(cx);
link_go_to_definition::init(cx);
workspace::register_project_item::<Editor>(cx); workspace::register_project_item::<Editor>(cx);
workspace::register_followable_item::<Editor>(cx); workspace::register_followable_item::<Editor>(cx);
@ -432,6 +435,7 @@ pub struct Editor {
input_enabled: bool, input_enabled: bool,
leader_replica_id: Option<u16>, leader_replica_id: Option<u16>,
hover_state: HoverState, hover_state: HoverState,
link_go_to_definition_state: LinkGoToDefinitionState,
} }
pub struct EditorSnapshot { pub struct EditorSnapshot {
@ -1021,6 +1025,7 @@ impl Editor {
input_enabled: true, input_enabled: true,
leader_replica_id: None, leader_replica_id: None,
hover_state: Default::default(), hover_state: Default::default(),
link_go_to_definition_state: Default::default(),
}; };
this.end_selection(cx); this.end_selection(cx);
@ -4597,24 +4602,7 @@ impl Editor {
cx.spawn(|workspace, mut cx| async move { cx.spawn(|workspace, mut cx| async move {
let definitions = definitions.await?; let definitions = definitions.await?;
workspace.update(&mut cx, |workspace, cx| { workspace.update(&mut cx, |workspace, cx| {
let nav_history = workspace.active_pane().read(cx).nav_history().clone(); Editor::navigate_to_definitions(workspace, editor_handle, definitions, cx);
for definition in definitions {
let range = definition.range.to_offset(definition.buffer.read(cx));
let target_editor_handle = workspace.open_project_item(definition.buffer, cx);
target_editor_handle.update(cx, |target_editor, cx| {
// When selecting a definition in a different buffer, disable the nav history
// to avoid creating a history entry at the previous cursor location.
if editor_handle != target_editor_handle {
nav_history.borrow_mut().disable();
}
target_editor.change_selections(Some(Autoscroll::Center), cx, |s| {
s.select_ranges([range]);
});
nav_history.borrow_mut().enable();
});
}
}); });
Ok::<(), anyhow::Error>(()) Ok::<(), anyhow::Error>(())
@ -4622,6 +4610,35 @@ impl Editor {
.detach_and_log_err(cx); .detach_and_log_err(cx);
} }
pub fn navigate_to_definitions(
workspace: &mut Workspace,
editor_handle: ViewHandle<Editor>,
definitions: Vec<LocationLink>,
cx: &mut ViewContext<Workspace>,
) {
let nav_history = workspace.active_pane().read(cx).nav_history().clone();
for definition in definitions {
let range = definition
.target
.range
.to_offset(definition.target.buffer.read(cx));
let target_editor_handle = workspace.open_project_item(definition.target.buffer, cx);
target_editor_handle.update(cx, |target_editor, cx| {
// When selecting a definition in a different buffer, disable the nav history
// to avoid creating a history entry at the previous cursor location.
if editor_handle != target_editor_handle {
nav_history.borrow_mut().disable();
}
target_editor.change_selections(Some(Autoscroll::Center), cx, |s| {
s.select_ranges([range]);
});
nav_history.borrow_mut().enable();
});
}
}
pub fn find_all_references( pub fn find_all_references(
workspace: &mut Workspace, workspace: &mut Workspace,
_: &FindAllReferences, _: &FindAllReferences,

View file

@ -6,6 +6,7 @@ use super::{
use crate::{ use crate::{
display_map::{BlockStyle, DisplaySnapshot, TransformBlock}, display_map::{BlockStyle, DisplaySnapshot, TransformBlock},
hover_popover::HoverAt, hover_popover::HoverAt,
link_go_to_definition::{CmdChanged, GoToFetchedDefinition, UpdateGoToDefinitionLink},
EditorStyle, EditorStyle,
}; };
use clock::ReplicaId; use clock::ReplicaId;
@ -104,7 +105,7 @@ impl EditorElement {
fn mouse_down( fn mouse_down(
&self, &self,
position: Vector2F, position: Vector2F,
_: bool, cmd: bool,
alt: bool, alt: bool,
shift: bool, shift: bool,
mut click_count: usize, mut click_count: usize,
@ -112,6 +113,14 @@ impl EditorElement {
paint: &mut PaintState, paint: &mut PaintState,
cx: &mut EventContext, cx: &mut EventContext,
) -> bool { ) -> bool {
if cmd && paint.text_bounds.contains_point(position) {
let (point, overshoot) = paint.point_for_position(&self.snapshot(cx), layout, position);
if overshoot.is_zero() {
cx.dispatch_action(GoToFetchedDefinition { point });
return true;
}
}
if paint.gutter_bounds.contains_point(position) { if paint.gutter_bounds.contains_point(position) {
click_count = 3; // Simulate triple-click when clicking the gutter to select lines click_count = 3; // Simulate triple-click when clicking the gutter to select lines
} else if !paint.text_bounds.contains_point(position) { } else if !paint.text_bounds.contains_point(position) {
@ -203,6 +212,52 @@ impl EditorElement {
} }
} }
fn mouse_moved(
&self,
position: Vector2F,
cmd: bool,
layout: &LayoutState,
paint: &PaintState,
cx: &mut EventContext,
) -> bool {
// This will be handled more correctly once https://github.com/zed-industries/zed/issues/1218 is completed
// Don't trigger hover popover if mouse is hovering over context menu
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(UpdateGoToDefinitionLink {
point,
cmd_held: cmd,
});
if paint
.context_menu_bounds
.map_or(false, |context_menu_bounds| {
context_menu_bounds.contains_point(position)
})
{
return false;
}
if paint
.hover_bounds
.map_or(false, |hover_bounds| hover_bounds.contains_point(position))
{
return false;
}
cx.dispatch_action(HoverAt { point });
true
}
fn key_down(&self, input: Option<&str>, cx: &mut EventContext) -> bool { fn key_down(&self, input: Option<&str>, cx: &mut EventContext) -> bool {
let view = self.view.upgrade(cx.app).unwrap(); let view = self.view.upgrade(cx.app).unwrap();
@ -218,6 +273,11 @@ impl EditorElement {
} }
} }
fn modifiers_changed(&self, cmd: bool, cx: &mut EventContext) -> bool {
cx.dispatch_action(CmdChanged { cmd_down: cmd });
false
}
fn scroll( fn scroll(
&self, &self,
position: Vector2F, position: Vector2F,
@ -367,9 +427,14 @@ impl EditorElement {
let content_origin = bounds.origin() + vec2f(layout.gutter_margin, 0.); let content_origin = bounds.origin() + vec2f(layout.gutter_margin, 0.);
cx.scene.push_layer(Some(bounds)); cx.scene.push_layer(Some(bounds));
cx.scene.push_cursor_region(CursorRegion { cx.scene.push_cursor_region(CursorRegion {
bounds, bounds,
style: CursorStyle::IBeam, style: if !view.link_go_to_definition_state.definitions.is_empty() {
CursorStyle::PointingHand
} else {
CursorStyle::IBeam
},
}); });
for (range, color) in &layout.highlighted_ranges { for (range, color) in &layout.highlighted_ranges {
@ -1417,7 +1482,7 @@ impl Element for EditorElement {
cx, cx,
), ),
Event::LeftMouseUp { position, .. } => self.mouse_up(*position, cx), Event::LeftMouseUp { position, .. } => self.mouse_up(*position, cx),
Event::LeftMouseDragged { position } => { Event::LeftMouseDragged { position, .. } => {
self.mouse_dragged(*position, layout, paint, cx) self.mouse_dragged(*position, layout, paint, cx)
} }
Event::ScrollWheel { Event::ScrollWheel {
@ -1426,40 +1491,11 @@ impl Element for EditorElement {
precise, precise,
} => self.scroll(*position, *delta, *precise, layout, paint, cx), } => self.scroll(*position, *delta, *precise, layout, paint, cx),
Event::KeyDown { input, .. } => self.key_down(input.as_deref(), cx), Event::KeyDown { input, .. } => self.key_down(input.as_deref(), cx),
Event::MouseMoved { position, .. } => { Event::ModifiersChanged { cmd, .. } => self.modifiers_changed(*cmd, cx),
// This will be handled more correctly once https://github.com/zed-industries/zed/issues/1218 is completed Event::MouseMoved { position, cmd, .. } => {
// Don't trigger hover popover if mouse is hovering over context menu self.mouse_moved(*position, *cmd, layout, paint, cx)
if paint
.context_menu_bounds
.map_or(false, |context_menu_bounds| {
context_menu_bounds.contains_point(*position)
})
{
return false;
}
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, _ => false,
} }
} }

View file

@ -1,8 +1,3 @@
use std::{
ops::Range,
time::{Duration, Instant},
};
use gpui::{ use gpui::{
actions, actions,
elements::{Flex, MouseEventHandler, Padding, Text}, elements::{Flex, MouseEventHandler, Padding, Text},
@ -12,6 +7,7 @@ use gpui::{
}; };
use language::Bias; use language::Bias;
use project::{HoverBlock, Project}; use project::{HoverBlock, Project};
use std::{ops::Range, time::Duration};
use util::TryFutureExt; use util::TryFutureExt;
use crate::{ use crate::{
@ -60,7 +56,6 @@ pub fn hide_hover(editor: &mut Editor, cx: &mut ViewContext<Editor>) -> bool {
// only notify the context once // only notify the context once
if editor.hover_state.popover.is_some() { if editor.hover_state.popover.is_some() {
editor.hover_state.popover = None; editor.hover_state.popover = None;
editor.hover_state.hidden_at = Some(cx.background().now());
did_hide = true; did_hide = true;
cx.notify(); cx.notify();
} }
@ -242,7 +237,6 @@ fn show_hover(
#[derive(Default)] #[derive(Default)]
pub struct HoverState { pub struct HoverState {
pub popover: Option<HoverPopover>, pub popover: Option<HoverPopover>,
pub hidden_at: Option<Instant>,
pub triggered_from: Option<Anchor>, pub triggered_from: Option<Anchor>,
pub symbol_range: Option<Range<Anchor>>, pub symbol_range: Option<Range<Anchor>>,
pub task: Option<Task<Option<()>>>, pub task: Option<Task<Option<()>>>,

View file

@ -1,58 +1,611 @@
use std::{ use std::ops::Range;
ops::Range,
time::{Duration, Instant},
};
use gpui::{ use gpui::{impl_internal_actions, MutableAppContext, Task, ViewContext};
actions, use language::{Bias, ToOffset};
elements::{Flex, MouseEventHandler, Padding, Text}, use project::LocationLink;
impl_internal_actions, use settings::Settings;
platform::CursorStyle,
Axis, Element, ElementBox, ModelHandle, MutableAppContext, RenderContext, Task, ViewContext,
};
use language::Bias;
use project::{HoverBlock, Project};
use util::TryFutureExt; use util::TryFutureExt;
use workspace::Workspace;
use crate::{ use crate::{Anchor, DisplayPoint, Editor, EditorSnapshot, GoToDefinition, Select, SelectPhase};
display_map::ToDisplayPoint, Anchor, AnchorRangeExt, DisplayPoint, Editor, EditorSnapshot,
EditorStyle,
};
#[derive(Clone, PartialEq)] #[derive(Clone, PartialEq)]
pub struct FetchDefinition { pub struct UpdateGoToDefinitionLink {
pub point: Option<DisplayPoint>, pub point: Option<DisplayPoint>,
pub cmd_held: bool,
}
#[derive(Clone, PartialEq)]
pub struct CmdChanged {
pub cmd_down: bool,
} }
#[derive(Clone, PartialEq)] #[derive(Clone, PartialEq)]
pub struct GoToFetchedDefinition { pub struct GoToFetchedDefinition {
pub point: Option<DisplayPoint>, pub point: DisplayPoint,
} }
impl_internal_actions!(edtior, [FetchDefinition, GoToFetchedDefinition]); impl_internal_actions!(
editor,
[UpdateGoToDefinitionLink, CmdChanged, GoToFetchedDefinition]
);
pub fn init(cx: &mut MutableAppContext) { pub fn init(cx: &mut MutableAppContext) {
cx.add_action(fetch_definition); cx.add_action(update_go_to_definition_link);
cx.add_action(cmd_changed);
cx.add_action(go_to_fetched_definition); cx.add_action(go_to_fetched_definition);
} }
pub fn fetch_definition(
editor: &mut Editor,
FetchDefinition { point }: &FetchDefinition,
cx: &mut ViewContext<Editor>,
) {
}
pub fn go_to_fetched_definition(
editor: &mut Editor,
GoToFetchedDefinition { point }: &GoToFetchedDefinition,
cx: &mut ViewContext<Editor>,
) {
}
#[derive(Default)] #[derive(Default)]
pub struct LinkGoToDefinitionState { pub struct LinkGoToDefinitionState {
pub triggered_from pub last_mouse_location: Option<Anchor>,
pub symbol_range: Option<Range<Anchor>>, pub symbol_range: Option<Range<Anchor>>,
pub definitions: Vec<LocationLink>,
pub task: Option<Task<Option<()>>>, pub task: Option<Task<Option<()>>>,
} }
pub fn update_go_to_definition_link(
editor: &mut Editor,
&UpdateGoToDefinitionLink { point, cmd_held }: &UpdateGoToDefinitionLink,
cx: &mut ViewContext<Editor>,
) {
// Store new mouse point as an anchor
let snapshot = editor.snapshot(cx);
let point = point.map(|point| {
snapshot
.buffer_snapshot
.anchor_before(point.to_offset(&snapshot.display_snapshot, Bias::Left))
});
// If the new point is the same as the previously stored one, return early
if let (Some(a), Some(b)) = (
&point,
&editor.link_go_to_definition_state.last_mouse_location,
) {
if a.cmp(&b, &snapshot.buffer_snapshot).is_eq() {
return;
}
}
editor.link_go_to_definition_state.last_mouse_location = point.clone();
if cmd_held {
if let Some(point) = point {
show_link_definition(editor, point, snapshot, cx);
return;
}
}
hide_link_definition(editor, cx);
}
pub fn cmd_changed(
editor: &mut Editor,
&CmdChanged { cmd_down }: &CmdChanged,
cx: &mut ViewContext<Editor>,
) {
if let Some(point) = editor
.link_go_to_definition_state
.last_mouse_location
.clone()
{
if cmd_down {
let snapshot = editor.snapshot(cx);
show_link_definition(editor, point.clone(), snapshot, cx);
} else {
hide_link_definition(editor, cx)
}
}
}
pub fn show_link_definition(
editor: &mut Editor,
trigger_point: Anchor,
snapshot: EditorSnapshot,
cx: &mut ViewContext<Editor>,
) {
if editor.pending_rename.is_some() {
return;
}
let (buffer, buffer_position) = if let Some(output) = editor
.buffer
.read(cx)
.text_anchor_for_position(trigger_point.clone(), cx)
{
output
} else {
return;
};
let excerpt_id = if let Some((excerpt_id, _, _)) = editor
.buffer()
.read(cx)
.excerpt_containing(trigger_point.clone(), cx)
{
excerpt_id
} else {
return;
};
let project = if let Some(project) = editor.project.clone() {
project
} else {
return;
};
// Don't request again if the location is within the symbol region of a previous request
if let Some(symbol_range) = &editor.link_go_to_definition_state.symbol_range {
if symbol_range
.start
.cmp(&trigger_point, &snapshot.buffer_snapshot)
.is_le()
&& symbol_range
.end
.cmp(&trigger_point, &snapshot.buffer_snapshot)
.is_ge()
{
return;
}
}
let task = cx.spawn_weak(|this, mut cx| {
async move {
// query the LSP for definition info
let definition_request = cx.update(|cx| {
project.update(cx, |project, cx| {
project.definition(&buffer, buffer_position.clone(), cx)
})
});
let result = definition_request.await.ok().map(|definition_result| {
(
definition_result.iter().find_map(|link| {
link.origin.as_ref().map(|origin| {
let start = snapshot
.buffer_snapshot
.anchor_in_excerpt(excerpt_id.clone(), origin.range.start);
let end = snapshot
.buffer_snapshot
.anchor_in_excerpt(excerpt_id.clone(), origin.range.end);
start..end
})
}),
definition_result,
)
});
if let Some(this) = this.upgrade(&cx) {
this.update(&mut cx, |this, cx| {
// Clear any existing highlights
this.clear_text_highlights::<LinkGoToDefinitionState>(cx);
this.link_go_to_definition_state.symbol_range = result
.as_ref()
.and_then(|(symbol_range, _)| symbol_range.clone());
if let Some((symbol_range, definitions)) = result {
this.link_go_to_definition_state.definitions = definitions.clone();
let buffer_snapshot = buffer.read(cx).snapshot();
// Only show highlight if there exists a definition to jump to that doesn't contain
// the current location.
if definitions.iter().any(|definition| {
let target = &definition.target;
if target.buffer == buffer {
let range = &target.range;
// Expand range by one character as lsp definition ranges include positions adjacent
// but not contained by the symbol range
let start = buffer_snapshot.clip_offset(
range.start.to_offset(&buffer_snapshot).saturating_sub(1),
Bias::Left,
);
let end = buffer_snapshot.clip_offset(
range.end.to_offset(&buffer_snapshot) + 1,
Bias::Right,
);
let offset = buffer_position.to_offset(&buffer_snapshot);
!(start <= offset && end >= offset)
} else {
true
}
}) {
// If no symbol range returned from language server, use the surrounding word.
let highlight_range = symbol_range.unwrap_or_else(|| {
let snapshot = &snapshot.buffer_snapshot;
let (offset_range, _) = snapshot.surrounding_word(trigger_point);
snapshot.anchor_before(offset_range.start)
..snapshot.anchor_after(offset_range.end)
});
// Highlight symbol using theme link definition highlight style
let style = cx.global::<Settings>().theme.editor.link_definition;
this.highlight_text::<LinkGoToDefinitionState>(
vec![highlight_range],
style,
cx,
)
} else {
hide_link_definition(this, cx);
}
}
})
}
Ok::<_, anyhow::Error>(())
}
.log_err()
});
editor.link_go_to_definition_state.task = Some(task);
}
pub fn hide_link_definition(editor: &mut Editor, cx: &mut ViewContext<Editor>) {
if editor.link_go_to_definition_state.symbol_range.is_some()
|| !editor.link_go_to_definition_state.definitions.is_empty()
{
editor.link_go_to_definition_state.symbol_range.take();
editor.link_go_to_definition_state.definitions.clear();
cx.notify();
}
editor.link_go_to_definition_state.task = None;
editor.clear_text_highlights::<LinkGoToDefinitionState>(cx);
}
pub fn go_to_fetched_definition(
workspace: &mut Workspace,
GoToFetchedDefinition { point }: &GoToFetchedDefinition,
cx: &mut ViewContext<Workspace>,
) {
let active_item = workspace.active_item(cx);
let editor_handle = if let Some(editor) = active_item
.as_ref()
.and_then(|item| item.act_as::<Editor>(cx))
{
editor
} else {
return;
};
let definitions = editor_handle.update(cx, |editor, cx| {
let definitions = editor.link_go_to_definition_state.definitions.clone();
hide_link_definition(editor, cx);
definitions
});
if !definitions.is_empty() {
Editor::navigate_to_definitions(workspace, editor_handle, definitions, cx);
} else {
editor_handle.update(cx, |editor, cx| {
editor.select(
&Select(SelectPhase::Begin {
position: point.clone(),
add: false,
click_count: 1,
}),
cx,
);
});
Editor::go_to_definition(workspace, &GoToDefinition, cx);
}
}
#[cfg(test)]
mod tests {
use futures::StreamExt;
use indoc::indoc;
use crate::test::EditorLspTestContext;
use super::*;
#[gpui::test]
async fn test_link_go_to_definition(cx: &mut gpui::TestAppContext) {
let mut cx = EditorLspTestContext::new_rust(
lsp::ServerCapabilities {
hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
..Default::default()
},
cx,
)
.await;
cx.set_state(indoc! {"
fn |test()
do_work();
fn do_work()
test();"});
// Basic hold cmd, expect highlight in region if response contains definition
let hover_point = cx.display_point(indoc! {"
fn test()
do_w|ork();
fn do_work()
test();"});
let symbol_range = cx.lsp_range(indoc! {"
fn test()
[do_work]();
fn do_work()
test();"});
let target_range = cx.lsp_range(indoc! {"
fn test()
do_work();
fn [do_work]()
test();"});
let mut requests =
cx.lsp
.handle_request::<lsp::request::GotoDefinition, _, _>(move |_, _| async move {
Ok(Some(lsp::GotoDefinitionResponse::Link(vec![
lsp::LocationLink {
origin_selection_range: Some(symbol_range),
target_uri: lsp::Url::from_file_path("/root/dir/file.rs").unwrap(),
target_range,
target_selection_range: target_range,
},
])))
});
cx.update_editor(|editor, cx| {
update_go_to_definition_link(
editor,
&UpdateGoToDefinitionLink {
point: Some(hover_point),
cmd_held: true,
},
cx,
);
});
requests.next().await;
cx.foreground().run_until_parked();
cx.assert_editor_text_highlights::<LinkGoToDefinitionState>(indoc! {"
fn test()
[do_work]();
fn do_work()
test();"});
// Unpress cmd causes highlight to go away
cx.update_editor(|editor, cx| {
cmd_changed(editor, &CmdChanged { cmd_down: false }, cx);
});
// Assert no link highlights
cx.assert_editor_text_highlights::<LinkGoToDefinitionState>(indoc! {"
fn test()
do_work();
fn do_work()
test();"});
// Response without source range still highlights word
cx.update_editor(|editor, _| editor.link_go_to_definition_state.last_mouse_location = None);
let mut requests =
cx.lsp
.handle_request::<lsp::request::GotoDefinition, _, _>(move |_, _| async move {
Ok(Some(lsp::GotoDefinitionResponse::Link(vec![
lsp::LocationLink {
// No origin range
origin_selection_range: None,
target_uri: lsp::Url::from_file_path("/root/dir/file.rs").unwrap(),
target_range,
target_selection_range: target_range,
},
])))
});
cx.update_editor(|editor, cx| {
update_go_to_definition_link(
editor,
&UpdateGoToDefinitionLink {
point: Some(hover_point),
cmd_held: true,
},
cx,
);
});
requests.next().await;
cx.foreground().run_until_parked();
println!("tag");
cx.assert_editor_text_highlights::<LinkGoToDefinitionState>(indoc! {"
fn test()
[do_work]();
fn do_work()
test();"});
// Moving mouse to location with no response dismisses highlight
let hover_point = cx.display_point(indoc! {"
f|n test()
do_work();
fn do_work()
test();"});
let mut requests =
cx.lsp
.handle_request::<lsp::request::GotoDefinition, _, _>(move |_, _| async move {
// No definitions returned
Ok(Some(lsp::GotoDefinitionResponse::Link(vec![])))
});
cx.update_editor(|editor, cx| {
update_go_to_definition_link(
editor,
&UpdateGoToDefinitionLink {
point: Some(hover_point),
cmd_held: true,
},
cx,
);
});
requests.next().await;
cx.foreground().run_until_parked();
// Assert no link highlights
cx.assert_editor_text_highlights::<LinkGoToDefinitionState>(indoc! {"
fn test()
do_work();
fn do_work()
test();"});
// Move mouse without cmd and then pressing cmd triggers highlight
let hover_point = cx.display_point(indoc! {"
fn test()
do_work();
fn do_work()
te|st();"});
cx.update_editor(|editor, cx| {
update_go_to_definition_link(
editor,
&UpdateGoToDefinitionLink {
point: Some(hover_point),
cmd_held: false,
},
cx,
);
});
cx.foreground().run_until_parked();
// Assert no link highlights
cx.assert_editor_text_highlights::<LinkGoToDefinitionState>(indoc! {"
fn test()
do_work();
fn do_work()
test();"});
let symbol_range = cx.lsp_range(indoc! {"
fn test()
do_work();
fn do_work()
[test]();"});
let target_range = cx.lsp_range(indoc! {"
fn [test]()
do_work();
fn do_work()
test();"});
let mut requests =
cx.lsp
.handle_request::<lsp::request::GotoDefinition, _, _>(move |_, _| async move {
Ok(Some(lsp::GotoDefinitionResponse::Link(vec![
lsp::LocationLink {
origin_selection_range: Some(symbol_range),
target_uri: lsp::Url::from_file_path("/root/dir/file.rs").unwrap(),
target_range,
target_selection_range: target_range,
},
])))
});
cx.update_editor(|editor, cx| {
cmd_changed(editor, &CmdChanged { cmd_down: true }, cx);
});
requests.next().await;
cx.foreground().run_until_parked();
cx.assert_editor_text_highlights::<LinkGoToDefinitionState>(indoc! {"
fn test()
do_work();
fn do_work()
[test]();"});
// Moving within symbol range doesn't re-request
let hover_point = cx.display_point(indoc! {"
fn test()
do_work();
fn do_work()
tes|t();"});
cx.update_editor(|editor, cx| {
update_go_to_definition_link(
editor,
&UpdateGoToDefinitionLink {
point: Some(hover_point),
cmd_held: true,
},
cx,
);
});
cx.foreground().run_until_parked();
cx.assert_editor_text_highlights::<LinkGoToDefinitionState>(indoc! {"
fn test()
do_work();
fn do_work()
[test]();"});
// Cmd click with existing definition doesn't re-request and dismisses highlight
cx.update_workspace(|workspace, cx| {
go_to_fetched_definition(workspace, &GoToFetchedDefinition { point: hover_point }, cx);
});
// Assert selection moved to to definition
cx.lsp
.handle_request::<lsp::request::GotoDefinition, _, _>(move |_, _| async move {
// Empty definition response to make sure we aren't hitting the lsp and using
// the cached location instead
Ok(Some(lsp::GotoDefinitionResponse::Link(vec![])))
});
cx.assert_editor_state(indoc! {"
fn [test}()
do_work();
fn do_work()
test();"});
// Assert no link highlights after jump
cx.assert_editor_text_highlights::<LinkGoToDefinitionState>(indoc! {"
fn test()
do_work();
fn do_work()
test();"});
// Cmd click without existing definition requests and jumps
let hover_point = cx.display_point(indoc! {"
fn test()
do_w|ork();
fn do_work()
test();"});
let target_range = cx.lsp_range(indoc! {"
fn test()
do_work();
fn [do_work]()
test();"});
let mut requests =
cx.lsp
.handle_request::<lsp::request::GotoDefinition, _, _>(move |_, _| async move {
Ok(Some(lsp::GotoDefinitionResponse::Link(vec![
lsp::LocationLink {
origin_selection_range: None,
target_uri: lsp::Url::from_file_path("/root/dir/file.rs").unwrap(),
target_range,
target_selection_range: target_range,
},
])))
});
cx.update_workspace(|workspace, cx| {
go_to_fetched_definition(workspace, &GoToFetchedDefinition { point: hover_point }, cx);
});
requests.next().await;
cx.foreground().run_until_parked();
cx.assert_editor_state(indoc! {"
fn test()
do_work();
fn [do_work}()
test();"});
}
}

View file

@ -7,19 +7,20 @@ use futures::StreamExt;
use indoc::indoc; use indoc::indoc;
use collections::BTreeMap; use collections::BTreeMap;
use gpui::{keymap::Keystroke, AppContext, ModelHandle, ViewContext, ViewHandle}; use gpui::{json, keymap::Keystroke, AppContext, ModelHandle, ViewContext, ViewHandle};
use language::{point_to_lsp, FakeLspAdapter, Language, LanguageConfig, Selection}; use language::{point_to_lsp, FakeLspAdapter, Language, LanguageConfig, Selection};
use project::{FakeFs, Project}; use project::Project;
use settings::Settings; use settings::Settings;
use util::{ use util::{
set_eq, assert_set_eq, set_eq,
test::{marked_text, marked_text_ranges, marked_text_ranges_by, SetEqError}, test::{marked_text, marked_text_ranges, marked_text_ranges_by, SetEqError},
}; };
use workspace::{pane, AppState, Workspace, WorkspaceHandle};
use crate::{ use crate::{
display_map::{DisplayMap, DisplaySnapshot, ToDisplayPoint}, display_map::{DisplayMap, DisplaySnapshot, ToDisplayPoint},
multi_buffer::ToPointUtf16, multi_buffer::ToPointUtf16,
Autoscroll, DisplayPoint, Editor, EditorMode, MultiBuffer, ToPoint, AnchorRangeExt, Autoscroll, DisplayPoint, Editor, EditorMode, MultiBuffer, ToPoint,
}; };
#[cfg(test)] #[cfg(test)]
@ -215,6 +216,24 @@ impl<'a> EditorTestContext<'a> {
) )
} }
pub fn assert_editor_text_highlights<Tag: ?Sized + 'static>(&mut self, marked_text: &str) {
let (unmarked, mut ranges) = marked_text_ranges_by(marked_text, vec![('[', ']').into()]);
assert_eq!(unmarked, self.buffer_text());
let asserted_ranges = ranges.remove(&('[', ']').into()).unwrap();
let snapshot = self.update_editor(|editor, cx| editor.snapshot(cx));
let actual_ranges: Vec<Range<usize>> = snapshot
.display_snapshot
.highlight_ranges::<Tag>()
.map(|ranges| ranges.as_ref().clone().1)
.unwrap_or_default()
.into_iter()
.map(|range| range.to_offset(&snapshot.buffer_snapshot))
.collect();
assert_set_eq!(asserted_ranges, actual_ranges);
}
pub fn assert_editor_selections(&mut self, expected_selections: Vec<Selection<usize>>) { pub fn assert_editor_selections(&mut self, expected_selections: Vec<Selection<usize>>) {
let mut empty_selections = Vec::new(); let mut empty_selections = Vec::new();
let mut reverse_selections = Vec::new(); let mut reverse_selections = Vec::new();
@ -390,6 +409,7 @@ impl<'a> DerefMut for EditorTestContext<'a> {
pub struct EditorLspTestContext<'a> { pub struct EditorLspTestContext<'a> {
pub cx: EditorTestContext<'a>, pub cx: EditorTestContext<'a>,
pub lsp: lsp::FakeLanguageServer, pub lsp: lsp::FakeLanguageServer,
pub workspace: ViewHandle<Workspace>,
} }
impl<'a> EditorLspTestContext<'a> { impl<'a> EditorLspTestContext<'a> {
@ -398,8 +418,17 @@ impl<'a> EditorLspTestContext<'a> {
capabilities: lsp::ServerCapabilities, capabilities: lsp::ServerCapabilities,
cx: &'a mut gpui::TestAppContext, cx: &'a mut gpui::TestAppContext,
) -> EditorLspTestContext<'a> { ) -> EditorLspTestContext<'a> {
use json::json;
cx.update(|cx| {
crate::init(cx);
pane::init(cx);
});
let params = cx.update(AppState::test);
let file_name = format!( let file_name = format!(
"/file.{}", "file.{}",
language language
.path_suffixes() .path_suffixes()
.first() .first()
@ -411,30 +440,36 @@ impl<'a> EditorLspTestContext<'a> {
..Default::default() ..Default::default()
}); });
let fs = FakeFs::new(cx.background().clone()); let project = Project::test(params.fs.clone(), [], cx).await;
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))); project.update(cx, |project, _| project.languages().add(Arc::new(language)));
let buffer = project
.update(cx, |project, cx| project.open_local_buffer(file_name, cx)) params
.fs
.as_fake()
.insert_tree("/root", json!({ "dir": { file_name: "" }}))
.await;
let (window_id, workspace) = cx.add_window(|cx| Workspace::new(project.clone(), cx));
project
.update(cx, |project, cx| {
project.find_or_create_local_worktree("/root", true, cx)
})
.await .await
.unwrap(); .unwrap();
cx.read(|cx| workspace.read(cx).worktree_scans_complete(cx))
.await;
let (window_id, editor) = cx.update(|cx| { let file = cx.read(|cx| workspace.file_project_paths(cx)[0].clone());
cx.set_global(Settings::test(cx)); let item = workspace
crate::init(cx); .update(cx, |workspace, cx| workspace.open_path(file, true, cx))
.await
.expect("Could not open test file");
let (window_id, editor) = cx.add_window(Default::default(), |cx| { let editor = cx.update(|cx| {
let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx)); item.act_as::<Editor>(cx)
.expect("Opened test file wasn't an editor")
Editor::new(EditorMode::Full, buffer, Some(project), None, cx)
});
editor.update(cx, |_, cx| cx.focus_self());
(window_id, editor)
}); });
editor.update(cx, |_, cx| cx.focus_self());
let lsp = fake_servers.next().await.unwrap(); let lsp = fake_servers.next().await.unwrap();
@ -445,6 +480,7 @@ impl<'a> EditorLspTestContext<'a> {
editor, editor,
}, },
lsp, lsp,
workspace,
} }
} }
@ -493,6 +529,13 @@ impl<'a> EditorLspTestContext<'a> {
lsp::Range { start, end } lsp::Range { start, end }
}) })
} }
pub fn update_workspace<F, T>(&mut self, update: F) -> T
where
F: FnOnce(&mut Workspace, &mut ViewContext<Workspace>) -> T,
{
self.workspace.update(self.cx.cx, update)
}
} }
impl<'a> Deref for EditorLspTestContext<'a> { impl<'a> Deref for EditorLspTestContext<'a> {

View file

@ -13,6 +13,16 @@ pub enum Event {
input: Option<String>, input: Option<String>,
is_held: bool, is_held: bool,
}, },
KeyUp {
keystroke: Keystroke,
input: Option<String>,
},
ModifiersChanged {
ctrl: bool,
alt: bool,
shift: bool,
cmd: bool,
},
ScrollWheel { ScrollWheel {
position: Vector2F, position: Vector2F,
delta: Vector2F, delta: Vector2F,
@ -32,6 +42,10 @@ pub enum Event {
}, },
LeftMouseDragged { LeftMouseDragged {
position: Vector2F, position: Vector2F,
ctrl: bool,
alt: bool,
shift: bool,
cmd: bool,
}, },
RightMouseDown { RightMouseDown {
position: Vector2F, position: Vector2F,
@ -61,6 +75,10 @@ pub enum Event {
MouseMoved { MouseMoved {
position: Vector2F, position: Vector2F,
left_mouse_down: bool, left_mouse_down: bool,
ctrl: bool,
cmd: bool,
alt: bool,
shift: bool,
}, },
} }
@ -68,10 +86,12 @@ impl Event {
pub fn position(&self) -> Option<Vector2F> { pub fn position(&self) -> Option<Vector2F> {
match self { match self {
Event::KeyDown { .. } => None, Event::KeyDown { .. } => None,
Event::KeyUp { .. } => None,
Event::ModifiersChanged { .. } => None,
Event::ScrollWheel { position, .. } Event::ScrollWheel { position, .. }
| Event::LeftMouseDown { position, .. } | Event::LeftMouseDown { position, .. }
| Event::LeftMouseUp { position, .. } | Event::LeftMouseUp { position, .. }
| Event::LeftMouseDragged { position } | Event::LeftMouseDragged { position, .. }
| Event::RightMouseDown { position, .. } | Event::RightMouseDown { position, .. }
| Event::RightMouseUp { position, .. } | Event::RightMouseUp { position, .. }
| Event::NavigateMouseDown { position, .. } | Event::NavigateMouseDown { position, .. }

View file

@ -52,6 +52,20 @@ impl Event {
} }
match event_type { match event_type {
NSEventType::NSFlagsChanged => {
let modifiers = native_event.modifierFlags();
let ctrl = modifiers.contains(NSEventModifierFlags::NSControlKeyMask);
let alt = modifiers.contains(NSEventModifierFlags::NSAlternateKeyMask);
let shift = modifiers.contains(NSEventModifierFlags::NSShiftKeyMask);
let cmd = modifiers.contains(NSEventModifierFlags::NSCommandKeyMask);
Some(Self::ModifiersChanged {
ctrl,
alt,
shift,
cmd,
})
}
NSEventType::NSKeyDown => { NSEventType::NSKeyDown => {
let modifiers = native_event.modifierFlags(); let modifiers = native_event.modifierFlags();
let ctrl = modifiers.contains(NSEventModifierFlags::NSControlKeyMask); let ctrl = modifiers.contains(NSEventModifierFlags::NSControlKeyMask);
@ -60,71 +74,7 @@ impl Event {
let cmd = modifiers.contains(NSEventModifierFlags::NSCommandKeyMask); let cmd = modifiers.contains(NSEventModifierFlags::NSCommandKeyMask);
let function = modifiers.contains(NSEventModifierFlags::NSFunctionKeyMask); let function = modifiers.contains(NSEventModifierFlags::NSFunctionKeyMask);
let unmodified_chars = CStr::from_ptr( let (unmodified_chars, input) = get_key_text(native_event, cmd, ctrl, function)?;
native_event.charactersIgnoringModifiers().UTF8String() as *mut c_char,
)
.to_str()
.unwrap();
let mut input = None;
let unmodified_chars = if let Some(first_char) = unmodified_chars.chars().next() {
use cocoa::appkit::*;
const BACKSPACE_KEY: u16 = 0x7f;
const ENTER_KEY: u16 = 0x0d;
const ESCAPE_KEY: u16 = 0x1b;
const TAB_KEY: u16 = 0x09;
const SHIFT_TAB_KEY: u16 = 0x19;
const SPACE_KEY: u16 = b' ' as u16;
#[allow(non_upper_case_globals)]
match first_char as u16 {
SPACE_KEY => {
input = Some(" ".to_string());
"space"
}
BACKSPACE_KEY => "backspace",
ENTER_KEY => "enter",
ESCAPE_KEY => "escape",
TAB_KEY => "tab",
SHIFT_TAB_KEY => "tab",
NSUpArrowFunctionKey => "up",
NSDownArrowFunctionKey => "down",
NSLeftArrowFunctionKey => "left",
NSRightArrowFunctionKey => "right",
NSPageUpFunctionKey => "pageup",
NSPageDownFunctionKey => "pagedown",
NSDeleteFunctionKey => "delete",
NSF1FunctionKey => "f1",
NSF2FunctionKey => "f2",
NSF3FunctionKey => "f3",
NSF4FunctionKey => "f4",
NSF5FunctionKey => "f5",
NSF6FunctionKey => "f6",
NSF7FunctionKey => "f7",
NSF8FunctionKey => "f8",
NSF9FunctionKey => "f9",
NSF10FunctionKey => "f10",
NSF11FunctionKey => "f11",
NSF12FunctionKey => "f12",
_ => {
if !cmd && !ctrl && !function {
input = Some(
CStr::from_ptr(
native_event.characters().UTF8String() as *mut c_char
)
.to_str()
.unwrap()
.into(),
);
}
unmodified_chars
}
}
} else {
return None;
};
Some(Self::KeyDown { Some(Self::KeyDown {
keystroke: Keystroke { keystroke: Keystroke {
@ -138,6 +88,27 @@ impl Event {
is_held: native_event.isARepeat() == YES, is_held: native_event.isARepeat() == YES,
}) })
} }
NSEventType::NSKeyUp => {
let modifiers = native_event.modifierFlags();
let ctrl = modifiers.contains(NSEventModifierFlags::NSControlKeyMask);
let alt = modifiers.contains(NSEventModifierFlags::NSAlternateKeyMask);
let shift = modifiers.contains(NSEventModifierFlags::NSShiftKeyMask);
let cmd = modifiers.contains(NSEventModifierFlags::NSCommandKeyMask);
let function = modifiers.contains(NSEventModifierFlags::NSFunctionKeyMask);
let (unmodified_chars, input) = get_key_text(native_event, cmd, ctrl, function)?;
Some(Self::KeyUp {
keystroke: Keystroke {
ctrl,
alt,
shift,
cmd,
key: unmodified_chars.into(),
},
input,
})
}
NSEventType::NSLeftMouseDown => { NSEventType::NSLeftMouseDown => {
let modifiers = native_event.modifierFlags(); let modifiers = native_event.modifierFlags();
window_height.map(|window_height| Self::LeftMouseDown { window_height.map(|window_height| Self::LeftMouseDown {
@ -218,14 +189,19 @@ impl Event {
direction, direction,
}) })
} }
NSEventType::NSLeftMouseDragged => { NSEventType::NSLeftMouseDragged => window_height.map(|window_height| {
window_height.map(|window_height| Self::LeftMouseDragged { let modifiers = native_event.modifierFlags();
Self::LeftMouseDragged {
position: vec2f( position: vec2f(
native_event.locationInWindow().x as f32, native_event.locationInWindow().x as f32,
window_height - native_event.locationInWindow().y as f32, window_height - native_event.locationInWindow().y as f32,
), ),
}) ctrl: modifiers.contains(NSEventModifierFlags::NSControlKeyMask),
} alt: modifiers.contains(NSEventModifierFlags::NSAlternateKeyMask),
shift: modifiers.contains(NSEventModifierFlags::NSShiftKeyMask),
cmd: modifiers.contains(NSEventModifierFlags::NSCommandKeyMask),
}
}),
NSEventType::NSScrollWheel => window_height.map(|window_height| Self::ScrollWheel { NSEventType::NSScrollWheel => window_height.map(|window_height| Self::ScrollWheel {
position: vec2f( position: vec2f(
native_event.locationInWindow().x as f32, native_event.locationInWindow().x as f32,
@ -237,14 +213,90 @@ impl Event {
), ),
precise: native_event.hasPreciseScrollingDeltas() == YES, precise: native_event.hasPreciseScrollingDeltas() == YES,
}), }),
NSEventType::NSMouseMoved => window_height.map(|window_height| Self::MouseMoved { NSEventType::NSMouseMoved => window_height.map(|window_height| {
position: vec2f( let modifiers = native_event.modifierFlags();
native_event.locationInWindow().x as f32, Self::MouseMoved {
window_height - native_event.locationInWindow().y as f32, position: vec2f(
), native_event.locationInWindow().x as f32,
left_mouse_down: NSEvent::pressedMouseButtons(nil) & 1 != 0, window_height - native_event.locationInWindow().y as f32,
),
left_mouse_down: NSEvent::pressedMouseButtons(nil) & 1 != 0,
ctrl: modifiers.contains(NSEventModifierFlags::NSControlKeyMask),
alt: modifiers.contains(NSEventModifierFlags::NSAlternateKeyMask),
shift: modifiers.contains(NSEventModifierFlags::NSShiftKeyMask),
cmd: modifiers.contains(NSEventModifierFlags::NSCommandKeyMask),
}
}), }),
_ => None, _ => None,
} }
} }
} }
unsafe fn get_key_text(
native_event: id,
cmd: bool,
ctrl: bool,
function: bool,
) -> Option<(&'static str, Option<String>)> {
let unmodified_chars =
CStr::from_ptr(native_event.charactersIgnoringModifiers().UTF8String() as *mut c_char)
.to_str()
.unwrap();
let mut input = None;
let first_char = unmodified_chars.chars().next()?;
use cocoa::appkit::*;
const BACKSPACE_KEY: u16 = 0x7f;
const ENTER_KEY: u16 = 0x0d;
const ESCAPE_KEY: u16 = 0x1b;
const TAB_KEY: u16 = 0x09;
const SHIFT_TAB_KEY: u16 = 0x19;
const SPACE_KEY: u16 = b' ' as u16;
#[allow(non_upper_case_globals)]
let unmodified_chars = match first_char as u16 {
SPACE_KEY => {
input = Some(" ".to_string());
"space"
}
BACKSPACE_KEY => "backspace",
ENTER_KEY => "enter",
ESCAPE_KEY => "escape",
TAB_KEY => "tab",
SHIFT_TAB_KEY => "tab",
NSUpArrowFunctionKey => "up",
NSDownArrowFunctionKey => "down",
NSLeftArrowFunctionKey => "left",
NSRightArrowFunctionKey => "right",
NSPageUpFunctionKey => "pageup",
NSPageDownFunctionKey => "pagedown",
NSDeleteFunctionKey => "delete",
NSF1FunctionKey => "f1",
NSF2FunctionKey => "f2",
NSF3FunctionKey => "f3",
NSF4FunctionKey => "f4",
NSF5FunctionKey => "f5",
NSF6FunctionKey => "f6",
NSF7FunctionKey => "f7",
NSF8FunctionKey => "f8",
NSF9FunctionKey => "f9",
NSF10FunctionKey => "f10",
NSF11FunctionKey => "f11",
NSF12FunctionKey => "f12",
_ => {
if !cmd && !ctrl && !function {
input = Some(
CStr::from_ptr(native_event.characters().UTF8String() as *mut c_char)
.to_str()
.unwrap()
.into(),
);
}
unmodified_chars
}
};
Some((unmodified_chars, input))
}

View file

@ -135,6 +135,10 @@ unsafe fn build_classes() {
sel!(scrollWheel:), sel!(scrollWheel:),
handle_view_event as extern "C" fn(&Object, Sel, id), handle_view_event as extern "C" fn(&Object, Sel, id),
); );
decl.add_method(
sel!(flagsChanged:),
handle_view_event as extern "C" fn(&Object, Sel, id),
);
decl.add_method( decl.add_method(
sel!(cancelOperation:), sel!(cancelOperation:),
cancel_operation as extern "C" fn(&Object, Sel, id), cancel_operation as extern "C" fn(&Object, Sel, id),
@ -181,6 +185,7 @@ struct WindowState {
last_fresh_keydown: Option<(Keystroke, Option<String>)>, last_fresh_keydown: Option<(Keystroke, Option<String>)>,
layer: id, layer: id,
traffic_light_position: Option<Vector2F>, traffic_light_position: Option<Vector2F>,
previous_modifiers_changed_event: Option<Event>,
} }
impl Window { impl Window {
@ -263,6 +268,7 @@ impl Window {
last_fresh_keydown: None, last_fresh_keydown: None,
layer, layer,
traffic_light_position: options.traffic_light_position, traffic_light_position: options.traffic_light_position,
previous_modifiers_changed_event: None,
}))); })));
(*native_window).set_ivar( (*native_window).set_ivar(
@ -597,7 +603,7 @@ extern "C" fn handle_view_event(this: &Object, _: Sel, native_event: id) {
if let Some(event) = event { if let Some(event) = event {
match &event { match &event {
Event::LeftMouseDragged { position } => { Event::LeftMouseDragged { position, .. } => {
window_state_borrow.synthetic_drag_counter += 1; window_state_borrow.synthetic_drag_counter += 1;
window_state_borrow window_state_borrow
.executor .executor
@ -611,6 +617,31 @@ extern "C" fn handle_view_event(this: &Object, _: Sel, native_event: id) {
Event::LeftMouseUp { .. } => { Event::LeftMouseUp { .. } => {
window_state_borrow.synthetic_drag_counter += 1; window_state_borrow.synthetic_drag_counter += 1;
} }
Event::ModifiersChanged {
ctrl,
alt,
shift,
cmd,
} => {
// Only raise modifiers changed event when they have actually changed
if let Some(Event::ModifiersChanged {
ctrl: prev_ctrl,
alt: prev_alt,
shift: prev_shift,
cmd: prev_cmd,
}) = &window_state_borrow.previous_modifiers_changed_event
{
if prev_ctrl == ctrl
&& prev_alt == alt
&& prev_shift == shift
&& prev_cmd == cmd
{
return;
}
}
window_state_borrow.previous_modifiers_changed_event = Some(event.clone());
}
_ => {} _ => {}
} }
@ -805,7 +836,14 @@ async fn synthetic_drag(
if window_state_borrow.synthetic_drag_counter == drag_id { if window_state_borrow.synthetic_drag_counter == drag_id {
if let Some(mut callback) = window_state_borrow.event_callback.take() { if let Some(mut callback) = window_state_borrow.event_callback.take() {
drop(window_state_borrow); drop(window_state_borrow);
callback(Event::LeftMouseDragged { position }); callback(Event::LeftMouseDragged {
// TODO: Make sure empty modifiers is correct for this
position,
shift: false,
ctrl: false,
alt: false,
cmd: false,
});
window_state.borrow_mut().event_callback = Some(callback); window_state.borrow_mut().event_callback = Some(callback);
} }
} else { } else {

View file

@ -294,7 +294,13 @@ impl Presenter {
Event::MouseMoved { .. } => { Event::MouseMoved { .. } => {
self.last_mouse_moved_event = Some(event.clone()); self.last_mouse_moved_event = Some(event.clone());
} }
Event::LeftMouseDragged { position } => { Event::LeftMouseDragged {
position,
shift,
ctrl,
alt,
cmd,
} => {
if let Some((clicked_region, prev_drag_position)) = self if let Some((clicked_region, prev_drag_position)) = self
.clicked_region .clicked_region
.as_ref() .as_ref()
@ -308,6 +314,10 @@ impl Presenter {
self.last_mouse_moved_event = Some(Event::MouseMoved { self.last_mouse_moved_event = Some(Event::MouseMoved {
position, position,
left_mouse_down: true, left_mouse_down: true,
shift,
ctrl,
alt,
cmd,
}); });
} }
_ => {} _ => {}
@ -403,6 +413,7 @@ impl Presenter {
if let Event::MouseMoved { if let Event::MouseMoved {
position, position,
left_mouse_down, left_mouse_down,
..
} = event } = event
{ {
if !left_mouse_down { if !left_mouse_down {

View file

@ -1,4 +1,6 @@
use crate::{DocumentHighlight, Hover, HoverBlock, Location, Project, ProjectTransaction}; use crate::{
DocumentHighlight, Hover, HoverBlock, Location, LocationLink, Project, ProjectTransaction,
};
use anyhow::{anyhow, Result}; use anyhow::{anyhow, Result};
use async_trait::async_trait; use async_trait::async_trait;
use client::{proto, PeerId}; use client::{proto, PeerId};
@ -328,7 +330,7 @@ impl LspCommand for PerformRename {
#[async_trait(?Send)] #[async_trait(?Send)]
impl LspCommand for GetDefinition { impl LspCommand for GetDefinition {
type Response = Vec<Location>; type Response = Vec<LocationLink>;
type LspRequest = lsp::request::GotoDefinition; type LspRequest = lsp::request::GotoDefinition;
type ProtoRequest = proto::GetDefinition; type ProtoRequest = proto::GetDefinition;
@ -351,7 +353,7 @@ impl LspCommand for GetDefinition {
project: ModelHandle<Project>, project: ModelHandle<Project>,
buffer: ModelHandle<Buffer>, buffer: ModelHandle<Buffer>,
mut cx: AsyncAppContext, mut cx: AsyncAppContext,
) -> Result<Vec<Location>> { ) -> Result<Vec<LocationLink>> {
let mut definitions = Vec::new(); let mut definitions = Vec::new();
let (lsp_adapter, language_server) = project let (lsp_adapter, language_server) = project
.read_with(&cx, |project, cx| { .read_with(&cx, |project, cx| {
@ -362,24 +364,26 @@ impl LspCommand for GetDefinition {
.ok_or_else(|| anyhow!("no language server found for buffer"))?; .ok_or_else(|| anyhow!("no language server found for buffer"))?;
if let Some(message) = message { if let Some(message) = message {
let mut unresolved_locations = Vec::new(); let mut unresolved_links = Vec::new();
match message { match message {
lsp::GotoDefinitionResponse::Scalar(loc) => { lsp::GotoDefinitionResponse::Scalar(loc) => {
unresolved_locations.push((loc.uri, loc.range)); unresolved_links.push((None, loc.uri, loc.range));
} }
lsp::GotoDefinitionResponse::Array(locs) => { lsp::GotoDefinitionResponse::Array(locs) => {
unresolved_locations.extend(locs.into_iter().map(|l| (l.uri, l.range))); unresolved_links.extend(locs.into_iter().map(|l| (None, l.uri, l.range)));
} }
lsp::GotoDefinitionResponse::Link(links) => { lsp::GotoDefinitionResponse::Link(links) => {
unresolved_locations.extend( unresolved_links.extend(links.into_iter().map(|l| {
links (
.into_iter() l.origin_selection_range,
.map(|l| (l.target_uri, l.target_selection_range)), l.target_uri,
); l.target_selection_range,
)
}));
} }
} }
for (target_uri, target_range) in unresolved_locations { for (origin_range, target_uri, target_range) in unresolved_links {
let target_buffer_handle = project let target_buffer_handle = project
.update(&mut cx, |this, cx| { .update(&mut cx, |this, cx| {
this.open_local_buffer_via_lsp( this.open_local_buffer_via_lsp(
@ -392,16 +396,34 @@ impl LspCommand for GetDefinition {
.await?; .await?;
cx.read(|cx| { cx.read(|cx| {
let origin_location = origin_range.map(|origin_range| {
let origin_buffer = buffer.read(cx);
let origin_start = origin_buffer
.clip_point_utf16(point_from_lsp(origin_range.start), Bias::Left);
let origin_end = origin_buffer
.clip_point_utf16(point_from_lsp(origin_range.end), Bias::Left);
Location {
buffer: buffer.clone(),
range: origin_buffer.anchor_after(origin_start)
..origin_buffer.anchor_before(origin_end),
}
});
let target_buffer = target_buffer_handle.read(cx); let target_buffer = target_buffer_handle.read(cx);
let target_start = target_buffer let target_start = target_buffer
.clip_point_utf16(point_from_lsp(target_range.start), Bias::Left); .clip_point_utf16(point_from_lsp(target_range.start), Bias::Left);
let target_end = target_buffer let target_end = target_buffer
.clip_point_utf16(point_from_lsp(target_range.end), Bias::Left); .clip_point_utf16(point_from_lsp(target_range.end), Bias::Left);
definitions.push(Location { let target_location = Location {
buffer: target_buffer_handle, buffer: target_buffer_handle,
range: target_buffer.anchor_after(target_start) range: target_buffer.anchor_after(target_start)
..target_buffer.anchor_before(target_end), ..target_buffer.anchor_before(target_end),
}); };
definitions.push(LocationLink {
origin: origin_location,
target: target_location,
})
}); });
} }
} }
@ -441,24 +463,39 @@ impl LspCommand for GetDefinition {
} }
fn response_to_proto( fn response_to_proto(
response: Vec<Location>, response: Vec<LocationLink>,
project: &mut Project, project: &mut Project,
peer_id: PeerId, peer_id: PeerId,
_: &clock::Global, _: &clock::Global,
cx: &AppContext, cx: &AppContext,
) -> proto::GetDefinitionResponse { ) -> proto::GetDefinitionResponse {
let locations = response let links = response
.into_iter() .into_iter()
.map(|definition| { .map(|definition| {
let buffer = project.serialize_buffer_for_peer(&definition.buffer, peer_id, cx); let origin = definition.origin.map(|origin| {
proto::Location { let buffer = project.serialize_buffer_for_peer(&origin.buffer, peer_id, cx);
start: Some(serialize_anchor(&definition.range.start)), proto::Location {
end: Some(serialize_anchor(&definition.range.end)), start: Some(serialize_anchor(&origin.range.start)),
end: Some(serialize_anchor(&origin.range.end)),
buffer: Some(buffer),
}
});
let buffer =
project.serialize_buffer_for_peer(&definition.target.buffer, peer_id, cx);
let target = proto::Location {
start: Some(serialize_anchor(&definition.target.range.start)),
end: Some(serialize_anchor(&definition.target.range.end)),
buffer: Some(buffer), buffer: Some(buffer),
};
proto::LocationLink {
origin,
target: Some(target),
} }
}) })
.collect(); .collect();
proto::GetDefinitionResponse { locations } proto::GetDefinitionResponse { links }
} }
async fn response_from_proto( async fn response_from_proto(
@ -467,30 +504,60 @@ impl LspCommand for GetDefinition {
project: ModelHandle<Project>, project: ModelHandle<Project>,
_: ModelHandle<Buffer>, _: ModelHandle<Buffer>,
mut cx: AsyncAppContext, mut cx: AsyncAppContext,
) -> Result<Vec<Location>> { ) -> Result<Vec<LocationLink>> {
let mut locations = Vec::new(); let mut links = Vec::new();
for location in message.locations { for link in message.links {
let buffer = location.buffer.ok_or_else(|| anyhow!("missing buffer"))?; let origin = match link.origin {
Some(origin) => {
let buffer = origin
.buffer
.ok_or_else(|| anyhow!("missing origin buffer"))?;
let buffer = project
.update(&mut cx, |this, cx| this.deserialize_buffer(buffer, cx))
.await?;
let start = origin
.start
.and_then(deserialize_anchor)
.ok_or_else(|| anyhow!("missing origin start"))?;
let end = origin
.end
.and_then(deserialize_anchor)
.ok_or_else(|| anyhow!("missing origin end"))?;
buffer
.update(&mut cx, |buffer, _| buffer.wait_for_anchors([&start, &end]))
.await;
Some(Location {
buffer,
range: start..end,
})
}
None => None,
};
let target = link.target.ok_or_else(|| anyhow!("missing target"))?;
let buffer = target.buffer.ok_or_else(|| anyhow!("missing buffer"))?;
let buffer = project let buffer = project
.update(&mut cx, |this, cx| this.deserialize_buffer(buffer, cx)) .update(&mut cx, |this, cx| this.deserialize_buffer(buffer, cx))
.await?; .await?;
let start = location let start = target
.start .start
.and_then(deserialize_anchor) .and_then(deserialize_anchor)
.ok_or_else(|| anyhow!("missing target start"))?; .ok_or_else(|| anyhow!("missing target start"))?;
let end = location let end = target
.end .end
.and_then(deserialize_anchor) .and_then(deserialize_anchor)
.ok_or_else(|| anyhow!("missing target end"))?; .ok_or_else(|| anyhow!("missing target end"))?;
buffer buffer
.update(&mut cx, |buffer, _| buffer.wait_for_anchors([&start, &end])) .update(&mut cx, |buffer, _| buffer.wait_for_anchors([&start, &end]))
.await; .await;
locations.push(Location { let target = Location {
buffer, buffer,
range: start..end, range: start..end,
}) };
links.push(LocationLink { origin, target })
} }
Ok(locations) Ok(links)
} }
fn buffer_id_from_proto(message: &proto::GetDefinition) -> u64 { fn buffer_id_from_proto(message: &proto::GetDefinition) -> u64 {

View file

@ -202,12 +202,18 @@ pub struct DiagnosticSummary {
pub warning_count: usize, pub warning_count: usize,
} }
#[derive(Debug)] #[derive(Debug, Clone)]
pub struct Location { pub struct Location {
pub buffer: ModelHandle<Buffer>, pub buffer: ModelHandle<Buffer>,
pub range: Range<language::Anchor>, pub range: Range<language::Anchor>,
} }
#[derive(Debug, Clone)]
pub struct LocationLink {
pub origin: Option<Location>,
pub target: Location,
}
#[derive(Debug)] #[derive(Debug)]
pub struct DocumentHighlight { pub struct DocumentHighlight {
pub range: Range<language::Anchor>, pub range: Range<language::Anchor>,
@ -2915,7 +2921,7 @@ impl Project {
buffer: &ModelHandle<Buffer>, buffer: &ModelHandle<Buffer>,
position: T, position: T,
cx: &mut ModelContext<Self>, cx: &mut ModelContext<Self>,
) -> Task<Result<Vec<Location>>> { ) -> Task<Result<Vec<LocationLink>>> {
let position = position.to_point_utf16(buffer.read(cx)); let position = position.to_point_utf16(buffer.read(cx));
self.request_lsp(buffer.clone(), GetDefinition { position }, cx) self.request_lsp(buffer.clone(), GetDefinition { position }, cx)
} }
@ -7564,7 +7570,7 @@ mod tests {
assert_eq!(definitions.len(), 1); assert_eq!(definitions.len(), 1);
let definition = definitions.pop().unwrap(); let definition = definitions.pop().unwrap();
cx.update(|cx| { cx.update(|cx| {
let target_buffer = definition.buffer.read(cx); let target_buffer = definition.target.buffer.read(cx);
assert_eq!( assert_eq!(
target_buffer target_buffer
.file() .file()
@ -7574,7 +7580,7 @@ mod tests {
.abs_path(cx), .abs_path(cx),
Path::new("/dir/a.rs"), Path::new("/dir/a.rs"),
); );
assert_eq!(definition.range.to_offset(target_buffer), 9..10); assert_eq!(definition.target.range.to_offset(target_buffer), 9..10);
assert_eq!( assert_eq!(
list_worktrees(&project, cx), list_worktrees(&project, cx),
[("/dir/b.rs".as_ref(), true), ("/dir/a.rs".as_ref(), false)] [("/dir/b.rs".as_ref(), true), ("/dir/a.rs".as_ref(), false)]

View file

@ -248,7 +248,7 @@ message GetDefinition {
} }
message GetDefinitionResponse { message GetDefinitionResponse {
repeated Location locations = 1; repeated LocationLink links = 1;
} }
message GetReferences { message GetReferences {
@ -279,6 +279,11 @@ message Location {
Anchor end = 3; Anchor end = 3;
} }
message LocationLink {
optional Location origin = 1;
Location target = 2;
}
message DocumentHighlight { message DocumentHighlight {
Kind kind = 1; Kind kind = 1;
Anchor start = 2; Anchor start = 2;

View file

@ -6,4 +6,4 @@ pub use conn::Connection;
pub use peer::*; pub use peer::*;
mod macros; mod macros;
pub const PROTOCOL_VERSION: u32 = 25; pub const PROTOCOL_VERSION: u32 = 26;

View file

@ -446,6 +446,7 @@ pub struct Editor {
pub code_actions_indicator: Color, pub code_actions_indicator: Color,
pub unnecessary_code_fade: f32, pub unnecessary_code_fade: f32,
pub hover_popover: HoverPopover, pub hover_popover: HoverPopover,
pub link_definition: HighlightStyle,
pub jump_icon: Interactive<IconButton>, pub jump_icon: Interactive<IconButton>,
} }

View file

@ -5,8 +5,7 @@
"main": "index.js", "main": "index.js",
"scripts": { "scripts": {
"build": "npm run build-themes && npm run build-tokens", "build": "npm run build-themes && npm run build-tokens",
"build-themes": "ts-node ./src/buildThemes.ts", "build-themes": "ts-node ./src/buildThemes.ts"
"build-tokens": "ts-node ./src/buildTokens.ts"
}, },
"author": "", "author": "",
"license": "ISC", "license": "ISC",

View file

@ -1,74 +0,0 @@
import * as fs from "fs";
import * as path from "path";
import themes from "./themes";
import Theme from "./themes/common/theme";
import { colors, fontFamilies, fontSizes, fontWeights, sizes } from "./tokens";
// Organize theme tokens
function themeTokens(theme: Theme) {
return {
meta: {
themeName: theme.name,
},
text: theme.textColor,
icon: theme.iconColor,
background: theme.backgroundColor,
border: theme.borderColor,
editor: theme.editor,
syntax: {
primary: theme.syntax.primary.color,
comment: theme.syntax.comment.color,
keyword: theme.syntax.keyword.color,
function: theme.syntax.function.color,
type: theme.syntax.type.color,
variant: theme.syntax.variant.color,
property: theme.syntax.property.color,
enum: theme.syntax.enum.color,
operator: theme.syntax.operator.color,
string: theme.syntax.string.color,
number: theme.syntax.number.color,
boolean: theme.syntax.boolean.color,
},
player: theme.player,
shadow: theme.shadow,
};
}
// Organize core tokens
const coreTokens = {
color: colors,
text: {
family: fontFamilies,
weight: fontWeights,
},
size: sizes,
fontSize: fontSizes,
};
const combinedTokens: any = {};
const distPath = path.resolve(`${__dirname}/../dist`);
for (const file of fs.readdirSync(distPath)) {
fs.unlinkSync(path.join(distPath, file));
}
// Add core tokens to the combined tokens and write `core.json`.
// We write `core.json` as a separate file for the design team's convenience, but it isn't consumed by Figma Tokens directly.
const corePath = path.join(distPath, "core.json");
fs.writeFileSync(corePath, JSON.stringify(coreTokens, null, 2));
console.log(`- ${corePath} created`);
combinedTokens.core = coreTokens;
// Add each theme to the combined tokens and write ${theme}.json.
// We write `${theme}.json` as a separate file for the design team's convenience, but it isn't consumed by Figma Tokens directly.
themes.forEach((theme) => {
const themePath = `${distPath}/${theme.name}.json`
fs.writeFileSync(themePath, JSON.stringify(themeTokens(theme), null, 2));
console.log(`- ${themePath} created`);
combinedTokens[theme.name] = themeTokens(theme);
});
// Write combined tokens to `tokens.json`. This file is consumed by the Figma Tokens plugin to keep our designs consistent with the app.
const combinedPath = path.resolve(`${distPath}/tokens.json`);
fs.writeFileSync(combinedPath, JSON.stringify(combinedTokens, null, 2));
console.log(`- ${combinedPath} created`);

65
styles/src/common.ts Normal file
View file

@ -0,0 +1,65 @@
export const fontFamilies = {
sans: "Zed Sans",
mono: "Zed Mono",
}
export const fontSizes = {
"3xs": 8,
"2xs": 10,
xs: 12,
sm: 14,
md: 16,
lg: 18,
xl: 20,
};
export type FontWeight = "thin"
| "extra_light"
| "light"
| "normal"
| "medium"
| "semibold"
| "bold"
| "extra_bold"
| "black";
export const fontWeights: { [key: string]: FontWeight } = {
thin: "thin",
extra_light: "extra_light",
light: "light",
normal: "normal",
medium: "medium",
semibold: "semibold",
bold: "bold",
extra_bold: "extra_bold",
black: "black"
};
export const sizes = {
px: 1,
xs: 2,
sm: 4,
md: 6,
lg: 8,
xl: 12,
};
// export const colors = {
// neutral: colorRamp(["white", "black"], { steps: 37, increment: 25 }), // (900/25) + 1
// rose: colorRamp("#F43F5EFF"),
// red: colorRamp("#EF4444FF"),
// orange: colorRamp("#F97316FF"),
// amber: colorRamp("#F59E0BFF"),
// yellow: colorRamp("#EAB308FF"),
// lime: colorRamp("#84CC16FF"),
// green: colorRamp("#22C55EFF"),
// emerald: colorRamp("#10B981FF"),
// teal: colorRamp("#14B8A6FF"),
// cyan: colorRamp("#06BBD4FF"),
// sky: colorRamp("#0EA5E9FF"),
// blue: colorRamp("#3B82F6FF"),
// indigo: colorRamp("#6366F1FF"),
// violet: colorRamp("#8B5CF6FF"),
// purple: colorRamp("#A855F7FF"),
// fuschia: colorRamp("#D946E4FF"),
// pink: colorRamp("#EC4899FF"),
// }

View file

@ -4,7 +4,6 @@ import {
backgroundColor, backgroundColor,
border, border,
player, player,
modalShadow,
text, text,
TextColor, TextColor,
popoverShadow popoverShadow
@ -80,15 +79,15 @@ export default function chatPanel(theme: Theme) {
...message, ...message,
body: { body: {
...message.body, ...message.body,
color: theme.textColor.muted.value, color: theme.textColor.muted,
}, },
sender: { sender: {
...message.sender, ...message.sender,
color: theme.textColor.muted.value, color: theme.textColor.muted,
}, },
timestamp: { timestamp: {
...message.timestamp, ...message.timestamp,
color: theme.textColor.muted.value, color: theme.textColor.muted,
}, },
}, },
inputEditor: { inputEditor: {

View file

@ -1,8 +1,5 @@
import chroma from "chroma-js";
import { isIPv4 } from "net";
import Theme, { BackgroundColorSet } from "../themes/common/theme"; import Theme, { BackgroundColorSet } from "../themes/common/theme";
import { fontFamilies, fontSizes, FontWeight } from "../tokens"; import { fontFamilies, fontSizes, FontWeight } from "../common";
import { Color } from "../utils/color";
export type TextColor = keyof Theme["textColor"]; export type TextColor = keyof Theme["textColor"];
export function text( export function text(
@ -15,16 +12,16 @@ export function text(
underline?: boolean; underline?: boolean;
} }
) { ) {
let size = fontSizes[properties?.size || "sm"].value; let size = fontSizes[properties?.size || "sm"];
return { return {
family: fontFamilies[fontFamily].value, family: fontFamilies[fontFamily],
color: theme.textColor[color].value, color: theme.textColor[color],
...properties, ...properties,
size, size,
}; };
} }
export function textColor(theme: Theme, color: TextColor) { export function textColor(theme: Theme, color: TextColor) {
return theme.textColor[color].value; return theme.textColor[color];
} }
export type BorderColor = keyof Theme["borderColor"]; export type BorderColor = keyof Theme["borderColor"];
@ -48,19 +45,19 @@ export function border(
}; };
} }
export function borderColor(theme: Theme, color: BorderColor) { export function borderColor(theme: Theme, color: BorderColor) {
return theme.borderColor[color].value; return theme.borderColor[color];
} }
export type IconColor = keyof Theme["iconColor"]; export type IconColor = keyof Theme["iconColor"];
export function iconColor(theme: Theme, color: IconColor) { export function iconColor(theme: Theme, color: IconColor) {
return theme.iconColor[color].value; return theme.iconColor[color];
} }
export type PlayerIndex = 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8; export type PlayerIndex = 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8;
export interface Player { export interface Player {
selection: { selection: {
cursor: Color; cursor: string;
selection: Color; selection: string;
}; };
} }
export function player( export function player(
@ -69,8 +66,8 @@ export function player(
): Player { ): Player {
return { return {
selection: { selection: {
cursor: theme.player[playerNumber].cursorColor.value, cursor: theme.player[playerNumber].cursorColor,
selection: theme.player[playerNumber].selectionColor.value, selection: theme.player[playerNumber].selectionColor,
}, },
}; };
} }
@ -81,14 +78,14 @@ export function backgroundColor(
theme: Theme, theme: Theme,
name: BackgroundColor, name: BackgroundColor,
state?: BackgroundState, state?: BackgroundState,
): Color { ): string {
return theme.backgroundColor[name][state || "base"].value; return theme.backgroundColor[name][state || "base"];
} }
export function modalShadow(theme: Theme) { export function modalShadow(theme: Theme) {
return { return {
blur: 16, blur: 16,
color: theme.shadow.value, color: theme.shadow,
offset: [0, 2], offset: [0, 2],
}; };
} }
@ -96,7 +93,7 @@ export function modalShadow(theme: Theme) {
export function popoverShadow(theme: Theme) { export function popoverShadow(theme: Theme) {
return { return {
blur: 4, blur: 4,
color: theme.shadow.value, color: theme.shadow,
offset: [1, 2], offset: [1, 2],
}; };
} }

View file

@ -43,28 +43,28 @@ export default function editor(theme: Theme) {
for (const syntaxKey in theme.syntax) { for (const syntaxKey in theme.syntax) {
const style = theme.syntax[syntaxKey]; const style = theme.syntax[syntaxKey];
syntax[syntaxKey] = { syntax[syntaxKey] = {
color: style.color.value, color: style.color,
weight: style.weight.value, weight: style.weight,
underline: style.underline, underline: style.underline,
italic: style.italic, italic: style.italic,
}; };
} }
return { return {
textColor: theme.syntax.primary.color.value, textColor: theme.syntax.primary.color,
background: backgroundColor(theme, 500), background: backgroundColor(theme, 500),
activeLineBackground: theme.editor.line.active.value, activeLineBackground: theme.editor.line.active,
codeActionsIndicator: iconColor(theme, "muted"), codeActionsIndicator: iconColor(theme, "muted"),
diffBackgroundDeleted: backgroundColor(theme, "error"), diffBackgroundDeleted: backgroundColor(theme, "error"),
diffBackgroundInserted: backgroundColor(theme, "ok"), diffBackgroundInserted: backgroundColor(theme, "ok"),
documentHighlightReadBackground: theme.editor.highlight.occurrence.value, documentHighlightReadBackground: theme.editor.highlight.occurrence,
documentHighlightWriteBackground: theme.editor.highlight.activeOccurrence.value, documentHighlightWriteBackground: theme.editor.highlight.activeOccurrence,
errorColor: theme.textColor.error.value, errorColor: theme.textColor.error,
gutterBackground: backgroundColor(theme, 500), gutterBackground: backgroundColor(theme, 500),
gutterPaddingFactor: 3.5, gutterPaddingFactor: 3.5,
highlightedLineBackground: theme.editor.line.highlighted.value, highlightedLineBackground: theme.editor.line.highlighted,
lineNumber: theme.editor.gutter.primary.value, lineNumber: theme.editor.gutter.primary,
lineNumberActive: theme.editor.gutter.active.value, lineNumberActive: theme.editor.gutter.active,
renameFade: 0.6, renameFade: 0.6,
unnecessaryCodeFade: 0.5, unnecessaryCodeFade: 0.5,
selection: player(theme, 1).selection, selection: player(theme, 1).selection,
@ -120,7 +120,7 @@ export default function editor(theme: Theme) {
}, },
}, },
diagnosticPathHeader: { diagnosticPathHeader: {
background: theme.editor.line.active.value, background: theme.editor.line.active,
textScaleFactor: 0.857, textScaleFactor: 0.857,
filename: text(theme, "mono", "primary", { size: "sm" }), filename: text(theme, "mono", "primary", { size: "sm" }),
path: { path: {
@ -139,6 +139,10 @@ export default function editor(theme: Theme) {
invalidInformationDiagnostic: diagnostic(theme, "muted"), invalidInformationDiagnostic: diagnostic(theme, "muted"),
invalidWarningDiagnostic: diagnostic(theme, "muted"), invalidWarningDiagnostic: diagnostic(theme, "muted"),
hover_popover: hoverPopover(theme), hover_popover: hoverPopover(theme),
link_definition: {
color: theme.syntax.linkUri.color,
underline: theme.syntax.linkUri.underline,
},
jumpIcon: { jumpIcon: {
color: iconColor(theme, "muted"), color: iconColor(theme, "muted"),
iconWidth: 20, iconWidth: 20,

View file

@ -22,6 +22,6 @@ export default function HoverPopover(theme: Theme) {
padding: { top: 4 }, padding: { top: 4 },
}, },
prose: text(theme, "sans", "primary", { "size": "sm" }), prose: text(theme, "sans", "primary", { "size": "sm" }),
highlight: theme.editor.highlight.occurrence.value, highlight: theme.editor.highlight.occurrence,
} }
} }

View file

@ -25,7 +25,7 @@ export default function search(theme: Theme) {
}; };
return { return {
matchBackground: theme.editor.highlight.match.value, matchBackground: theme.editor.highlight.match,
tabIconSpacing: 8, tabIconSpacing: 8,
tabIconWidth: 14, tabIconWidth: 14,
optionButton: { optionButton: {

View file

@ -13,7 +13,7 @@ export default function updateNotification(theme: Theme): Object {
...text(theme, "sans", "secondary", { size: "xs" }), ...text(theme, "sans", "secondary", { size: "xs" }),
margin: { left: headerPadding, top: 6, bottom: 6 }, margin: { left: headerPadding, top: 6, bottom: 6 },
hover: { hover: {
color: theme.textColor["active"].value color: theme.textColor["active"]
} }
}, },
dismissButton: { dismissButton: {

View file

@ -147,7 +147,7 @@ export default function workspace(theme: Theme) {
}, },
disconnectedOverlay: { disconnectedOverlay: {
...text(theme, "sans", "active"), ...text(theme, "sans", "active"),
background: withOpacity(theme.backgroundColor[500].base, 0.8).value, background: withOpacity(theme.backgroundColor[500].base, 0.8),
}, },
notification: { notification: {
margin: { top: 10 }, margin: { top: 10 },

View file

@ -1,5 +1,5 @@
import chroma, { Color, Scale } from "chroma-js"; import chroma, { Color, Scale } from "chroma-js";
import { color, ColorToken, fontWeights, NumberToken } from "../../tokens"; import { fontWeights, } from "../../common";
import { withOpacity } from "../../utils/color"; import { withOpacity } from "../../utils/color";
import Theme, { buildPlayer, Syntax } from "./theme"; import Theme, { buildPlayer, Syntax } from "./theme";
@ -26,10 +26,10 @@ export function createTheme(
let blend = isLight ? 0.12 : 0.24; let blend = isLight ? 0.12 : 0.24;
function sample(ramp: Scale, index: number): ColorToken { function sample(ramp: Scale, index: number): string {
return color(ramp(index).hex()); return ramp(index).hex();
} }
const darkest = color(ramps.neutral(isLight ? 7 : 0).hex()); const darkest = ramps.neutral(isLight ? 7 : 0).hex();
const backgroundColor = { const backgroundColor = {
// Title bar // Title bar
@ -232,7 +232,7 @@ export function createTheme(
}; };
const shadow = withOpacity( const shadow = withOpacity(
color(ramps.neutral(isLight ? 7 : 0).darken().hex()), ramps.neutral(isLight ? 7 : 0).darken().hex(),
blend); blend);
return { return {

View file

@ -1,21 +1,21 @@
import { ColorToken, FontWeightToken, NumberToken } from "../../tokens"; import { FontWeight } from "../../common";
import { withOpacity } from "../../utils/color"; import { withOpacity } from "../../utils/color";
export interface SyntaxHighlightStyle { export interface SyntaxHighlightStyle {
color: ColorToken; color: string;
weight?: FontWeightToken; weight?: FontWeight;
underline?: boolean; underline?: boolean;
italic?: boolean; italic?: boolean;
} }
export interface Player { export interface Player {
baseColor: ColorToken; baseColor: string;
cursorColor: ColorToken; cursorColor: string;
selectionColor: ColorToken; selectionColor: string;
borderColor: ColorToken; borderColor: string;
} }
export function buildPlayer( export function buildPlayer(
color: ColorToken, color: string,
cursorOpacity?: number, cursorOpacity?: number,
selectionOpacity?: number, selectionOpacity?: number,
borderOpacity?: number borderOpacity?: number
@ -29,9 +29,9 @@ export function buildPlayer(
} }
export interface BackgroundColorSet { export interface BackgroundColorSet {
base: ColorToken; base: string;
hovered: ColorToken; hovered: string;
active: ColorToken; active: string;
} }
export interface Syntax { export interface Syntax {
@ -81,64 +81,64 @@ export default interface Theme {
info: BackgroundColorSet; info: BackgroundColorSet;
}; };
borderColor: { borderColor: {
primary: ColorToken; primary: string;
secondary: ColorToken; secondary: string;
muted: ColorToken; muted: string;
active: ColorToken; active: string;
/** /**
* Used for rendering borders on top of media like avatars, images, video, etc. * Used for rendering borders on top of media like avatars, images, video, etc.
*/ */
onMedia: ColorToken; onMedia: string;
ok: ColorToken; ok: string;
error: ColorToken; error: string;
warning: ColorToken; warning: string;
info: ColorToken; info: string;
}; };
textColor: { textColor: {
primary: ColorToken; primary: string;
secondary: ColorToken; secondary: string;
muted: ColorToken; muted: string;
placeholder: ColorToken; placeholder: string;
active: ColorToken; active: string;
feature: ColorToken; feature: string;
ok: ColorToken; ok: string;
error: ColorToken; error: string;
warning: ColorToken; warning: string;
info: ColorToken; info: string;
onMedia: ColorToken; onMedia: string;
}; };
iconColor: { iconColor: {
primary: ColorToken; primary: string;
secondary: ColorToken; secondary: string;
muted: ColorToken; muted: string;
placeholder: ColorToken; placeholder: string;
active: ColorToken; active: string;
feature: ColorToken; feature: string;
ok: ColorToken; ok: string;
error: ColorToken; error: string;
warning: ColorToken; warning: string;
info: ColorToken; info: string;
}; };
editor: { editor: {
background: ColorToken; background: string;
indent_guide: ColorToken; indent_guide: string;
indent_guide_active: ColorToken; indent_guide_active: string;
line: { line: {
active: ColorToken; active: string;
highlighted: ColorToken; highlighted: string;
}; };
highlight: { highlight: {
selection: ColorToken; selection: string;
occurrence: ColorToken; occurrence: string;
activeOccurrence: ColorToken; activeOccurrence: string;
matchingBracket: ColorToken; matchingBracket: string;
match: ColorToken; match: string;
activeMatch: ColorToken; activeMatch: string;
related: ColorToken; related: string;
}; };
gutter: { gutter: {
primary: ColorToken; primary: string;
active: ColorToken; active: string;
}; };
}; };
@ -154,5 +154,5 @@ export default interface Theme {
7: Player; 7: Player;
8: Player; 8: Player;
}, },
shadow: ColorToken; shadow: string;
} }

View file

@ -1,130 +0,0 @@
import { colorRamp } from "./utils/color";
interface Token<V, T> {
value: V,
type: T
}
export type FontFamily = string;
export type FontFamilyToken = Token<FontFamily, "fontFamily">;
function fontFamily(value: FontFamily): FontFamilyToken {
return {
value,
type: "fontFamily"
}
}
export const fontFamilies = {
sans: fontFamily("Zed Sans"),
mono: fontFamily("Zed Mono"),
}
export type FontSize = number;
export type FontSizeToken = Token<FontSize, "fontSize">;
function fontSize(value: FontSize) {
return {
value,
type: "fontSize"
};
}
export const fontSizes = {
"3xs": fontSize(8),
"2xs": fontSize(10),
xs: fontSize(12),
sm: fontSize(14),
md: fontSize(16),
lg: fontSize(18),
xl: fontSize(20),
};
export type FontWeight =
| "thin"
| "extra_light"
| "light"
| "normal"
| "medium"
| "semibold"
| "bold"
| "extra_bold"
| "black";
export type FontWeightToken = Token<FontWeight, "fontWeight">;
function fontWeight(value: FontWeight): FontWeightToken {
return {
value,
type: "fontWeight"
};
}
export const fontWeights = {
"thin": fontWeight("thin"),
"extra_light": fontWeight("extra_light"),
"light": fontWeight("light"),
"normal": fontWeight("normal"),
"medium": fontWeight("medium"),
"semibold": fontWeight("semibold"),
"bold": fontWeight("bold"),
"extra_bold": fontWeight("extra_bold"),
"black": fontWeight("black"),
}
// Standard size unit used for paddings, margins, borders, etc.
export type Size = number
export type SizeToken = Token<Size, "size">;
function size(value: Size): SizeToken {
return {
value,
type: "size"
};
}
export const sizes = {
px: size(1),
xs: size(2),
sm: size(4),
md: size(6),
lg: size(8),
xl: size(12),
};
export type Color = string;
export interface ColorToken {
value: Color,
type: "color",
step?: number,
}
export function color(value: string): ColorToken {
return {
value,
type: "color",
};
}
export const colors = {
neutral: colorRamp(["white", "black"], { steps: 37, increment: 25 }), // (900/25) + 1
rose: colorRamp("#F43F5EFF"),
red: colorRamp("#EF4444FF"),
orange: colorRamp("#F97316FF"),
amber: colorRamp("#F59E0BFF"),
yellow: colorRamp("#EAB308FF"),
lime: colorRamp("#84CC16FF"),
green: colorRamp("#22C55EFF"),
emerald: colorRamp("#10B981FF"),
teal: colorRamp("#14B8A6FF"),
cyan: colorRamp("#06BBD4FF"),
sky: colorRamp("#0EA5E9FF"),
blue: colorRamp("#3B82F6FF"),
indigo: colorRamp("#6366F1FF"),
violet: colorRamp("#8B5CF6FF"),
purple: colorRamp("#A855F7FF"),
fuschia: colorRamp("#D946E4FF"),
pink: colorRamp("#EC4899FF"),
}
export type NumberToken = Token<number, "number">;
export default {
fontFamilies,
fontSizes,
fontWeights,
size,
colors,
};

View file

@ -1,52 +1,5 @@
import chroma, { Scale } from "chroma-js"; import chroma from "chroma-js";
import { ColorToken } from "../tokens";
export type Color = string; export function withOpacity(color: string, opacity: number): string {
export type ColorRampStep = { value: Color; type: "color"; description: string }; return chroma(color).alpha(opacity).hex();
export type ColorRamp = {
[index: number]: ColorRampStep;
};
export function colorRamp(
color: Color | [Color, Color],
options?: { steps?: number; increment?: number; }
): ColorRamp {
let scale: Scale;
if (Array.isArray(color)) {
const [startColor, endColor] = color;
scale = chroma.scale([startColor, endColor]);
} else {
let hue = Math.round(chroma(color).hsl()[0]);
let startColor = chroma.hsl(hue, 0.88, 0.96);
let endColor = chroma.hsl(hue, 0.68, 0.12);
scale = chroma
.scale([startColor, color, endColor])
.domain([0, 0.5, 1])
.mode("hsl")
.gamma(1)
// .correctLightness(true)
.padding([0, 0]);
}
const ramp: ColorRamp = {};
const steps = options?.steps || 10;
const increment = options?.increment || 100;
scale.colors(steps, "hex").forEach((color, ix) => {
const step = ix * increment;
ramp[step] = {
value: color,
description: `Step: ${step}`,
type: "color",
};
});
return ramp;
}
export function withOpacity(color: ColorToken, opacity: number): ColorToken {
return {
...color,
value: chroma(color.value).alpha(opacity).hex()
};
} }