diff --git a/crates/editor/src/display_map.rs b/crates/editor/src/display_map.rs index 2c7c252e3b..917553c791 100644 --- a/crates/editor/src/display_map.rs +++ b/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( + &self, + ) -> Option>)>> { + let type_id = TypeId::of::(); + self.text_highlights.get(&Some(type_id)).cloned() + } } #[derive(Copy, Clone, Default, Eq, Ord, PartialOrd, PartialEq)] diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 8a20f8780b..9243df0004 100644 --- a/crates/editor/src/editor.rs +++ b/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, + definitions: Vec, + cx: &mut ViewContext, + ) { + 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, diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index cffa3454a6..491ed3db43 100644 --- a/crates/editor/src/element.rs +++ b/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, } } diff --git a/crates/editor/src/hover_popover.rs b/crates/editor/src/hover_popover.rs index a0acc908df..31323d01d2 100644 --- a/crates/editor/src/hover_popover.rs +++ b/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::{ diff --git a/crates/editor/src/link_go_to_definition.rs b/crates/editor/src/link_go_to_definition.rs index e1129dc092..ab8e365571 100644 --- a/crates/editor/src/link_go_to_definition.rs +++ b/crates/editor/src/link_go_to_definition.rs @@ -1,60 +1,77 @@ -use std::{ - ops::Range, - time::{Duration, Instant}, -}; +use std::ops::Range; -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 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, + pub cmd_held: bool, +} + +#[derive(Clone, PartialEq)] +pub struct CmdChanged { + pub cmd_down: bool, } #[derive(Clone, PartialEq)] pub struct GoToFetchedDefinition { - pub point: Option, + 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, pub triggered_from: Option, pub symbol_range: Option>, + pub definitions: Vec, pub 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, ) { - 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, +) { + 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::( - 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::(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::().theme.editor.link_definition; + this.highlight_text::( + 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) { - // 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::(cx); } pub fn go_to_fetched_definition( - editor: &mut Editor, + workspace: &mut Workspace, GoToFetchedDefinition { point }: &GoToFetchedDefinition, - cx: &mut ViewContext, + cx: &mut ViewContext, ) { + let active_item = workspace.active_item(cx); + let editor_handle = if let Some(editor) = active_item + .as_ref() + .and_then(|item| item.act_as::(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::(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::(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::(indoc! {" + fn test() + do_work(); + + fn do_work() + test();"}); + + // Response without source range still highlights word + let mut requests = + cx.lsp + .handle_request::(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::(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::(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::(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::(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::(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::(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::(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::(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::(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::(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();"}); + } } diff --git a/crates/editor/src/selections_collection.rs b/crates/editor/src/selections_collection.rs index 1d02b26e4b..8237c4fafc 100644 --- a/crates/editor/src/selections_collection.rs +++ b/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(&mut self, ranges: T) where T: IntoIterator>, diff --git a/crates/editor/src/test.rs b/crates/editor/src/test.rs index 33bb07e94c..91480ca6a9 100644 --- a/crates/editor/src/test.rs +++ b/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(&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> = snapshot + .display_snapshot + .highlight_ranges::() + .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>) { 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, } 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 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"); - 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()); - - (window_id, editor) + let editor = cx.update(|cx| { + item.act_as::(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(&mut self, update: F) -> T + where + F: FnOnce(&mut Workspace, &mut ViewContext) -> T, + { + self.workspace.update(self.cx.cx, update) + } } impl<'a> Deref for EditorLspTestContext<'a> { diff --git a/crates/gpui/src/platform/event.rs b/crates/gpui/src/platform/event.rs index 4c4cc90b32..f43d5bea49 100644 --- a/crates/gpui/src/platform/event.rs +++ b/crates/gpui/src/platform/event.rs @@ -13,6 +13,16 @@ pub enum Event { input: Option, is_held: bool, }, + KeyUp { + keystroke: Keystroke, + input: Option, + }, + 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 { match self { Event::KeyDown { .. } => None, + Event::KeyUp { .. } => None, + Event::ModifiersChanged { .. } => None, Event::ScrollWheel { position, .. } | Event::LeftMouseDown { position, .. } | Event::LeftMouseUp { position, .. } diff --git a/crates/gpui/src/platform/mac/event.rs b/crates/gpui/src/platform/mac/event.rs index 027c0ed5c8..ed4303cc4d 100644 --- a/crates/gpui/src/platform/mac/event.rs +++ b/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)> { + 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)) +} diff --git a/crates/gpui/src/platform/mac/window.rs b/crates/gpui/src/platform/mac/window.rs index 0624789a4e..14b9cf7ea8 100644 --- a/crates/gpui/src/platform/mac/window.rs +++ b/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)>, layer: id, traffic_light_position: Option, + previous_modifiers_changed_event: Option, } 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()); + } _ => {} } diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 7482e59650..9fa0c3fd45 100644 --- a/crates/project/src/project.rs +++ b/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, pub range: Range, } -#[derive(Debug)] +#[derive(Debug, Clone)] pub struct LocationLink { pub origin: Option, pub target: Location, diff --git a/crates/theme/src/theme.rs b/crates/theme/src/theme.rs index 08ca7898f5..ae269c00cb 100644 --- a/crates/theme/src/theme.rs +++ b/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, } diff --git a/styles/package.json b/styles/package.json index eebc80d521..ccdaca0853 100644 --- a/styles/package.json +++ b/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", diff --git a/styles/src/buildTokens.ts b/styles/src/buildTokens.ts deleted file mode 100644 index a6ec840bbf..0000000000 --- a/styles/src/buildTokens.ts +++ /dev/null @@ -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`); diff --git a/styles/src/common.ts b/styles/src/common.ts new file mode 100644 index 0000000000..713401f348 --- /dev/null +++ b/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"), +// } \ No newline at end of file diff --git a/styles/src/styleTree/chatPanel.ts b/styles/src/styleTree/chatPanel.ts index 693d464b39..103b8d2a3f 100644 --- a/styles/src/styleTree/chatPanel.ts +++ b/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: { diff --git a/styles/src/styleTree/components.ts b/styles/src/styleTree/components.ts index 214c255e4b..020005b301 100644 --- a/styles/src/styleTree/components.ts +++ b/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], }; } diff --git a/styles/src/styleTree/editor.ts b/styles/src/styleTree/editor.ts index 0f0e8724e9..8031a229c4 100644 --- a/styles/src/styleTree/editor.ts +++ b/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, diff --git a/styles/src/styleTree/hoverPopover.ts b/styles/src/styleTree/hoverPopover.ts index 38b47630e2..353706d0a0 100644 --- a/styles/src/styleTree/hoverPopover.ts +++ b/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, } } \ No newline at end of file diff --git a/styles/src/styleTree/search.ts b/styles/src/styleTree/search.ts index 7febfb98b1..989b9f6b7a 100644 --- a/styles/src/styleTree/search.ts +++ b/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: { diff --git a/styles/src/styleTree/updateNotification.ts b/styles/src/styleTree/updateNotification.ts index 1c3b705582..7a9ba196c0 100644 --- a/styles/src/styleTree/updateNotification.ts +++ b/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: { diff --git a/styles/src/styleTree/workspace.ts b/styles/src/styleTree/workspace.ts index 8863a8eb79..2deadc02e7 100644 --- a/styles/src/styleTree/workspace.ts +++ b/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 }, diff --git a/styles/src/themes/common/base16.ts b/styles/src/themes/common/base16.ts index 52715bd544..21a02cde25 100644 --- a/styles/src/themes/common/base16.ts +++ b/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 { diff --git a/styles/src/themes/common/theme.ts b/styles/src/themes/common/theme.ts index fe47ce8153..92b1f8eff8 100644 --- a/styles/src/themes/common/theme.ts +++ b/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; } diff --git a/styles/src/tokens.ts b/styles/src/tokens.ts deleted file mode 100644 index 69fc05ff63..0000000000 --- a/styles/src/tokens.ts +++ /dev/null @@ -1,130 +0,0 @@ -import { colorRamp } from "./utils/color"; - -interface Token { - value: V, - type: T -} - -export type FontFamily = string; -export type FontFamilyToken = Token; -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; -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; -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; -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; - -export default { - fontFamilies, - fontSizes, - fontWeights, - size, - colors, -}; diff --git a/styles/src/utils/color.ts b/styles/src/utils/color.ts index f28de02fa1..7d2d1dd535 100644 --- a/styles/src/utils/color.ts +++ b/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(); }