working jump to definition with tests

Keith Simmons created

Change summary

crates/editor/src/display_map.rs           |   8 
crates/editor/src/editor.rs                |  54 +-
crates/editor/src/element.rs               | 103 ++-
crates/editor/src/hover_popover.rs         |   3 
crates/editor/src/link_go_to_definition.rs | 525 +++++++++++++++++++++--
crates/editor/src/selections_collection.rs |   1 
crates/editor/src/test.rs                  |  89 +++-
crates/gpui/src/platform/event.rs          |  12 
crates/gpui/src/platform/mac/event.rs      | 170 ++++--
crates/gpui/src/platform/mac/window.rs     |  31 +
crates/project/src/project.rs              |   4 
crates/theme/src/theme.rs                  |   1 
styles/package.json                        |   3 
styles/src/buildTokens.ts                  |  74 ---
styles/src/common.ts                       |  65 ++
styles/src/styleTree/chatPanel.ts          |   7 
styles/src/styleTree/components.ts         |  33 
styles/src/styleTree/editor.ts             |  26 
styles/src/styleTree/hoverPopover.ts       |   2 
styles/src/styleTree/search.ts             |   2 
styles/src/styleTree/updateNotification.ts |   2 
styles/src/styleTree/workspace.ts          |   2 
styles/src/themes/common/base16.ts         |  10 
styles/src/themes/common/theme.ts          | 112 ++--
styles/src/tokens.ts                       | 130 -----
styles/src/utils/color.ts                  |  53 --
26 files changed, 957 insertions(+), 565 deletions(-)

Detailed changes

crates/editor/src/display_map.rs 🔗

@@ -474,6 +474,14 @@ impl DisplaySnapshot {
     pub fn longest_row(&self) -> u32 {
         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)]

crates/editor/src/editor.rs 🔗

@@ -45,7 +45,7 @@ pub use multi_buffer::{
     ToPoint,
 };
 use ordered_float::OrderedFloat;
-use project::{Project, ProjectPath, ProjectTransaction};
+use project::{LocationLink, Project, ProjectPath, ProjectTransaction};
 use selections_collection::{resolve_multiple, MutableSelectionsCollection, SelectionsCollection};
 use serde::{Deserialize, Serialize};
 use settings::Settings;
@@ -4602,28 +4602,7 @@ impl Editor {
         cx.spawn(|workspace, mut cx| async move {
             let definitions = definitions.await?;
             workspace.update(&mut cx, |workspace, cx| {
-                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();
-                    });
-                }
+                Editor::navigate_to_definitions(workspace, editor_handle, definitions, cx);
             });
 
             Ok::<(), anyhow::Error>(())
