Detailed changes
@@ -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)]
@@ -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,
@@ -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,
}
}
@@ -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();"});
+ }
}
@@ -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>>,
@@ -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> {
@@ -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, .. }
@@ -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))
+}
@@ -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());
+ }
_ => {}
}
@@ -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,
@@ -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>,
}
@@ -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",
@@ -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`);
@@ -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"),
+// }
@@ -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: {
@@ -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],
};
}
@@ -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,
@@ -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,
}
}
@@ -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: {
@@ -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: {
@@ -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 },
@@ -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 {
@@ -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;
}
@@ -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,
-};
@@ -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();
}