@@ -4631,6 +4610,35 @@ impl Editor {
         .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(
         workspace: &mut Workspace,
         _: &FindAllReferences,

crates/editor/src/element.rs 🔗

@@ -6,9 +6,9 @@ use super::{
 use crate::{
     display_map::{BlockStyle, DisplaySnapshot, TransformBlock},
     hover_popover::HoverAt,
+    link_go_to_definition::{CmdChanged, GoToFetchedDefinition, UpdateGoToDefinitionLink},
     EditorStyle,
 };
-use crate::{hover_popover::HoverAt, link_go_to_definition::FetchDefinition};
 use clock::ReplicaId;
 use collections::{BTreeMap, HashMap};
 use gpui::{
@@ -105,7 +105,7 @@ impl EditorElement {
     fn mouse_down(
         &self,
         position: Vector2F,
-        _: bool,
+        cmd: bool,
         alt: bool,
         shift: bool,
         mut click_count: usize,
@@ -113,6 +113,14 @@ impl EditorElement {
         paint: &mut PaintState,
         cx: &mut EventContext,
     ) -> 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) {
             click_count = 3; // Simulate triple-click when clicking the gutter to select lines
         } else if !paint.text_bounds.contains_point(position) {
@@ -204,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 {
         let view = self.view.upgrade(cx.app).unwrap();
 
@@ -219,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(
         &self,
         position: Vector2F,
@@ -1427,45 +1486,11 @@ impl Element for EditorElement {
                 precise,
             } => self.scroll(*position, *delta, *precise, layout, paint, cx),
             Event::KeyDown { input, .. } => self.key_down(input.as_deref(), cx),
+            Event::ModifiersChanged { cmd, .. } => self.modifiers_changed(*cmd, cx),
             Event::MouseMoved { position, cmd, .. } => {
-                // 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
-                };
-
-                if *cmd {
-                    cx.dispatch_action(FetchDefinition { point });
-                }
-
-                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
+                self.mouse_moved(*position, *cmd, layout, paint, cx)
             }
+
             _ => false,
         }
     }

crates/editor/src/hover_popover.rs 🔗

@@ -1,5 +1,3 @@
-use std::{ops::Range, time::Duration};
-
 use gpui::{
     actions,
     elements::{Flex, MouseEventHandler, Padding, Text},
@@ -9,6 +7,7 @@ use gpui::{
 };
 use language::Bias;
 use project::{HoverBlock, Project};
+use std::{ops::Range, time::Duration};
 use util::TryFutureExt;
 
 use crate::{
@@ -1,60 +1,77 @@
-use std::{
-    ops::Range,
-    time::{Duration, Instant},
-};
-
-use gpui::{
-    actions,
-    color::Color,
-    elements::{Flex, MouseEventHandler, Padding, Text},
-    fonts::{HighlightStyle, Underline},
-    impl_internal_actions,
-    platform::CursorStyle,
-    Axis, Element, ElementBox, ModelHandle, MutableAppContext, RenderContext, Task, ViewContext,
-};
-use language::Bias;
-use project::{HoverBlock, Project};
+use std::ops::Range;
+
+use gpui::{impl_internal_actions, MutableAppContext, Task, ViewContext};
+use language::{Bias, ToOffset};
+use project::LocationLink;
+use settings::Settings;
 use util::TryFutureExt;
+use workspace::Workspace;
 
-use crate::{
-    display_map::ToDisplayPoint, Anchor, AnchorRangeExt, DisplayPoint, Editor, EditorSnapshot,
-    EditorStyle,
-};
+use crate::{Anchor, DisplayPoint, Editor, GoToDefinition};
 
 #[derive(Clone, PartialEq)]
-pub struct FetchDefinition {
+pub struct UpdateGoToDefinitionLink {
     pub point: Option<DisplayPoint>,
+    pub cmd_held: bool,
+}
+
+#[derive(Clone, PartialEq)]
+pub struct CmdChanged {
+    pub cmd_down: bool,
 }
 
 #[derive(Clone, PartialEq)]
 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) {
-    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);
 }
 
 #[derive(Default)]
 pub struct LinkGoToDefinitionState {
+    pub last_mouse_point: Option<DisplayPoint>,
     pub triggered_from: Option<Anchor>,
     pub symbol_range: Option<Range<Anchor>>,
+    pub definitions: Vec<LocationLink>,
     pub task: Option<Task<Option<()>>>,
 }
 
-pub fn fetch_definition(
+pub fn update_go_to_definition_link(
     editor: &mut Editor,
-    &FetchDefinition { point }: &FetchDefinition,
+    &UpdateGoToDefinitionLink { point, cmd_held }: &UpdateGoToDefinitionLink,
     cx: &mut ViewContext<Editor>,
 ) {
-    if let Some(point) = point {
-        show_link_definition(editor, point, cx);
-    } else {
-        //TODO: Also needs to be dispatched when cmd modifier is released
-        hide_link_definition(editor, cx);
+    editor.link_go_to_definition_state.last_mouse_point = point;
+    if cmd_held {
+        if let Some(point) = point {
+            show_link_definition(editor, point, 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_point {
+        if cmd_down {
+            show_link_definition(editor, point, cx);
+        } else {
+            hide_link_definition(editor, cx)
+        }
     }
 }
 
@@ -101,7 +118,22 @@ pub fn show_link_definition(
         .buffer_snapshot
         .anchor_at(multibuffer_offset, Bias::Left);
 
-    // Don't request again if the location is the same as the previous request
+    // 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(&anchor, &snapshot.buffer_snapshot)
+            .is_le()
+            && symbol_range
+                .end
+                .cmp(&anchor, &snapshot.buffer_snapshot)
+                .is_ge()
+        {
+            return;
+        }
+    }
+
+    // Don't request from the exact same location again
     if let Some(triggered_from) = &editor.link_go_to_definition_state.triggered_from {
         if triggered_from
             .cmp(&anchor, &snapshot.buffer_snapshot)
@@ -120,11 +152,10 @@ pub fn show_link_definition(
                 })
             });
 
-            let origin_range = definition_request.await.ok().and_then(|definition_result| {
-                definition_result
-                    .into_iter()
-                    .filter_map(|link| {
-                        link.origin.map(|origin| {
+            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);
@@ -134,25 +165,65 @@ pub fn show_link_definition(
 
                             start..end
                         })
-                    })
-                    .next()
+                    }),
+                    definition_result,
+                )
             });
 
             if let Some(this) = this.upgrade(&cx) {
                 this.update(&mut cx, |this, cx| {
-                    if let Some(origin_range) = origin_range {
-                        this.highlight_text::<LinkGoToDefinitionState>(
-                            vec![origin_range],
-                            HighlightStyle {
-                                underline: Some(Underline {
-                                    color: Some(Color::red()),
-                                    thickness: 1.0.into(),
-                                    squiggly: false,
-                                }),
-                                ..Default::default()
-                            },
-                            cx,
-                        )
+                    // Clear any existing highlights
+                    this.clear_text_highlights::<LinkGoToDefinitionState>(cx);
+                    this.link_go_to_definition_state.triggered_from = Some(anchor.clone());
+                    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(anchor);
+
+                                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);
+                        }
                     }
                 })
             }
@@ -166,21 +237,363 @@ pub fn show_link_definition(
 }
 
 pub fn hide_link_definition(editor: &mut Editor, cx: &mut ViewContext<Editor>) {
-    // only notify the context once
     if editor.link_go_to_definition_state.symbol_range.is_some() {
         editor.link_go_to_definition_state.symbol_range.take();
         cx.notify();
     }
 
-    editor.link_go_to_definition_state.task = None;
     editor.link_go_to_definition_state.triggered_from = None;
+    editor.link_go_to_definition_state.task = None;
 
     editor.clear_text_highlights::<LinkGoToDefinitionState>(cx);
 }
 
 pub fn go_to_fetched_definition(
-    editor: &mut Editor,
+    workspace: &mut Workspace,
     GoToFetchedDefinition { point }: &GoToFetchedDefinition,
-    cx: &mut ViewContext<Editor>,
+    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 mut definitions = Vec::new();
+
+    editor_handle.update(cx, |editor, cx| {
+        hide_link_definition(editor, cx);
+        std::mem::swap(
+            &mut editor.link_go_to_definition_state.definitions,
+            &mut definitions,
+        );
+    });
+
+    if !definitions.is_empty() {
+        Editor::navigate_to_definitions(workspace, editor_handle, definitions, cx);
+    } else {
+        editor_handle.update(cx, |editor, cx| {
+            editor.change_selections(None, cx, |s| s.select_display_ranges(vec![*point..*point]));
+        });
+
+        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
+        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();
+        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();"});
+    }
 }

crates/editor/src/selections_collection.rs 🔗

@@ -536,7 +536,6 @@ impl<'a> MutableSelectionsCollection<'a> {
         self.select_anchors(selections)
     }
 
-    #[cfg(any(test, feature = "test-support"))]
     pub fn select_display_ranges<T>(&mut self, ranges: T)
     where
         T: IntoIterator<Item = Range<DisplayPoint>>,

crates/editor/src/test.rs 🔗

@@ -7,19 +7,20 @@ use futures::StreamExt;
 use indoc::indoc;
 
 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 project::{FakeFs, Project};
+use project::Project;
 use settings::Settings;
 use util::{
-    set_eq,
+    assert_set_eq, set_eq,
     test::{marked_text, marked_text_ranges, marked_text_ranges_by, SetEqError},
 };
+use workspace::{pane, AppState, Workspace, WorkspaceHandle};
 
 use crate::{
     display_map::{DisplayMap, DisplaySnapshot, ToDisplayPoint},
     multi_buffer::ToPointUtf16,
-    Autoscroll, DisplayPoint, Editor, EditorMode, MultiBuffer, ToPoint,
+    AnchorRangeExt, Autoscroll, DisplayPoint, Editor, EditorMode, MultiBuffer, ToPoint,
 };
 
 #[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>>) {
         let mut empty_selections = Vec::new();
         let mut reverse_selections = Vec::new();
@@ -390,6 +409,7 @@ impl<'a> DerefMut for EditorTestContext<'a> {
 pub struct EditorLspTestContext<'a> {
     pub cx: EditorTestContext<'a>,
     pub lsp: lsp::FakeLanguageServer,
+    pub workspace: ViewHandle<Workspace>,
 }
 
 impl<'a> EditorLspTestContext<'a> {
@@ -398,8 +418,17 @@ impl<'a> EditorLspTestContext<'a> {
         capabilities: lsp::ServerCapabilities,
         cx: &'a mut gpui::TestAppContext,
     ) -> EditorLspTestContext<'a> {
+        use json::json;
+
+        cx.update(|cx| {
+            crate::init(cx);
+            pane::init(cx);
+        });
+
+        let params = cx.update(AppState::test);
+
         let file_name = format!(
-            "/file.{}",
+            "file.{}",
             language
                 .path_suffixes()
                 .first()
@@ -411,30 +440,36 @@ impl<'a> EditorLspTestContext<'a> {
             ..Default::default()
         });
 
-        let fs = FakeFs::new(cx.background().clone());
-        fs.insert_file(file_name.clone(), "".to_string()).await;
-
-        let project = Project::test(fs, [file_name.as_ref()], cx).await;
+        let project = Project::test(params.fs.clone(), [], cx).await;
         project.update(cx, |project, _| project.languages().add(Arc::new(language)));
-        let buffer = project
-            .update(cx, |project, cx| project.open_local_buffer(file_name, cx))
+
+        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
             .unwrap();
+        cx.read(|cx| workspace.read(cx).worktree_scans_complete(cx))
+            .await;
 
-        let (window_id, editor) = cx.update(|cx| {
-            cx.set_global(Settings::test(cx));
-            crate::init(cx);
-
-            let (window_id, editor) = cx.add_window(Default::default(), |cx| {
-                let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx));
-
-                Editor::new(EditorMode::Full, buffer, Some(project), None, cx)
-            });
-
-            editor.update(cx, |_, cx| cx.focus_self());
+        let file = cx.read(|cx| workspace.file_project_paths(cx)[0].clone());
+        let item = workspace
+            .update(cx, |workspace, cx| workspace.open_path(file, true, cx))
+            .await
+            .expect("Could not open test file");
 
-            (window_id, editor)
+        let editor = cx.update(|cx| {
+            item.act_as::<Editor>(cx)
+                .expect("Opened test file wasn't an editor")
         });
+        editor.update(cx, |_, cx| cx.focus_self());
 
         let lsp = fake_servers.next().await.unwrap();
 
@@ -445,6 +480,7 @@ impl<'a> EditorLspTestContext<'a> {
                 editor,
             },
             lsp,
+            workspace,
         }
     }
 
@@ -493,6 +529,13 @@ impl<'a> EditorLspTestContext<'a> {
             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> {

crates/gpui/src/platform/event.rs 🔗

@@ -13,6 +13,16 @@ pub enum Event {
         input: Option<String>,
         is_held: bool,
     },
+    KeyUp {
+        keystroke: Keystroke,
+        input: Option<String>,
+    },
+    ModifiersChanged {
+        ctrl: bool,
+        alt: bool,
+        shift: bool,
+        cmd: bool,
+    },
     ScrollWheel {
         position: Vector2F,
         delta: Vector2F,
@@ -76,6 +86,8 @@ impl Event {
     pub fn position(&self) -> Option<Vector2F> {
         match self {
             Event::KeyDown { .. } => None,
+            Event::KeyUp { .. } => None,
+            Event::ModifiersChanged { .. } => None,
             Event::ScrollWheel { position, .. }
             | Event::LeftMouseDown { position, .. }
             | Event::LeftMouseUp { position, .. }

crates/gpui/src/platform/mac/event.rs 🔗

@@ -52,6 +52,20 @@ impl Event {
         }
 
         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 => {
                 let modifiers = native_event.modifierFlags();
                 let ctrl = modifiers.contains(NSEventModifierFlags::NSControlKeyMask);
@@ -60,71 +74,7 @@ impl Event {
                 let cmd = modifiers.contains(NSEventModifierFlags::NSCommandKeyMask);
                 let function = modifiers.contains(NSEventModifierFlags::NSFunctionKeyMask);
 
-                let unmodified_chars = CStr::from_ptr(
-                    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;
-                };
+                let (unmodified_chars, input) = get_key_text(native_event, cmd, ctrl, function)?;
 
                 Some(Self::KeyDown {
                     keystroke: Keystroke {
@@ -138,6 +88,27 @@ impl Event {
                     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 => {
                 let modifiers = native_event.modifierFlags();
                 window_height.map(|window_height| Self::LeftMouseDown {
@@ -260,3 +231,72 @@ impl Event {
         }
     }
 }
+
+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))
+}

crates/gpui/src/platform/mac/window.rs 🔗

@@ -135,6 +135,10 @@ unsafe fn build_classes() {
             sel!(scrollWheel:),
             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(
             sel!(cancelOperation:),
             cancel_operation as extern "C" fn(&Object, Sel, id),
@@ -181,6 +185,7 @@ struct WindowState {
     last_fresh_keydown: Option<(Keystroke, Option<String>)>,
     layer: id,
     traffic_light_position: Option<Vector2F>,
+    previous_modifiers_changed_event: Option<Event>,
 }
 
 impl Window {
@@ -263,6 +268,7 @@ impl Window {
                 last_fresh_keydown: None,
                 layer,
                 traffic_light_position: options.traffic_light_position,
+                previous_modifiers_changed_event: None,
             })));
 
             (*native_window).set_ivar(
@@ -611,6 +617,31 @@ extern "C" fn handle_view_event(this: &Object, _: Sel, native_event: id) {
             Event::LeftMouseUp { .. } => {
                 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());
+            }
             _ => {}
         }
 

crates/project/src/project.rs 🔗

@@ -202,13 +202,13 @@ pub struct DiagnosticSummary {
     pub warning_count: usize,
 }
 
-#[derive(Debug)]
+#[derive(Debug, Clone)]
 pub struct Location {
     pub buffer: ModelHandle<Buffer>,
     pub range: Range<language::Anchor>,
 }
 
-#[derive(Debug)]
+#[derive(Debug, Clone)]
 pub struct LocationLink {
     pub origin: Option<Location>,
     pub target: Location,

crates/theme/src/theme.rs 🔗

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

styles/package.json 🔗

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

styles/src/buildTokens.ts 🔗

@@ -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`);

styles/src/common.ts 🔗

@@ -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"),
+// }

styles/src/styleTree/chatPanel.ts 🔗

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

styles/src/styleTree/components.ts 🔗

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

styles/src/styleTree/editor.ts 🔗

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

styles/src/styleTree/hoverPopover.ts 🔗

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

styles/src/styleTree/search.ts 🔗

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

styles/src/styleTree/updateNotification.ts 🔗

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

styles/src/styleTree/workspace.ts 🔗

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

styles/src/themes/common/base16.ts 🔗

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

styles/src/themes/common/theme.ts 🔗

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

styles/src/tokens.ts 🔗

@@ -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,
-};

styles/src/utils/color.ts 🔗

@@ -1,52 +1,5 @@
-import chroma, { Scale } from "chroma-js";
-import { ColorToken } from "../tokens";
+import chroma from "chroma-js";
 
-export type Color = string;
-export type ColorRampStep = { value: Color; type: "color"; description: string };
-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()
-  };
+export function withOpacity(color: string, opacity: number): string {
+  return chroma(color).alpha(opacity).hex();
 }