Cargo.lock π
@@ -4277,6 +4277,7 @@ dependencies = [
"async-tar",
"futures 0.3.28",
"gpui",
+ "log",
"parking_lot 0.11.2",
"serde",
"serde_derive",
KCaverly created
Cargo.lock | 1
assets/settings/default.json | 10
crates/activity_indicator/src/activity_indicator.rs | 13
crates/collab/src/rpc.rs | 16
crates/collab/src/tests/integration_tests.rs | 584 +++
crates/collab_ui/src/collab_titlebar_item.rs | 5
crates/context_menu/src/context_menu.rs | 54
crates/copilot/src/copilot.rs | 13
crates/copilot_button/src/copilot_button.rs | 7
crates/editor/src/display_map.rs | 157
crates/editor/src/display_map/block_map.rs | 75
crates/editor/src/display_map/fold_map.rs | 606 +--
crates/editor/src/display_map/inlay_map.rs | 1697 ++++++++++
crates/editor/src/display_map/suggestion_map.rs | 871 -----
crates/editor/src/display_map/tab_map.rs | 224
crates/editor/src/display_map/wrap_map.rs | 78
crates/editor/src/editor.rs | 249 +
crates/editor/src/element.rs | 9
crates/editor/src/inlay_hint_cache.rs | 2021 ++++++++++++
crates/editor/src/multi_buffer/anchor.rs | 17
crates/editor/src/scroll.rs | 24
crates/fs/src/fs.rs | 11
crates/gpui/src/app/window.rs | 13
crates/gpui/src/elements/mouse_event_handler.rs | 13
crates/gpui/src/scene/mouse_event.rs | 22
crates/gpui/src/scene/mouse_region.rs | 37
crates/language/src/language.rs | 174
crates/language/src/language_settings.rs | 61
crates/lsp/src/lsp.rs | 33
crates/node_runtime/Cargo.toml | 1
crates/node_runtime/src/node_runtime.rs | 203
crates/project/src/lsp_command.rs | 337 +
crates/project/src/project.rs | 893 +++-
crates/project/src/project_tests.rs | 9
crates/project/src/worktree.rs | 189
crates/project/src/worktree_tests.rs | 17
crates/rope/src/rope.rs | 6
crates/rpc/proto/zed.proto | 66
crates/rpc/src/proto.rs | 7
crates/sum_tree/src/cursor.rs | 36
crates/sum_tree/src/sum_tree.rs | 58
crates/terminal_view/src/terminal_element.rs | 21
crates/terminal_view/src/terminal_panel.rs | 2
crates/theme/src/theme.rs | 1
crates/util/src/util.rs | 14
crates/workspace/src/pane.rs | 24
crates/zed/src/languages/c.rs | 67
crates/zed/src/languages/elixir.rs | 41
crates/zed/src/languages/go.rs | 67
crates/zed/src/languages/html.rs | 75
crates/zed/src/languages/json.rs | 69
crates/zed/src/languages/language_plugin.rs | 11
crates/zed/src/languages/lua.rs | 69
crates/zed/src/languages/python.rs | 73
crates/zed/src/languages/ruby.rs | 11
crates/zed/src/languages/rust.rs | 40
crates/zed/src/languages/typescript.rs | 109
crates/zed/src/languages/yaml.rs | 73
crates/zed/src/main.rs | 2
crates/zed/src/zed.rs | 2
styles/.eslintrc.js | 33
styles/.prettierrc | 6
styles/package-lock.json | 817 ++++
styles/package.json | 34
styles/src/buildLicenses.ts | 50
styles/src/buildThemes.ts | 43
styles/src/buildTokens.ts | 87
styles/src/buildTypes.ts | 64
styles/src/build_licenses.ts | 50
styles/src/build_themes.ts | 43
styles/src/build_tokens.ts | 87
styles/src/build_types.ts | 62
styles/src/common.ts | 26
styles/src/component/icon_button.ts | 4
styles/src/component/text_button.ts | 4
styles/src/element/interactive.test.ts | 18
styles/src/element/interactive.ts | 42
styles/src/element/toggle.ts | 8
styles/src/styleTree/app.ts | 75
styles/src/styleTree/contactFinder.ts | 70
styles/src/styleTree/contactNotification.ts | 53
styles/src/styleTree/contactsPopover.ts | 16
styles/src/styleTree/contextMenu.ts | 67
styles/src/styleTree/editor.ts | 314 -
styles/src/styleTree/feedback.ts | 49
styles/src/styleTree/hoverPopover.ts | 46
styles/src/styleTree/incomingCallNotification.ts | 53
styles/src/styleTree/projectDiagnostics.ts | 13
styles/src/styleTree/projectPanel.ts | 188 -
styles/src/styleTree/projectSharedNotification.ts | 54
styles/src/styleTree/search.ts | 135
styles/src/styleTree/sharedScreen.ts | 9
styles/src/styleTree/simpleMessageNotification.ts | 53
styles/src/styleTree/terminal.ts | 52
styles/src/styleTree/toggle.ts | 47
styles/src/styleTree/toolbarDropdownMenu.ts | 64
styles/src/styleTree/tooltip.ts | 23
styles/src/styleTree/updateNotification.ts | 40
styles/src/styleTree/welcome.ts | 134
styles/src/styleTree/workspace.ts | 200 -
styles/src/style_tree/app.ts | 75
styles/src/style_tree/assistant.ts | 132
styles/src/style_tree/command_palette.ts | 24
styles/src/style_tree/components.ts | 136
styles/src/style_tree/contact_finder.ts | 70
styles/src/style_tree/contact_list.ts | 129
styles/src/style_tree/contact_notification.ts | 53
styles/src/style_tree/contacts_popover.ts | 14
styles/src/style_tree/context_menu.ts | 68
styles/src/style_tree/copilot.ts | 105
styles/src/style_tree/editor.ts | 313 +
styles/src/style_tree/feedback.ts | 49
styles/src/style_tree/hover_popover.ts | 47
styles/src/style_tree/incoming_call_notification.ts | 55
styles/src/style_tree/picker.ts | 67
styles/src/style_tree/project_diagnostics.ts | 12
styles/src/style_tree/project_panel.ts | 202 +
styles/src/style_tree/project_shared_notification.ts | 55
styles/src/style_tree/search.ts | 136
styles/src/style_tree/shared_screen.ts | 8
styles/src/style_tree/simple_message_notification.ts | 50
styles/src/style_tree/status_bar.ts | 96
styles/src/style_tree/tab_bar.ts | 64
styles/src/style_tree/terminal.ts | 52
styles/src/style_tree/titlebar.ts | 80
styles/src/style_tree/toolbar_dropdown_menu.ts | 64
styles/src/style_tree/tooltip.ts | 22
styles/src/style_tree/update_notification.ts | 39
styles/src/style_tree/welcome.ts | 155
styles/src/style_tree/workspace.ts | 190 +
styles/src/system/lib/convert.ts | 11
styles/src/system/lib/curve.ts | 26
styles/src/system/lib/generate.ts | 159
styles/src/system/ref/color.ts | 445 --
styles/src/system/ref/curves.ts | 25
styles/src/system/system.ts | 32
styles/src/system/types.ts | 66
styles/src/theme/color.ts | 2
styles/src/theme/colorScheme.ts | 286 -
styles/src/theme/color_scheme.ts | 282 +
styles/src/theme/index.ts | 4
styles/src/theme/ramps.ts | 44
styles/src/theme/syntax.ts | 117
styles/src/theme/theme_config.ts | 12
styles/src/theme/tokens/colorScheme.ts | 99
styles/src/theme/tokens/color_scheme.ts | 97
styles/src/theme/tokens/layer.ts | 28
styles/src/theme/tokens/players.ts | 37
styles/src/theme/tokens/token.ts | 2
styles/src/themes/andromeda/LICENSE | 2
styles/src/themes/andromeda/andromeda.ts | 26
styles/src/themes/atelier/LICENSE | 2
styles/src/themes/atelier/atelier-cave-dark.ts | 34
styles/src/themes/atelier/atelier-cave-light.ts | 34
styles/src/themes/atelier/atelier-dune-dark.ts | 34
styles/src/themes/atelier/atelier-dune-light.ts | 34
styles/src/themes/atelier/atelier-estuary-dark.ts | 34
styles/src/themes/atelier/atelier-estuary-light.ts | 34
styles/src/themes/atelier/atelier-forest-dark.ts | 34
styles/src/themes/atelier/atelier-forest-light.ts | 34
styles/src/themes/atelier/atelier-heath-dark.ts | 34
styles/src/themes/atelier/atelier-heath-light.ts | 34
styles/src/themes/atelier/atelier-lakeside-dark.ts | 34
styles/src/themes/atelier/atelier-lakeside-light.ts | 34
styles/src/themes/atelier/atelier-plateau-dark.ts | 34
styles/src/themes/atelier/atelier-plateau-light.ts | 34
styles/src/themes/atelier/atelier-savanna-dark.ts | 34
styles/src/themes/atelier/atelier-savanna-light.ts | 34
styles/src/themes/atelier/atelier-seaside-dark.ts | 34
styles/src/themes/atelier/atelier-seaside-light.ts | 34
styles/src/themes/atelier/atelier-sulphurpool-dark.ts | 34
styles/src/themes/atelier/atelier-sulphurpool-light.ts | 34
styles/src/themes/atelier/common.ts | 6
styles/src/themes/ayu/LICENSE | 2
styles/src/themes/ayu/ayu-dark.ts | 12
styles/src/themes/ayu/ayu-light.ts | 12
styles/src/themes/ayu/ayu-mirage.ts | 12
styles/src/themes/ayu/common.ts | 30
styles/src/themes/gruvbox/LICENSE | 2
styles/src/themes/gruvbox/gruvbox-common.ts | 117
styles/src/themes/gruvbox/gruvbox-dark-hard.ts | 2
styles/src/themes/gruvbox/gruvbox-dark-soft.ts | 2
styles/src/themes/gruvbox/gruvbox-dark.ts | 2
styles/src/themes/gruvbox/gruvbox-light-hard.ts | 2
styles/src/themes/gruvbox/gruvbox-light-soft.ts | 2
styles/src/themes/gruvbox/gruvbox-light.ts | 2
styles/src/themes/index.ts | 148
styles/src/themes/one/LICENSE | 2
styles/src/themes/one/one-dark.ts | 39
styles/src/themes/one/one-light.ts | 38
styles/src/themes/rose-pine/LICENSE | 2
styles/src/themes/rose-pine/common.ts | 22
styles/src/themes/rose-pine/rose-pine-dawn.ts | 28
styles/src/themes/rose-pine/rose-pine-moon.ts | 28
styles/src/themes/rose-pine/rose-pine.ts | 28
styles/src/themes/sandcastle/LICENSE | 2
styles/src/themes/sandcastle/sandcastle.ts | 26
styles/src/themes/solarized/LICENSE | 2
styles/src/themes/solarized/solarized.ts | 34
styles/src/themes/summercamp/LICENSE | 2
styles/src/themes/summercamp/summercamp.ts | 26
styles/src/utils/slugify.ts | 4
styles/src/utils/snakeCase.ts | 35
styles/tsconfig.json | 2
204 files changed, 11,741 insertions(+), 6,899 deletions(-)
@@ -4277,6 +4277,7 @@ dependencies = [
"async-tar",
"futures 0.3.28",
"gpui",
+ "log",
"parking_lot 0.11.2",
"serde",
"serde_derive",
@@ -73,6 +73,16 @@
// Whether to show git diff indicators in the scrollbar.
"git_diff": true
},
+ // Inlay hint related settings
+ "inlay_hints": {
+ // Global switch to toggle hints on and off, switched off by default.
+ "enabled": false,
+ // Toggle certain types of hints on and off, all switched on by default.
+ "show_type_hints": true,
+ "show_parameter_hints": true,
+ // Corresponds to null/None LSP hint type value.
+ "show_other_hints": true
+ },
"project_panel": {
// Whether to show the git status in the project panel.
"git_status": true,
@@ -207,16 +207,11 @@ impl ActivityIndicator {
let mut checking_for_update = SmallVec::<[_; 3]>::new();
let mut failed = SmallVec::<[_; 3]>::new();
for status in &self.statuses {
+ let name = status.name.clone();
match status.status {
- LanguageServerBinaryStatus::CheckingForUpdate => {
- checking_for_update.push(status.name.clone());
- }
- LanguageServerBinaryStatus::Downloading => {
- downloading.push(status.name.clone());
- }
- LanguageServerBinaryStatus::Failed { .. } => {
- failed.push(status.name.clone());
- }
+ LanguageServerBinaryStatus::CheckingForUpdate => checking_for_update.push(name),
+ LanguageServerBinaryStatus::Downloading => downloading.push(name),
+ LanguageServerBinaryStatus::Failed { .. } => failed.push(name),
LanguageServerBinaryStatus::Downloaded | LanguageServerBinaryStatus::Cached => {}
}
}
@@ -201,6 +201,7 @@ impl Server {
.add_message_handler(update_language_server)
.add_message_handler(update_diagnostic_summary)
.add_message_handler(update_worktree_settings)
+ .add_message_handler(refresh_inlay_hints)
.add_request_handler(forward_project_request::<proto::GetHover>)
.add_request_handler(forward_project_request::<proto::GetDefinition>)
.add_request_handler(forward_project_request::<proto::GetTypeDefinition>)
@@ -226,6 +227,7 @@ impl Server {
.add_request_handler(forward_project_request::<proto::DeleteProjectEntry>)
.add_request_handler(forward_project_request::<proto::ExpandProjectEntry>)
.add_request_handler(forward_project_request::<proto::OnTypeFormatting>)
+ .add_request_handler(forward_project_request::<proto::InlayHints>)
.add_message_handler(create_buffer_for_peer)
.add_request_handler(update_buffer)
.add_message_handler(update_buffer_file)
@@ -1574,6 +1576,10 @@ async fn update_worktree_settings(
Ok(())
}
+async fn refresh_inlay_hints(request: proto::RefreshInlayHints, session: Session) -> Result<()> {
+ broadcast_project_message(request.project_id, request, session).await
+}
+
async fn start_language_server(
request: proto::StartLanguageServer,
session: Session,
@@ -1750,7 +1756,15 @@ async fn buffer_reloaded(request: proto::BufferReloaded, session: Session) -> Re
}
async fn buffer_saved(request: proto::BufferSaved, session: Session) -> Result<()> {
- let project_id = ProjectId::from_proto(request.project_id);
+ broadcast_project_message(request.project_id, request, session).await
+}
+
+async fn broadcast_project_message<T: EnvelopedMessage>(
+ project_id: u64,
+ request: T,
+ session: Session,
+) -> Result<()> {
+ let project_id = ProjectId::from_proto(project_id);
let project_connection_ids = session
.db()
.await
@@ -18,7 +18,7 @@ use gpui::{
};
use indoc::indoc;
use language::{
- language_settings::{AllLanguageSettings, Formatter},
+ language_settings::{AllLanguageSettings, Formatter, InlayHintKind, InlayHintSettings},
tree_sitter_rust, Anchor, Diagnostic, DiagnosticEntry, FakeLspAdapter, Language,
LanguageConfig, OffsetRangeExt, Point, Rope,
};
@@ -34,7 +34,7 @@ use std::{
path::{Path, PathBuf},
rc::Rc,
sync::{
- atomic::{AtomicBool, Ordering::SeqCst},
+ atomic::{AtomicBool, AtomicU32, Ordering::SeqCst},
Arc,
},
};
@@ -7800,6 +7800,572 @@ async fn test_on_input_format_from_guest_to_host(
});
}
+#[gpui::test]
+async fn test_mutual_editor_inlay_hint_cache_update(
+ deterministic: Arc<Deterministic>,
+ cx_a: &mut TestAppContext,
+ cx_b: &mut TestAppContext,
+) {
+ deterministic.forbid_parking();
+ let mut server = TestServer::start(&deterministic).await;
+ let client_a = server.create_client(cx_a, "user_a").await;
+ let client_b = server.create_client(cx_b, "user_b").await;
+ server
+ .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
+ .await;
+ let active_call_a = cx_a.read(ActiveCall::global);
+ let active_call_b = cx_b.read(ActiveCall::global);
+
+ cx_a.update(editor::init);
+ cx_b.update(editor::init);
+
+ cx_a.update(|cx| {
+ cx.update_global(|store: &mut SettingsStore, cx| {
+ store.update_user_settings::<AllLanguageSettings>(cx, |settings| {
+ settings.defaults.inlay_hints = Some(InlayHintSettings {
+ enabled: true,
+ show_type_hints: true,
+ show_parameter_hints: false,
+ show_other_hints: true,
+ })
+ });
+ });
+ });
+ cx_b.update(|cx| {
+ cx.update_global(|store: &mut SettingsStore, cx| {
+ store.update_user_settings::<AllLanguageSettings>(cx, |settings| {
+ settings.defaults.inlay_hints = Some(InlayHintSettings {
+ enabled: true,
+ show_type_hints: true,
+ show_parameter_hints: false,
+ show_other_hints: true,
+ })
+ });
+ });
+ });
+ let allowed_hint_kinds = HashSet::from_iter([None, Some(InlayHintKind::Type)]);
+
+ let mut language = Language::new(
+ LanguageConfig {
+ name: "Rust".into(),
+ path_suffixes: vec!["rs".to_string()],
+ ..Default::default()
+ },
+ Some(tree_sitter_rust::language()),
+ );
+ let mut fake_language_servers = language
+ .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
+ capabilities: lsp::ServerCapabilities {
+ inlay_hint_provider: Some(lsp::OneOf::Left(true)),
+ ..Default::default()
+ },
+ ..Default::default()
+ }))
+ .await;
+ let language = Arc::new(language);
+ client_a.language_registry.add(Arc::clone(&language));
+ client_b.language_registry.add(language);
+
+ client_a
+ .fs
+ .insert_tree(
+ "/a",
+ json!({
+ "main.rs": "fn main() { a } // and some long comment to ensure inlays are not trimmed out",
+ "other.rs": "// Test file",
+ }),
+ )
+ .await;
+ let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
+ active_call_a
+ .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx))
+ .await
+ .unwrap();
+ let project_id = active_call_a
+ .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
+ .await
+ .unwrap();
+
+ let project_b = client_b.build_remote_project(project_id, cx_b).await;
+ active_call_b
+ .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx))
+ .await
+ .unwrap();
+
+ let workspace_a = client_a.build_workspace(&project_a, cx_a);
+ cx_a.foreground().start_waiting();
+
+ let _buffer_a = project_a
+ .update(cx_a, |project, cx| {
+ project.open_local_buffer("/a/main.rs", cx)
+ })
+ .await
+ .unwrap();
+ let fake_language_server = fake_language_servers.next().await.unwrap();
+ let next_call_id = Arc::new(AtomicU32::new(0));
+ let editor_a = workspace_a
+ .update(cx_a, |workspace, cx| {
+ workspace.open_path((worktree_id, "main.rs"), None, true, cx)
+ })
+ .await
+ .unwrap()
+ .downcast::<Editor>()
+ .unwrap();
+ fake_language_server
+ .handle_request::<lsp::request::InlayHintRequest, _, _>(move |params, _| {
+ let task_next_call_id = Arc::clone(&next_call_id);
+ async move {
+ assert_eq!(
+ params.text_document.uri,
+ lsp::Url::from_file_path("/a/main.rs").unwrap(),
+ );
+ let mut current_call_id = Arc::clone(&task_next_call_id).fetch_add(1, SeqCst);
+ let mut new_hints = Vec::with_capacity(current_call_id as usize);
+ loop {
+ new_hints.push(lsp::InlayHint {
+ position: lsp::Position::new(0, current_call_id),
+ label: lsp::InlayHintLabel::String(current_call_id.to_string()),
+ kind: None,
+ text_edits: None,
+ tooltip: None,
+ padding_left: None,
+ padding_right: None,
+ data: None,
+ });
+ if current_call_id == 0 {
+ break;
+ }
+ current_call_id -= 1;
+ }
+ Ok(Some(new_hints))
+ }
+ })
+ .next()
+ .await
+ .unwrap();
+
+ cx_a.foreground().finish_waiting();
+ cx_a.foreground().run_until_parked();
+
+ let mut edits_made = 1;
+ editor_a.update(cx_a, |editor, _| {
+ assert_eq!(
+ vec!["0".to_string()],
+ extract_hint_labels(editor),
+ "Host should get its first hints when opens an editor"
+ );
+ let inlay_cache = editor.inlay_hint_cache();
+ assert_eq!(
+ inlay_cache.allowed_hint_kinds, allowed_hint_kinds,
+ "Cache should use editor settings to get the allowed hint kinds"
+ );
+ assert_eq!(
+ inlay_cache.version, edits_made,
+ "Host editor update the cache version after every cache/view change",
+ );
+ });
+ let workspace_b = client_b.build_workspace(&project_b, cx_b);
+ let editor_b = workspace_b
+ .update(cx_b, |workspace, cx| {
+ workspace.open_path((worktree_id, "main.rs"), None, true, cx)
+ })
+ .await
+ .unwrap()
+ .downcast::<Editor>()
+ .unwrap();
+
+ cx_b.foreground().run_until_parked();
+ editor_b.update(cx_b, |editor, _| {
+ assert_eq!(
+ vec!["0".to_string(), "1".to_string()],
+ extract_hint_labels(editor),
+ "Client should get its first hints when opens an editor"
+ );
+ let inlay_cache = editor.inlay_hint_cache();
+ assert_eq!(
+ inlay_cache.allowed_hint_kinds, allowed_hint_kinds,
+ "Cache should use editor settings to get the allowed hint kinds"
+ );
+ assert_eq!(
+ inlay_cache.version, edits_made,
+ "Guest editor update the cache version after every cache/view change"
+ );
+ });
+
+ editor_b.update(cx_b, |editor, cx| {
+ editor.change_selections(None, cx, |s| s.select_ranges([13..13].clone()));
+ editor.handle_input(":", cx);
+ cx.focus(&editor_b);
+ edits_made += 1;
+ });
+ cx_a.foreground().run_until_parked();
+ cx_b.foreground().run_until_parked();
+ editor_a.update(cx_a, |editor, _| {
+ assert_eq!(
+ vec!["0".to_string(), "1".to_string(), "2".to_string()],
+ extract_hint_labels(editor),
+ "Host should get hints from the 1st edit and 1st LSP query"
+ );
+ let inlay_cache = editor.inlay_hint_cache();
+ assert_eq!(
+ inlay_cache.allowed_hint_kinds, allowed_hint_kinds,
+ "Inlay kinds settings never change during the test"
+ );
+ assert_eq!(inlay_cache.version, edits_made);
+ });
+ editor_b.update(cx_b, |editor, _| {
+ assert_eq!(
+ vec![
+ "0".to_string(),
+ "1".to_string(),
+ "2".to_string(),
+ "3".to_string()
+ ],
+ extract_hint_labels(editor),
+ "Guest should get hints the 1st edit and 2nd LSP query"
+ );
+ let inlay_cache = editor.inlay_hint_cache();
+ assert_eq!(
+ inlay_cache.allowed_hint_kinds, allowed_hint_kinds,
+ "Inlay kinds settings never change during the test"
+ );
+ assert_eq!(inlay_cache.version, edits_made);
+ });
+
+ editor_a.update(cx_a, |editor, cx| {
+ editor.change_selections(None, cx, |s| s.select_ranges([13..13]));
+ editor.handle_input("a change to increment both buffers' versions", cx);
+ cx.focus(&editor_a);
+ edits_made += 1;
+ });
+ cx_a.foreground().run_until_parked();
+ cx_b.foreground().run_until_parked();
+ editor_a.update(cx_a, |editor, _| {
+ assert_eq!(
+ vec![
+ "0".to_string(),
+ "1".to_string(),
+ "2".to_string(),
+ "3".to_string(),
+ "4".to_string()
+ ],
+ extract_hint_labels(editor),
+ "Host should get hints from 3rd edit, 5th LSP query: \
+4th query was made by guest (but not applied) due to cache invalidation logic"
+ );
+ let inlay_cache = editor.inlay_hint_cache();
+ assert_eq!(
+ inlay_cache.allowed_hint_kinds, allowed_hint_kinds,
+ "Inlay kinds settings never change during the test"
+ );
+ assert_eq!(inlay_cache.version, edits_made);
+ });
+ editor_b.update(cx_b, |editor, _| {
+ assert_eq!(
+ vec![
+ "0".to_string(),
+ "1".to_string(),
+ "2".to_string(),
+ "3".to_string(),
+ "4".to_string(),
+ "5".to_string(),
+ ],
+ extract_hint_labels(editor),
+ "Guest should get hints from 3rd edit, 6th LSP query"
+ );
+ let inlay_cache = editor.inlay_hint_cache();
+ assert_eq!(
+ inlay_cache.allowed_hint_kinds, allowed_hint_kinds,
+ "Inlay kinds settings never change during the test"
+ );
+ assert_eq!(inlay_cache.version, edits_made);
+ });
+
+ fake_language_server
+ .request::<lsp::request::InlayHintRefreshRequest>(())
+ .await
+ .expect("inlay refresh request failed");
+ edits_made += 1;
+ cx_a.foreground().run_until_parked();
+ cx_b.foreground().run_until_parked();
+ editor_a.update(cx_a, |editor, _| {
+ assert_eq!(
+ vec![
+ "0".to_string(),
+ "1".to_string(),
+ "2".to_string(),
+ "3".to_string(),
+ "4".to_string(),
+ "5".to_string(),
+ "6".to_string(),
+ ],
+ extract_hint_labels(editor),
+ "Host should react to /refresh LSP request and get new hints from 7th LSP query"
+ );
+ let inlay_cache = editor.inlay_hint_cache();
+ assert_eq!(
+ inlay_cache.allowed_hint_kinds, allowed_hint_kinds,
+ "Inlay kinds settings never change during the test"
+ );
+ assert_eq!(
+ inlay_cache.version, edits_made,
+ "Host should accepted all edits and bump its cache version every time"
+ );
+ });
+ editor_b.update(cx_b, |editor, _| {
+ assert_eq!(
+ vec![
+ "0".to_string(),
+ "1".to_string(),
+ "2".to_string(),
+ "3".to_string(),
+ "4".to_string(),
+ "5".to_string(),
+ "6".to_string(),
+ "7".to_string(),
+ ],
+ extract_hint_labels(editor),
+ "Guest should get a /refresh LSP request propagated by host and get new hints from 8th LSP query"
+ );
+ let inlay_cache = editor.inlay_hint_cache();
+ assert_eq!(
+ inlay_cache.allowed_hint_kinds, allowed_hint_kinds,
+ "Inlay kinds settings never change during the test"
+ );
+ assert_eq!(
+ inlay_cache.version,
+ edits_made,
+ "Guest should accepted all edits and bump its cache version every time"
+ );
+ });
+}
+
+#[gpui::test]
+async fn test_inlay_hint_refresh_is_forwarded(
+ deterministic: Arc<Deterministic>,
+ cx_a: &mut TestAppContext,
+ cx_b: &mut TestAppContext,
+) {
+ deterministic.forbid_parking();
+ let mut server = TestServer::start(&deterministic).await;
+ let client_a = server.create_client(cx_a, "user_a").await;
+ let client_b = server.create_client(cx_b, "user_b").await;
+ server
+ .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
+ .await;
+ let active_call_a = cx_a.read(ActiveCall::global);
+ let active_call_b = cx_b.read(ActiveCall::global);
+
+ cx_a.update(editor::init);
+ cx_b.update(editor::init);
+
+ cx_a.update(|cx| {
+ cx.update_global(|store: &mut SettingsStore, cx| {
+ store.update_user_settings::<AllLanguageSettings>(cx, |settings| {
+ settings.defaults.inlay_hints = Some(InlayHintSettings {
+ enabled: false,
+ show_type_hints: true,
+ show_parameter_hints: false,
+ show_other_hints: true,
+ })
+ });
+ });
+ });
+ cx_b.update(|cx| {
+ cx.update_global(|store: &mut SettingsStore, cx| {
+ store.update_user_settings::<AllLanguageSettings>(cx, |settings| {
+ settings.defaults.inlay_hints = Some(InlayHintSettings {
+ enabled: true,
+ show_type_hints: true,
+ show_parameter_hints: false,
+ show_other_hints: true,
+ })
+ });
+ });
+ });
+ let allowed_hint_kinds = HashSet::from_iter([None, Some(InlayHintKind::Type)]);
+
+ let mut language = Language::new(
+ LanguageConfig {
+ name: "Rust".into(),
+ path_suffixes: vec!["rs".to_string()],
+ ..Default::default()
+ },
+ Some(tree_sitter_rust::language()),
+ );
+ let mut fake_language_servers = language
+ .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
+ capabilities: lsp::ServerCapabilities {
+ inlay_hint_provider: Some(lsp::OneOf::Left(true)),
+ ..Default::default()
+ },
+ ..Default::default()
+ }))
+ .await;
+ let language = Arc::new(language);
+ client_a.language_registry.add(Arc::clone(&language));
+ client_b.language_registry.add(language);
+
+ client_a
+ .fs
+ .insert_tree(
+ "/a",
+ json!({
+ "main.rs": "fn main() { a } // and some long comment to ensure inlays are not trimmed out",
+ "other.rs": "// Test file",
+ }),
+ )
+ .await;
+ let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
+ active_call_a
+ .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx))
+ .await
+ .unwrap();
+ let project_id = active_call_a
+ .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
+ .await
+ .unwrap();
+
+ let project_b = client_b.build_remote_project(project_id, cx_b).await;
+ active_call_b
+ .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx))
+ .await
+ .unwrap();
+
+ let workspace_a = client_a.build_workspace(&project_a, cx_a);
+ let workspace_b = client_b.build_workspace(&project_b, cx_b);
+ cx_a.foreground().start_waiting();
+ cx_b.foreground().start_waiting();
+
+ let editor_a = workspace_a
+ .update(cx_a, |workspace, cx| {
+ workspace.open_path((worktree_id, "main.rs"), None, true, cx)
+ })
+ .await
+ .unwrap()
+ .downcast::<Editor>()
+ .unwrap();
+
+ let editor_b = workspace_b
+ .update(cx_b, |workspace, cx| {
+ workspace.open_path((worktree_id, "main.rs"), None, true, cx)
+ })
+ .await
+ .unwrap()
+ .downcast::<Editor>()
+ .unwrap();
+
+ let fake_language_server = fake_language_servers.next().await.unwrap();
+ let next_call_id = Arc::new(AtomicU32::new(0));
+ fake_language_server
+ .handle_request::<lsp::request::InlayHintRequest, _, _>(move |params, _| {
+ let task_next_call_id = Arc::clone(&next_call_id);
+ async move {
+ assert_eq!(
+ params.text_document.uri,
+ lsp::Url::from_file_path("/a/main.rs").unwrap(),
+ );
+ let mut current_call_id = Arc::clone(&task_next_call_id).fetch_add(1, SeqCst);
+ let mut new_hints = Vec::with_capacity(current_call_id as usize);
+ loop {
+ new_hints.push(lsp::InlayHint {
+ position: lsp::Position::new(0, current_call_id),
+ label: lsp::InlayHintLabel::String(current_call_id.to_string()),
+ kind: None,
+ text_edits: None,
+ tooltip: None,
+ padding_left: None,
+ padding_right: None,
+ data: None,
+ });
+ if current_call_id == 0 {
+ break;
+ }
+ current_call_id -= 1;
+ }
+ Ok(Some(new_hints))
+ }
+ })
+ .next()
+ .await
+ .unwrap();
+ cx_a.foreground().finish_waiting();
+ cx_b.foreground().finish_waiting();
+
+ cx_a.foreground().run_until_parked();
+ editor_a.update(cx_a, |editor, _| {
+ assert!(
+ extract_hint_labels(editor).is_empty(),
+ "Host should get no hints due to them turned off"
+ );
+ let inlay_cache = editor.inlay_hint_cache();
+ assert_eq!(
+ inlay_cache.allowed_hint_kinds, allowed_hint_kinds,
+ "Host should have allowed hint kinds set despite hints are off"
+ );
+ assert_eq!(
+ inlay_cache.version, 0,
+ "Host should not increment its cache version due to no changes",
+ );
+ });
+
+ let mut edits_made = 1;
+ cx_b.foreground().run_until_parked();
+ editor_b.update(cx_b, |editor, _| {
+ assert_eq!(
+ vec!["0".to_string()],
+ extract_hint_labels(editor),
+ "Client should get its first hints when opens an editor"
+ );
+ let inlay_cache = editor.inlay_hint_cache();
+ assert_eq!(
+ inlay_cache.allowed_hint_kinds, allowed_hint_kinds,
+ "Cache should use editor settings to get the allowed hint kinds"
+ );
+ assert_eq!(
+ inlay_cache.version, edits_made,
+ "Guest editor update the cache version after every cache/view change"
+ );
+ });
+
+ fake_language_server
+ .request::<lsp::request::InlayHintRefreshRequest>(())
+ .await
+ .expect("inlay refresh request failed");
+ cx_a.foreground().run_until_parked();
+ editor_a.update(cx_a, |editor, _| {
+ assert!(
+ extract_hint_labels(editor).is_empty(),
+ "Host should get nop hints due to them turned off, even after the /refresh"
+ );
+ let inlay_cache = editor.inlay_hint_cache();
+ assert_eq!(inlay_cache.allowed_hint_kinds, allowed_hint_kinds);
+ assert_eq!(
+ inlay_cache.version, 0,
+ "Host should not increment its cache version due to no changes",
+ );
+ });
+
+ edits_made += 1;
+ cx_b.foreground().run_until_parked();
+ editor_b.update(cx_b, |editor, _| {
+ assert_eq!(
+ vec!["0".to_string(), "1".to_string(),],
+ extract_hint_labels(editor),
+ "Guest should get a /refresh LSP request propagated by host despite host hints are off"
+ );
+ let inlay_cache = editor.inlay_hint_cache();
+ assert_eq!(
+ inlay_cache.allowed_hint_kinds, allowed_hint_kinds,
+ "Inlay kinds settings never change during the test"
+ );
+ assert_eq!(
+ inlay_cache.version, edits_made,
+ "Guest should accepted all edits and bump its cache version every time"
+ );
+ });
+}
+
#[derive(Debug, Eq, PartialEq)]
struct RoomParticipants {
remote: Vec<String>,
@@ -7823,3 +8389,17 @@ fn room_participants(room: &ModelHandle<Room>, cx: &mut TestAppContext) -> RoomP
RoomParticipants { remote, pending }
})
}
+
+fn extract_hint_labels(editor: &Editor) -> Vec<String> {
+ let mut labels = Vec::new();
+ for (_, excerpt_hints) in &editor.inlay_hint_cache().hints {
+ let excerpt_hints = excerpt_hints.read();
+ for (_, inlay) in excerpt_hints.hints.iter() {
+ match &inlay.label {
+ project::InlayHintLabel::String(s) => labels.push(s.to_string()),
+ _ => unreachable!(),
+ }
+ }
+ }
+ labels
+}
@@ -317,7 +317,7 @@ impl CollabTitlebarItem {
),
]
};
- user_menu.show(Default::default(), AnchorCorner::TopRight, items, cx);
+ user_menu.toggle(Default::default(), AnchorCorner::TopRight, items, cx);
});
}
@@ -683,6 +683,9 @@ impl CollabTitlebarItem {
.into_any()
})
.with_cursor_style(CursorStyle::PointingHand)
+ .on_down(MouseButton::Left, move |_, this, cx| {
+ this.user_menu.update(cx, |menu, _| menu.delay_cancel());
+ })
.on_click(MouseButton::Left, move |_, this, cx| {
this.toggle_user_menu(&Default::default(), cx)
})
@@ -124,6 +124,7 @@ pub struct ContextMenu {
items: Vec<ContextMenuItem>,
selected_index: Option<usize>,
visible: bool,
+ delay_cancel: bool,
previously_focused_view_id: Option<usize>,
parent_view_id: usize,
_actions_observation: Subscription,
@@ -178,6 +179,7 @@ impl ContextMenu {
pub fn new(parent_view_id: usize, cx: &mut ViewContext<Self>) -> Self {
Self {
show_count: 0,
+ delay_cancel: false,
anchor_position: Default::default(),
anchor_corner: AnchorCorner::TopLeft,
position_mode: OverlayPositionMode::Window,
@@ -232,15 +234,23 @@ impl ContextMenu {
}
}
+ pub fn delay_cancel(&mut self) {
+ self.delay_cancel = true;
+ }
+
fn cancel(&mut self, _: &Cancel, cx: &mut ViewContext<Self>) {
- self.reset(cx);
- let show_count = self.show_count;
- cx.defer(move |this, cx| {
- if cx.handle().is_focused(cx) && this.show_count == show_count {
- let window_id = cx.window_id();
- (**cx).focus(window_id, this.previously_focused_view_id.take());
- }
- });
+ if !self.delay_cancel {
+ self.reset(cx);
+ let show_count = self.show_count;
+ cx.defer(move |this, cx| {
+ if cx.handle().is_focused(cx) && this.show_count == show_count {
+ let window_id = cx.window_id();
+ (**cx).focus(window_id, this.previously_focused_view_id.take());
+ }
+ });
+ } else {
+ self.delay_cancel = false;
+ }
}
fn reset(&mut self, cx: &mut ViewContext<Self>) {
@@ -293,6 +303,34 @@ impl ContextMenu {
}
}
+ pub fn toggle(
+ &mut self,
+ anchor_position: Vector2F,
+ anchor_corner: AnchorCorner,
+ items: Vec<ContextMenuItem>,
+ cx: &mut ViewContext<Self>,
+ ) {
+ if self.visible() {
+ self.cancel(&Cancel, cx);
+ } else {
+ let mut items = items.into_iter().peekable();
+ if items.peek().is_some() {
+ self.items = items.collect();
+ self.anchor_position = anchor_position;
+ self.anchor_corner = anchor_corner;
+ self.visible = true;
+ self.show_count += 1;
+ if !cx.is_self_focused() {
+ self.previously_focused_view_id = cx.focused_view_id();
+ }
+ cx.focus_self();
+ } else {
+ self.visible = false;
+ }
+ }
+ cx.notify();
+ }
+
pub fn show(
&mut self,
anchor_position: Vector2F,
@@ -15,7 +15,7 @@ use language::{
ToPointUtf16,
};
use log::{debug, error};
-use lsp::{LanguageServer, LanguageServerId};
+use lsp::{LanguageServer, LanguageServerBinary, LanguageServerId};
use node_runtime::NodeRuntime;
use request::{LogMessage, StatusNotification};
use settings::SettingsStore;
@@ -340,7 +340,7 @@ impl Copilot {
let http = util::http::FakeHttpClient::create(|_| async { unreachable!() });
let this = cx.add_model(|cx| Self {
http: http.clone(),
- node_runtime: NodeRuntime::new(http, cx.background().clone()),
+ node_runtime: NodeRuntime::instance(http, cx.background().clone()),
server: CopilotServer::Running(RunningCopilotServer {
lsp: Arc::new(server),
sign_in_status: SignInStatus::Authorized,
@@ -361,11 +361,14 @@ impl Copilot {
let start_language_server = async {
let server_path = get_copilot_lsp(http).await?;
let node_path = node_runtime.binary_path().await?;
- let arguments: &[OsString] = &[server_path.into(), "--stdio".into()];
+ let arguments: Vec<OsString> = vec![server_path.into(), "--stdio".into()];
+ let binary = LanguageServerBinary {
+ path: node_path,
+ arguments,
+ };
let server = LanguageServer::new(
LanguageServerId(0),
- &node_path,
- arguments,
+ binary,
Path::new("/"),
None,
cx.clone(),
@@ -102,6 +102,9 @@ impl View for CopilotButton {
}
})
.with_cursor_style(CursorStyle::PointingHand)
+ .on_down(MouseButton::Left, |_, this, cx| {
+ this.popup_menu.update(cx, |menu, _| menu.delay_cancel());
+ })
.on_click(MouseButton::Left, {
let status = status.clone();
move |_, this, cx| match status {
@@ -186,7 +189,7 @@ impl CopilotButton {
}));
self.popup_menu.update(cx, |menu, cx| {
- menu.show(
+ menu.toggle(
Default::default(),
AnchorCorner::BottomRight,
menu_options,
@@ -266,7 +269,7 @@ impl CopilotButton {
menu_options.push(ContextMenuItem::action("Sign Out", SignOut));
self.popup_menu.update(cx, |menu, cx| {
- menu.show(
+ menu.toggle(
Default::default(),
AnchorCorner::BottomRight,
menu_options,
@@ -1,26 +1,26 @@
mod block_map;
mod fold_map;
-mod suggestion_map;
+mod inlay_map;
mod tab_map;
mod wrap_map;
-use crate::{Anchor, AnchorRangeExt, MultiBuffer, MultiBufferSnapshot, ToOffset, ToPoint};
+use crate::{Anchor, AnchorRangeExt, InlayId, MultiBuffer, MultiBufferSnapshot, ToOffset, ToPoint};
pub use block_map::{BlockMap, BlockPoint};
use collections::{HashMap, HashSet};
-use fold_map::{FoldMap, FoldOffset};
+use fold_map::FoldMap;
use gpui::{
color::Color,
fonts::{FontId, HighlightStyle},
Entity, ModelContext, ModelHandle,
};
+use inlay_map::InlayMap;
use language::{
language_settings::language_settings, OffsetUtf16, Point, Subscription as BufferSubscription,
};
use std::{any::TypeId, fmt::Debug, num::NonZeroU32, ops::Range, sync::Arc};
-pub use suggestion_map::Suggestion;
-use suggestion_map::SuggestionMap;
use sum_tree::{Bias, TreeMap};
use tab_map::TabMap;
+use text::Rope;
use wrap_map::WrapMap;
pub use block_map::{
@@ -28,6 +28,8 @@ pub use block_map::{
BlockDisposition, BlockId, BlockProperties, BlockStyle, RenderBlock, TransformBlock,
};
+pub use self::inlay_map::{Inlay, InlayProperties};
+
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub enum FoldStatus {
Folded,
@@ -44,7 +46,7 @@ pub struct DisplayMap {
buffer: ModelHandle<MultiBuffer>,
buffer_subscription: BufferSubscription,
fold_map: FoldMap,
- suggestion_map: SuggestionMap,
+ inlay_map: InlayMap,
tab_map: TabMap,
wrap_map: ModelHandle<WrapMap>,
block_map: BlockMap,
@@ -69,8 +71,8 @@ impl DisplayMap {
let buffer_subscription = buffer.update(cx, |buffer, _| buffer.subscribe());
let tab_size = Self::tab_size(&buffer, cx);
- let (fold_map, snapshot) = FoldMap::new(buffer.read(cx).snapshot(cx));
- let (suggestion_map, snapshot) = SuggestionMap::new(snapshot);
+ let (inlay_map, snapshot) = InlayMap::new(buffer.read(cx).snapshot(cx));
+ let (fold_map, snapshot) = FoldMap::new(snapshot);
let (tab_map, snapshot) = TabMap::new(snapshot, tab_size);
let (wrap_map, snapshot) = WrapMap::new(snapshot, font_id, font_size, wrap_width, cx);
let block_map = BlockMap::new(snapshot, buffer_header_height, excerpt_header_height);
@@ -79,7 +81,7 @@ impl DisplayMap {
buffer,
buffer_subscription,
fold_map,
- suggestion_map,
+ inlay_map,
tab_map,
wrap_map,
block_map,
@@ -88,16 +90,13 @@ impl DisplayMap {
}
}
- pub fn snapshot(&self, cx: &mut ModelContext<Self>) -> DisplaySnapshot {
+ pub fn snapshot(&mut self, cx: &mut ModelContext<Self>) -> DisplaySnapshot {
let buffer_snapshot = self.buffer.read(cx).snapshot(cx);
let edits = self.buffer_subscription.consume().into_inner();
- let (fold_snapshot, edits) = self.fold_map.read(buffer_snapshot, edits);
- let (suggestion_snapshot, edits) = self.suggestion_map.sync(fold_snapshot.clone(), edits);
-
+ let (inlay_snapshot, edits) = self.inlay_map.sync(buffer_snapshot, edits);
+ let (fold_snapshot, edits) = self.fold_map.read(inlay_snapshot.clone(), edits);
let tab_size = Self::tab_size(&self.buffer, cx);
- let (tab_snapshot, edits) = self
- .tab_map
- .sync(suggestion_snapshot.clone(), edits, tab_size);
+ let (tab_snapshot, edits) = self.tab_map.sync(fold_snapshot.clone(), edits, tab_size);
let (wrap_snapshot, edits) = self
.wrap_map
.update(cx, |map, cx| map.sync(tab_snapshot.clone(), edits, cx));
@@ -106,7 +105,7 @@ impl DisplayMap {
DisplaySnapshot {
buffer_snapshot: self.buffer.read(cx).snapshot(cx),
fold_snapshot,
- suggestion_snapshot,
+ inlay_snapshot,
tab_snapshot,
wrap_snapshot,
block_snapshot,
@@ -132,15 +131,14 @@ impl DisplayMap {
let snapshot = self.buffer.read(cx).snapshot(cx);
let edits = self.buffer_subscription.consume().into_inner();
let tab_size = Self::tab_size(&self.buffer, cx);
+ let (snapshot, edits) = self.inlay_map.sync(snapshot, edits);
let (mut fold_map, snapshot, edits) = self.fold_map.write(snapshot, edits);
- let (snapshot, edits) = self.suggestion_map.sync(snapshot, edits);
let (snapshot, edits) = self.tab_map.sync(snapshot, edits, tab_size);
let (snapshot, edits) = self
.wrap_map
.update(cx, |map, cx| map.sync(snapshot, edits, cx));
self.block_map.read(snapshot, edits);
let (snapshot, edits) = fold_map.fold(ranges);
- let (snapshot, edits) = self.suggestion_map.sync(snapshot, edits);
let (snapshot, edits) = self.tab_map.sync(snapshot, edits, tab_size);
let (snapshot, edits) = self
.wrap_map
@@ -157,15 +155,14 @@ impl DisplayMap {
let snapshot = self.buffer.read(cx).snapshot(cx);
let edits = self.buffer_subscription.consume().into_inner();
let tab_size = Self::tab_size(&self.buffer, cx);
+ let (snapshot, edits) = self.inlay_map.sync(snapshot, edits);
let (mut fold_map, snapshot, edits) = self.fold_map.write(snapshot, edits);
- let (snapshot, edits) = self.suggestion_map.sync(snapshot, edits);
let (snapshot, edits) = self.tab_map.sync(snapshot, edits, tab_size);
let (snapshot, edits) = self
.wrap_map
.update(cx, |map, cx| map.sync(snapshot, edits, cx));
self.block_map.read(snapshot, edits);
let (snapshot, edits) = fold_map.unfold(ranges, inclusive);
- let (snapshot, edits) = self.suggestion_map.sync(snapshot, edits);
let (snapshot, edits) = self.tab_map.sync(snapshot, edits, tab_size);
let (snapshot, edits) = self
.wrap_map
@@ -181,8 +178,8 @@ impl DisplayMap {
let snapshot = self.buffer.read(cx).snapshot(cx);
let edits = self.buffer_subscription.consume().into_inner();
let tab_size = Self::tab_size(&self.buffer, cx);
+ let (snapshot, edits) = self.inlay_map.sync(snapshot, edits);
let (snapshot, edits) = self.fold_map.read(snapshot, edits);
- let (snapshot, edits) = self.suggestion_map.sync(snapshot, edits);
let (snapshot, edits) = self.tab_map.sync(snapshot, edits, tab_size);
let (snapshot, edits) = self
.wrap_map
@@ -199,8 +196,8 @@ impl DisplayMap {
let snapshot = self.buffer.read(cx).snapshot(cx);
let edits = self.buffer_subscription.consume().into_inner();
let tab_size = Self::tab_size(&self.buffer, cx);
+ let (snapshot, edits) = self.inlay_map.sync(snapshot, edits);
let (snapshot, edits) = self.fold_map.read(snapshot, edits);
- let (snapshot, edits) = self.suggestion_map.sync(snapshot, edits);
let (snapshot, edits) = self.tab_map.sync(snapshot, edits, tab_size);
let (snapshot, edits) = self
.wrap_map
@@ -231,32 +228,6 @@ impl DisplayMap {
self.text_highlights.remove(&Some(type_id))
}
- pub fn has_suggestion(&self) -> bool {
- self.suggestion_map.has_suggestion()
- }
-
- pub fn replace_suggestion<T>(
- &self,
- new_suggestion: Option<Suggestion<T>>,
- cx: &mut ModelContext<Self>,
- ) -> Option<Suggestion<FoldOffset>>
- where
- T: ToPoint,
- {
- let snapshot = self.buffer.read(cx).snapshot(cx);
- let edits = self.buffer_subscription.consume().into_inner();
- let tab_size = Self::tab_size(&self.buffer, cx);
- let (snapshot, edits) = self.fold_map.read(snapshot, edits);
- let (snapshot, edits, old_suggestion) =
- self.suggestion_map.replace(new_suggestion, snapshot, edits);
- let (snapshot, edits) = self.tab_map.sync(snapshot, edits, tab_size);
- let (snapshot, edits) = self
- .wrap_map
- .update(cx, |map, cx| map.sync(snapshot, edits, cx));
- self.block_map.read(snapshot, edits);
- old_suggestion
- }
-
pub fn set_font(&self, font_id: FontId, font_size: f32, cx: &mut ModelContext<Self>) -> bool {
self.wrap_map
.update(cx, |map, cx| map.set_font(font_id, font_size, cx))
@@ -271,6 +242,39 @@ impl DisplayMap {
.update(cx, |map, cx| map.set_wrap_width(width, cx))
}
+ pub fn current_inlays(&self) -> impl Iterator<Item = &Inlay> {
+ self.inlay_map.current_inlays()
+ }
+
+ pub fn splice_inlays<T: Into<Rope>>(
+ &mut self,
+ to_remove: Vec<InlayId>,
+ to_insert: Vec<(InlayId, InlayProperties<T>)>,
+ cx: &mut ModelContext<Self>,
+ ) {
+ if to_remove.is_empty() && to_insert.is_empty() {
+ return;
+ }
+ let buffer_snapshot = self.buffer.read(cx).snapshot(cx);
+ let edits = self.buffer_subscription.consume().into_inner();
+ let (snapshot, edits) = self.inlay_map.sync(buffer_snapshot, edits);
+ let (snapshot, edits) = self.fold_map.read(snapshot, edits);
+ let tab_size = Self::tab_size(&self.buffer, cx);
+ let (snapshot, edits) = self.tab_map.sync(snapshot, edits, tab_size);
+ let (snapshot, edits) = self
+ .wrap_map
+ .update(cx, |map, cx| map.sync(snapshot, edits, cx));
+ self.block_map.read(snapshot, edits);
+
+ let (snapshot, edits) = self.inlay_map.splice(to_remove, to_insert);
+ let (snapshot, edits) = self.fold_map.read(snapshot, edits);
+ let (snapshot, edits) = self.tab_map.sync(snapshot, edits, tab_size);
+ let (snapshot, edits) = self
+ .wrap_map
+ .update(cx, |map, cx| map.sync(snapshot, edits, cx));
+ self.block_map.read(snapshot, edits);
+ }
+
fn tab_size(buffer: &ModelHandle<MultiBuffer>, cx: &mut ModelContext<Self>) -> NonZeroU32 {
let language = buffer
.read(cx)
@@ -288,7 +292,7 @@ impl DisplayMap {
pub struct DisplaySnapshot {
pub buffer_snapshot: MultiBufferSnapshot,
fold_snapshot: fold_map::FoldSnapshot,
- suggestion_snapshot: suggestion_map::SuggestionSnapshot,
+ inlay_snapshot: inlay_map::InlaySnapshot,
tab_snapshot: tab_map::TabSnapshot,
wrap_snapshot: wrap_map::WrapSnapshot,
block_snapshot: block_map::BlockSnapshot,
@@ -316,9 +320,11 @@ impl DisplaySnapshot {
pub fn prev_line_boundary(&self, mut point: Point) -> (Point, DisplayPoint) {
loop {
- let mut fold_point = self.fold_snapshot.to_fold_point(point, Bias::Left);
- *fold_point.column_mut() = 0;
- point = fold_point.to_buffer_point(&self.fold_snapshot);
+ let mut inlay_point = self.inlay_snapshot.to_inlay_point(point);
+ let mut fold_point = self.fold_snapshot.to_fold_point(inlay_point, Bias::Left);
+ fold_point.0.column = 0;
+ inlay_point = fold_point.to_inlay_point(&self.fold_snapshot);
+ point = self.inlay_snapshot.to_buffer_point(inlay_point);
let mut display_point = self.point_to_display_point(point, Bias::Left);
*display_point.column_mut() = 0;
@@ -332,9 +338,11 @@ impl DisplaySnapshot {
pub fn next_line_boundary(&self, mut point: Point) -> (Point, DisplayPoint) {
loop {
- let mut fold_point = self.fold_snapshot.to_fold_point(point, Bias::Right);
- *fold_point.column_mut() = self.fold_snapshot.line_len(fold_point.row());
- point = fold_point.to_buffer_point(&self.fold_snapshot);
+ let mut inlay_point = self.inlay_snapshot.to_inlay_point(point);
+ let mut fold_point = self.fold_snapshot.to_fold_point(inlay_point, Bias::Right);
+ fold_point.0.column = self.fold_snapshot.line_len(fold_point.row());
+ inlay_point = fold_point.to_inlay_point(&self.fold_snapshot);
+ point = self.inlay_snapshot.to_buffer_point(inlay_point);
let mut display_point = self.point_to_display_point(point, Bias::Right);
*display_point.column_mut() = self.line_len(display_point.row());
@@ -364,9 +372,9 @@ impl DisplaySnapshot {
}
fn point_to_display_point(&self, point: Point, bias: Bias) -> DisplayPoint {
- let fold_point = self.fold_snapshot.to_fold_point(point, bias);
- let suggestion_point = self.suggestion_snapshot.to_suggestion_point(fold_point);
- let tab_point = self.tab_snapshot.to_tab_point(suggestion_point);
+ let inlay_point = self.inlay_snapshot.to_inlay_point(point);
+ let fold_point = self.fold_snapshot.to_fold_point(inlay_point, bias);
+ let tab_point = self.tab_snapshot.to_tab_point(fold_point);
let wrap_point = self.wrap_snapshot.tab_point_to_wrap_point(tab_point);
let block_point = self.block_snapshot.to_block_point(wrap_point);
DisplayPoint(block_point)
@@ -376,9 +384,9 @@ impl DisplaySnapshot {
let block_point = point.0;
let wrap_point = self.block_snapshot.to_wrap_point(block_point);
let tab_point = self.wrap_snapshot.to_tab_point(wrap_point);
- let suggestion_point = self.tab_snapshot.to_suggestion_point(tab_point, bias).0;
- let fold_point = self.suggestion_snapshot.to_fold_point(suggestion_point);
- fold_point.to_buffer_point(&self.fold_snapshot)
+ let fold_point = self.tab_snapshot.to_fold_point(tab_point, bias).0;
+ let inlay_point = fold_point.to_inlay_point(&self.fold_snapshot);
+ self.inlay_snapshot.to_buffer_point(inlay_point)
}
pub fn max_point(&self) -> DisplayPoint {
@@ -388,7 +396,13 @@ impl DisplaySnapshot {
/// Returns text chunks starting at the given display row until the end of the file
pub fn text_chunks(&self, display_row: u32) -> impl Iterator<Item = &str> {
self.block_snapshot
- .chunks(display_row..self.max_point().row() + 1, false, None, None)
+ .chunks(
+ display_row..self.max_point().row() + 1,
+ false,
+ None,
+ None,
+ None,
+ )
.map(|h| h.text)
}
@@ -396,7 +410,7 @@ impl DisplaySnapshot {
pub fn reverse_text_chunks(&self, display_row: u32) -> impl Iterator<Item = &str> {
(0..=display_row).into_iter().rev().flat_map(|row| {
self.block_snapshot
- .chunks(row..row + 1, false, None, None)
+ .chunks(row..row + 1, false, None, None, None)
.map(|h| h.text)
.collect::<Vec<_>>()
.into_iter()
@@ -408,13 +422,15 @@ impl DisplaySnapshot {
&self,
display_rows: Range<u32>,
language_aware: bool,
- suggestion_highlight: Option<HighlightStyle>,
+ hint_highlights: Option<HighlightStyle>,
+ suggestion_highlights: Option<HighlightStyle>,
) -> DisplayChunks<'_> {
self.block_snapshot.chunks(
display_rows,
language_aware,
Some(&self.text_highlights),
- suggestion_highlight,
+ hint_highlights,
+ suggestion_highlights,
)
}
@@ -790,9 +806,10 @@ impl DisplayPoint {
pub fn to_offset(self, map: &DisplaySnapshot, bias: Bias) -> usize {
let wrap_point = map.block_snapshot.to_wrap_point(self.0);
let tab_point = map.wrap_snapshot.to_tab_point(wrap_point);
- let suggestion_point = map.tab_snapshot.to_suggestion_point(tab_point, bias).0;
- let fold_point = map.suggestion_snapshot.to_fold_point(suggestion_point);
- fold_point.to_buffer_offset(&map.fold_snapshot)
+ let fold_point = map.tab_snapshot.to_fold_point(tab_point, bias).0;
+ let inlay_point = fold_point.to_inlay_point(&map.fold_snapshot);
+ map.inlay_snapshot
+ .to_buffer_offset(map.inlay_snapshot.to_offset(inlay_point))
}
}
@@ -1706,7 +1723,7 @@ pub mod tests {
) -> Vec<(String, Option<Color>, Option<Color>)> {
let snapshot = map.update(cx, |map, cx| map.snapshot(cx));
let mut chunks: Vec<(String, Option<Color>, Option<Color>)> = Vec::new();
- for chunk in snapshot.chunks(rows, true, None) {
+ for chunk in snapshot.chunks(rows, true, None, None) {
let syntax_color = chunk
.syntax_highlight_id
.and_then(|id| id.style(theme)?.color);
@@ -573,9 +573,15 @@ impl<'a> BlockMapWriter<'a> {
impl BlockSnapshot {
#[cfg(test)]
pub fn text(&self) -> String {
- self.chunks(0..self.transforms.summary().output_rows, false, None, None)
- .map(|chunk| chunk.text)
- .collect()
+ self.chunks(
+ 0..self.transforms.summary().output_rows,
+ false,
+ None,
+ None,
+ None,
+ )
+ .map(|chunk| chunk.text)
+ .collect()
}
pub fn chunks<'a>(
@@ -583,7 +589,8 @@ impl BlockSnapshot {
rows: Range<u32>,
language_aware: bool,
text_highlights: Option<&'a TextHighlights>,
- suggestion_highlight: Option<HighlightStyle>,
+ hint_highlights: Option<HighlightStyle>,
+ suggestion_highlights: Option<HighlightStyle>,
) -> BlockChunks<'a> {
let max_output_row = cmp::min(rows.end, self.transforms.summary().output_rows);
let mut cursor = self.transforms.cursor::<(BlockRow, WrapRow)>();
@@ -616,7 +623,8 @@ impl BlockSnapshot {
input_start..input_end,
language_aware,
text_highlights,
- suggestion_highlight,
+ hint_highlights,
+ suggestion_highlights,
),
input_chunk: Default::default(),
transforms: cursor,
@@ -989,7 +997,7 @@ fn offset_for_row(s: &str, target: u32) -> (u32, usize) {
#[cfg(test)]
mod tests {
use super::*;
- use crate::display_map::suggestion_map::SuggestionMap;
+ use crate::display_map::inlay_map::InlayMap;
use crate::display_map::{fold_map::FoldMap, tab_map::TabMap, wrap_map::WrapMap};
use crate::multi_buffer::MultiBuffer;
use gpui::{elements::Empty, Element};
@@ -1030,9 +1038,9 @@ mod tests {
let buffer = MultiBuffer::build_simple(text, cx);
let buffer_snapshot = buffer.read(cx).snapshot(cx);
let subscription = buffer.update(cx, |buffer, _| buffer.subscribe());
- let (fold_map, fold_snapshot) = FoldMap::new(buffer_snapshot.clone());
- let (suggestion_map, suggestion_snapshot) = SuggestionMap::new(fold_snapshot);
- let (tab_map, tab_snapshot) = TabMap::new(suggestion_snapshot, 1.try_into().unwrap());
+ let (mut inlay_map, inlay_snapshot) = InlayMap::new(buffer_snapshot.clone());
+ let (mut fold_map, fold_snapshot) = FoldMap::new(inlay_snapshot);
+ let (mut tab_map, tab_snapshot) = TabMap::new(fold_snapshot, 1.try_into().unwrap());
let (wrap_map, wraps_snapshot) = WrapMap::new(tab_snapshot, font_id, 14.0, None, cx);
let mut block_map = BlockMap::new(wraps_snapshot.clone(), 1, 1);
@@ -1175,12 +1183,11 @@ mod tests {
buffer.snapshot(cx)
});
- let (fold_snapshot, fold_edits) =
- fold_map.read(buffer_snapshot, subscription.consume().into_inner());
- let (suggestion_snapshot, suggestion_edits) =
- suggestion_map.sync(fold_snapshot, fold_edits);
+ let (inlay_snapshot, inlay_edits) =
+ inlay_map.sync(buffer_snapshot, subscription.consume().into_inner());
+ let (fold_snapshot, fold_edits) = fold_map.read(inlay_snapshot, inlay_edits);
let (tab_snapshot, tab_edits) =
- tab_map.sync(suggestion_snapshot, suggestion_edits, 4.try_into().unwrap());
+ tab_map.sync(fold_snapshot, fold_edits, 4.try_into().unwrap());
let (wraps_snapshot, wrap_edits) = wrap_map.update(cx, |wrap_map, cx| {
wrap_map.sync(tab_snapshot, tab_edits, cx)
});
@@ -1205,9 +1212,9 @@ mod tests {
let buffer = MultiBuffer::build_simple(text, cx);
let buffer_snapshot = buffer.read(cx).snapshot(cx);
- let (_, fold_snapshot) = FoldMap::new(buffer_snapshot.clone());
- let (_, suggestion_snapshot) = SuggestionMap::new(fold_snapshot);
- let (_, tab_snapshot) = TabMap::new(suggestion_snapshot, 1.try_into().unwrap());
+ let (_, inlay_snapshot) = InlayMap::new(buffer_snapshot.clone());
+ let (_, fold_snapshot) = FoldMap::new(inlay_snapshot);
+ let (_, tab_snapshot) = TabMap::new(fold_snapshot, 4.try_into().unwrap());
let (_, wraps_snapshot) = WrapMap::new(tab_snapshot, font_id, 14.0, Some(60.), cx);
let mut block_map = BlockMap::new(wraps_snapshot.clone(), 1, 1);
@@ -1277,9 +1284,9 @@ mod tests {
};
let mut buffer_snapshot = buffer.read(cx).snapshot(cx);
- let (fold_map, fold_snapshot) = FoldMap::new(buffer_snapshot.clone());
- let (suggestion_map, suggestion_snapshot) = SuggestionMap::new(fold_snapshot);
- let (tab_map, tab_snapshot) = TabMap::new(suggestion_snapshot, tab_size);
+ let (mut inlay_map, inlay_snapshot) = InlayMap::new(buffer_snapshot.clone());
+ let (mut fold_map, fold_snapshot) = FoldMap::new(inlay_snapshot);
+ let (mut tab_map, tab_snapshot) = TabMap::new(fold_snapshot, 4.try_into().unwrap());
let (wrap_map, wraps_snapshot) =
WrapMap::new(tab_snapshot, font_id, font_size, wrap_width, cx);
let mut block_map = BlockMap::new(
@@ -1332,12 +1339,11 @@ mod tests {
})
.collect::<Vec<_>>();
- let (fold_snapshot, fold_edits) =
- fold_map.read(buffer_snapshot.clone(), vec![]);
- let (suggestion_snapshot, suggestion_edits) =
- suggestion_map.sync(fold_snapshot, fold_edits);
+ let (inlay_snapshot, inlay_edits) =
+ inlay_map.sync(buffer_snapshot.clone(), vec![]);
+ let (fold_snapshot, fold_edits) = fold_map.read(inlay_snapshot, inlay_edits);
let (tab_snapshot, tab_edits) =
- tab_map.sync(suggestion_snapshot, suggestion_edits, tab_size);
+ tab_map.sync(fold_snapshot, fold_edits, tab_size);
let (wraps_snapshot, wrap_edits) = wrap_map.update(cx, |wrap_map, cx| {
wrap_map.sync(tab_snapshot, tab_edits, cx)
});
@@ -1357,12 +1363,11 @@ mod tests {
})
.collect();
- let (fold_snapshot, fold_edits) =
- fold_map.read(buffer_snapshot.clone(), vec![]);
- let (suggestion_snapshot, suggestion_edits) =
- suggestion_map.sync(fold_snapshot, fold_edits);
+ let (inlay_snapshot, inlay_edits) =
+ inlay_map.sync(buffer_snapshot.clone(), vec![]);
+ let (fold_snapshot, fold_edits) = fold_map.read(inlay_snapshot, inlay_edits);
let (tab_snapshot, tab_edits) =
- tab_map.sync(suggestion_snapshot, suggestion_edits, tab_size);
+ tab_map.sync(fold_snapshot, fold_edits, tab_size);
let (wraps_snapshot, wrap_edits) = wrap_map.update(cx, |wrap_map, cx| {
wrap_map.sync(tab_snapshot, tab_edits, cx)
});
@@ -1381,11 +1386,10 @@ mod tests {
}
}
- let (fold_snapshot, fold_edits) = fold_map.read(buffer_snapshot.clone(), buffer_edits);
- let (suggestion_snapshot, suggestion_edits) =
- suggestion_map.sync(fold_snapshot, fold_edits);
- let (tab_snapshot, tab_edits) =
- tab_map.sync(suggestion_snapshot, suggestion_edits, tab_size);
+ let (inlay_snapshot, inlay_edits) =
+ inlay_map.sync(buffer_snapshot.clone(), buffer_edits);
+ let (fold_snapshot, fold_edits) = fold_map.read(inlay_snapshot, inlay_edits);
+ let (tab_snapshot, tab_edits) = tab_map.sync(fold_snapshot, fold_edits, tab_size);
let (wraps_snapshot, wrap_edits) = wrap_map.update(cx, |wrap_map, cx| {
wrap_map.sync(tab_snapshot, tab_edits, cx)
});
@@ -1499,6 +1503,7 @@ mod tests {
false,
None,
None,
+ None,
)
.map(|chunk| chunk.text)
.collect::<String>();
@@ -1,19 +1,15 @@
-use super::TextHighlights;
-use crate::{
- multi_buffer::MultiBufferRows, Anchor, AnchorRangeExt, MultiBufferChunks, MultiBufferSnapshot,
- ToOffset,
+use super::{
+ inlay_map::{InlayBufferRows, InlayChunks, InlayEdit, InlayOffset, InlayPoint, InlaySnapshot},
+ TextHighlights,
};
-use collections::BTreeMap;
+use crate::{Anchor, AnchorRangeExt, MultiBufferSnapshot, ToOffset};
use gpui::{color::Color, fonts::HighlightStyle};
use language::{Chunk, Edit, Point, TextSummary};
-use parking_lot::Mutex;
use std::{
any::TypeId,
cmp::{self, Ordering},
- iter::{self, Peekable},
- ops::{Range, Sub},
- sync::atomic::{AtomicUsize, Ordering::SeqCst},
- vec,
+ iter,
+ ops::{Add, AddAssign, Range, Sub},
};
use sum_tree::{Bias, Cursor, FilterCursor, SumTree};
@@ -29,28 +25,24 @@ impl FoldPoint {
self.0.row
}
+ pub fn column(self) -> u32 {
+ self.0.column
+ }
+
pub fn row_mut(&mut self) -> &mut u32 {
&mut self.0.row
}
+ #[cfg(test)]
pub fn column_mut(&mut self) -> &mut u32 {
&mut self.0.column
}
- pub fn to_buffer_point(self, snapshot: &FoldSnapshot) -> Point {
- let mut cursor = snapshot.transforms.cursor::<(FoldPoint, Point)>();
- cursor.seek(&self, Bias::Right, &());
- let overshoot = self.0 - cursor.start().0 .0;
- cursor.start().1 + overshoot
- }
-
- pub fn to_buffer_offset(self, snapshot: &FoldSnapshot) -> usize {
- let mut cursor = snapshot.transforms.cursor::<(FoldPoint, Point)>();
+ pub fn to_inlay_point(self, snapshot: &FoldSnapshot) -> InlayPoint {
+ let mut cursor = snapshot.transforms.cursor::<(FoldPoint, InlayPoint)>();
cursor.seek(&self, Bias::Right, &());
let overshoot = self.0 - cursor.start().0 .0;
- snapshot
- .buffer_snapshot
- .point_to_offset(cursor.start().1 + overshoot)
+ InlayPoint(cursor.start().1 .0 + overshoot)
}
pub fn to_offset(self, snapshot: &FoldSnapshot) -> FoldOffset {
@@ -63,10 +55,10 @@ impl FoldPoint {
if !overshoot.is_zero() {
let transform = cursor.item().expect("display point out of range");
assert!(transform.output_text.is_none());
- let end_buffer_offset = snapshot
- .buffer_snapshot
- .point_to_offset(cursor.start().1.input.lines + overshoot);
- offset += end_buffer_offset - cursor.start().1.input.len;
+ let end_inlay_offset = snapshot
+ .inlay_snapshot
+ .to_offset(InlayPoint(cursor.start().1.input.lines + overshoot));
+ offset += end_inlay_offset.0 - cursor.start().1.input.len;
}
FoldOffset(offset)
}
@@ -87,8 +79,9 @@ impl<'a> FoldMapWriter<'a> {
) -> (FoldSnapshot, Vec<FoldEdit>) {
let mut edits = Vec::new();
let mut folds = Vec::new();
- let buffer = self.0.buffer.lock().clone();
+ let snapshot = self.0.snapshot.inlay_snapshot.clone();
for range in ranges.into_iter() {
+ let buffer = &snapshot.buffer;
let range = range.start.to_offset(&buffer)..range.end.to_offset(&buffer);
// Ignore any empty ranges.
@@ -103,35 +96,32 @@ impl<'a> FoldMapWriter<'a> {
}
folds.push(fold);
- edits.push(text::Edit {
- old: range.clone(),
- new: range,
+
+ let inlay_range =
+ snapshot.to_inlay_offset(range.start)..snapshot.to_inlay_offset(range.end);
+ edits.push(InlayEdit {
+ old: inlay_range.clone(),
+ new: inlay_range,
});
}
- folds.sort_unstable_by(|a, b| sum_tree::SeekTarget::cmp(a, b, &buffer));
+ let buffer = &snapshot.buffer;
+ folds.sort_unstable_by(|a, b| sum_tree::SeekTarget::cmp(a, b, buffer));
- self.0.folds = {
+ self.0.snapshot.folds = {
let mut new_tree = SumTree::new();
- let mut cursor = self.0.folds.cursor::<Fold>();
+ let mut cursor = self.0.snapshot.folds.cursor::<Fold>();
for fold in folds {
- new_tree.append(cursor.slice(&fold, Bias::Right, &buffer), &buffer);
- new_tree.push(fold, &buffer);
+ new_tree.append(cursor.slice(&fold, Bias::Right, buffer), buffer);
+ new_tree.push(fold, buffer);
}
- new_tree.append(cursor.suffix(&buffer), &buffer);
+ new_tree.append(cursor.suffix(buffer), buffer);
new_tree
};
- consolidate_buffer_edits(&mut edits);
- let edits = self.0.sync(buffer.clone(), edits);
- let snapshot = FoldSnapshot {
- transforms: self.0.transforms.lock().clone(),
- folds: self.0.folds.clone(),
- buffer_snapshot: buffer,
- version: self.0.version.load(SeqCst),
- ellipses_color: self.0.ellipses_color,
- };
- (snapshot, edits)
+ consolidate_inlay_edits(&mut edits);
+ let edits = self.0.sync(snapshot.clone(), edits);
+ (self.0.snapshot.clone(), edits)
}
pub fn unfold<T: ToOffset>(
@@ -141,110 +131,93 @@ impl<'a> FoldMapWriter<'a> {
) -> (FoldSnapshot, Vec<FoldEdit>) {
let mut edits = Vec::new();
let mut fold_ixs_to_delete = Vec::new();
- let buffer = self.0.buffer.lock().clone();
+ let snapshot = self.0.snapshot.inlay_snapshot.clone();
+ let buffer = &snapshot.buffer;
for range in ranges.into_iter() {
// Remove intersecting folds and add their ranges to edits that are passed to sync.
- let mut folds_cursor = intersecting_folds(&buffer, &self.0.folds, range, inclusive);
+ let mut folds_cursor =
+ intersecting_folds(&snapshot, &self.0.snapshot.folds, range, inclusive);
while let Some(fold) = folds_cursor.item() {
- let offset_range = fold.0.start.to_offset(&buffer)..fold.0.end.to_offset(&buffer);
+ let offset_range = fold.0.start.to_offset(buffer)..fold.0.end.to_offset(buffer);
if offset_range.end > offset_range.start {
- edits.push(text::Edit {
- old: offset_range.clone(),
- new: offset_range,
+ let inlay_range = snapshot.to_inlay_offset(offset_range.start)
+ ..snapshot.to_inlay_offset(offset_range.end);
+ edits.push(InlayEdit {
+ old: inlay_range.clone(),
+ new: inlay_range,
});
}
fold_ixs_to_delete.push(*folds_cursor.start());
- folds_cursor.next(&buffer);
+ folds_cursor.next(buffer);
}
}
fold_ixs_to_delete.sort_unstable();
fold_ixs_to_delete.dedup();
- self.0.folds = {
- let mut cursor = self.0.folds.cursor::<usize>();
+ self.0.snapshot.folds = {
+ let mut cursor = self.0.snapshot.folds.cursor::<usize>();
let mut folds = SumTree::new();
for fold_ix in fold_ixs_to_delete {
- folds.append(cursor.slice(&fold_ix, Bias::Right, &buffer), &buffer);
- cursor.next(&buffer);
+ folds.append(cursor.slice(&fold_ix, Bias::Right, buffer), buffer);
+ cursor.next(buffer);
}
- folds.append(cursor.suffix(&buffer), &buffer);
+ folds.append(cursor.suffix(buffer), buffer);
folds
};
- consolidate_buffer_edits(&mut edits);
- let edits = self.0.sync(buffer.clone(), edits);
- let snapshot = FoldSnapshot {
- transforms: self.0.transforms.lock().clone(),
- folds: self.0.folds.clone(),
- buffer_snapshot: buffer,
- version: self.0.version.load(SeqCst),
- ellipses_color: self.0.ellipses_color,
- };
- (snapshot, edits)
+ consolidate_inlay_edits(&mut edits);
+ let edits = self.0.sync(snapshot.clone(), edits);
+ (self.0.snapshot.clone(), edits)
}
}
pub struct FoldMap {
- buffer: Mutex<MultiBufferSnapshot>,
- transforms: Mutex<SumTree<Transform>>,
- folds: SumTree<Fold>,
- version: AtomicUsize,
+ snapshot: FoldSnapshot,
ellipses_color: Option<Color>,
}
impl FoldMap {
- pub fn new(buffer: MultiBufferSnapshot) -> (Self, FoldSnapshot) {
+ pub fn new(inlay_snapshot: InlaySnapshot) -> (Self, FoldSnapshot) {
let this = Self {
- buffer: Mutex::new(buffer.clone()),
- folds: Default::default(),
- transforms: Mutex::new(SumTree::from_item(
- Transform {
- summary: TransformSummary {
- input: buffer.text_summary(),
- output: buffer.text_summary(),
+ snapshot: FoldSnapshot {
+ folds: Default::default(),
+ transforms: SumTree::from_item(
+ Transform {
+ summary: TransformSummary {
+ input: inlay_snapshot.text_summary(),
+ output: inlay_snapshot.text_summary(),
+ },
+ output_text: None,
},
- output_text: None,
- },
- &(),
- )),
- ellipses_color: None,
- version: Default::default(),
- };
-
- let snapshot = FoldSnapshot {
- transforms: this.transforms.lock().clone(),
- folds: this.folds.clone(),
- buffer_snapshot: this.buffer.lock().clone(),
- version: this.version.load(SeqCst),
+ &(),
+ ),
+ inlay_snapshot: inlay_snapshot.clone(),
+ version: 0,
+ ellipses_color: None,
+ },
ellipses_color: None,
};
+ let snapshot = this.snapshot.clone();
(this, snapshot)
}
pub fn read(
- &self,
- buffer: MultiBufferSnapshot,
- edits: Vec<Edit<usize>>,
+ &mut self,
+ inlay_snapshot: InlaySnapshot,
+ edits: Vec<InlayEdit>,
) -> (FoldSnapshot, Vec<FoldEdit>) {
- let edits = self.sync(buffer, edits);
+ let edits = self.sync(inlay_snapshot, edits);
self.check_invariants();
- let snapshot = FoldSnapshot {
- transforms: self.transforms.lock().clone(),
- folds: self.folds.clone(),
- buffer_snapshot: self.buffer.lock().clone(),
- version: self.version.load(SeqCst),
- ellipses_color: self.ellipses_color,
- };
- (snapshot, edits)
+ (self.snapshot.clone(), edits)
}
pub fn write(
&mut self,
- buffer: MultiBufferSnapshot,
- edits: Vec<Edit<usize>>,
+ inlay_snapshot: InlaySnapshot,
+ edits: Vec<InlayEdit>,
) -> (FoldMapWriter, FoldSnapshot, Vec<FoldEdit>) {
- let (snapshot, edits) = self.read(buffer, edits);
+ let (snapshot, edits) = self.read(inlay_snapshot, edits);
(FoldMapWriter(self), snapshot, edits)
}
@@ -260,15 +233,17 @@ impl FoldMap {
fn check_invariants(&self) {
if cfg!(test) {
assert_eq!(
- self.transforms.lock().summary().input.len,
- self.buffer.lock().len(),
- "transform tree does not match buffer's length"
+ self.snapshot.transforms.summary().input.len,
+ self.snapshot.inlay_snapshot.len().0,
+ "transform tree does not match inlay snapshot's length"
);
- let mut folds = self.folds.iter().peekable();
+ let mut folds = self.snapshot.folds.iter().peekable();
while let Some(fold) = folds.next() {
if let Some(next_fold) = folds.peek() {
- let comparison = fold.0.cmp(&next_fold.0, &self.buffer.lock());
+ let comparison = fold
+ .0
+ .cmp(&next_fold.0, &self.snapshot.inlay_snapshot.buffer);
assert!(comparison.is_le());
}
}
@@ -276,50 +251,42 @@ impl FoldMap {
}
fn sync(
- &self,
- new_buffer: MultiBufferSnapshot,
- buffer_edits: Vec<text::Edit<usize>>,
+ &mut self,
+ inlay_snapshot: InlaySnapshot,
+ inlay_edits: Vec<InlayEdit>,
) -> Vec<FoldEdit> {
- if buffer_edits.is_empty() {
- let mut buffer = self.buffer.lock();
- if buffer.edit_count() != new_buffer.edit_count()
- || buffer.parse_count() != new_buffer.parse_count()
- || buffer.diagnostics_update_count() != new_buffer.diagnostics_update_count()
- || buffer.git_diff_update_count() != new_buffer.git_diff_update_count()
- || buffer.trailing_excerpt_update_count()
- != new_buffer.trailing_excerpt_update_count()
- {
- self.version.fetch_add(1, SeqCst);
+ if inlay_edits.is_empty() {
+ if self.snapshot.inlay_snapshot.version != inlay_snapshot.version {
+ self.snapshot.version += 1;
}
- *buffer = new_buffer;
+ self.snapshot.inlay_snapshot = inlay_snapshot;
Vec::new()
} else {
- let mut buffer_edits_iter = buffer_edits.iter().cloned().peekable();
+ let mut inlay_edits_iter = inlay_edits.iter().cloned().peekable();
let mut new_transforms = SumTree::new();
- let mut transforms = self.transforms.lock();
- let mut cursor = transforms.cursor::<usize>();
- cursor.seek(&0, Bias::Right, &());
+ let mut cursor = self.snapshot.transforms.cursor::<InlayOffset>();
+ cursor.seek(&InlayOffset(0), Bias::Right, &());
- while let Some(mut edit) = buffer_edits_iter.next() {
+ while let Some(mut edit) = inlay_edits_iter.next() {
new_transforms.append(cursor.slice(&edit.old.start, Bias::Left, &()), &());
- edit.new.start -= edit.old.start - cursor.start();
+ edit.new.start -= edit.old.start - *cursor.start();
edit.old.start = *cursor.start();
cursor.seek(&edit.old.end, Bias::Right, &());
cursor.next(&());
- let mut delta = edit.new.len() as isize - edit.old.len() as isize;
+ let mut delta = edit.new_len().0 as isize - edit.old_len().0 as isize;
loop {
edit.old.end = *cursor.start();
- if let Some(next_edit) = buffer_edits_iter.peek() {
+ if let Some(next_edit) = inlay_edits_iter.peek() {
if next_edit.old.start > edit.old.end {
break;
}
- let next_edit = buffer_edits_iter.next().unwrap();
- delta += next_edit.new.len() as isize - next_edit.old.len() as isize;
+ let next_edit = inlay_edits_iter.next().unwrap();
+ delta += next_edit.new_len().0 as isize - next_edit.old_len().0 as isize;
if next_edit.old.end >= edit.old.end {
edit.old.end = next_edit.old.end;
@@ -331,19 +298,29 @@ impl FoldMap {
}
}
- edit.new.end = ((edit.new.start + edit.old.len()) as isize + delta) as usize;
-
- let anchor = new_buffer.anchor_before(edit.new.start);
- let mut folds_cursor = self.folds.cursor::<Fold>();
- folds_cursor.seek(&Fold(anchor..Anchor::max()), Bias::Left, &new_buffer);
+ edit.new.end =
+ InlayOffset(((edit.new.start + edit.old_len()).0 as isize + delta) as usize);
+
+ let anchor = inlay_snapshot
+ .buffer
+ .anchor_before(inlay_snapshot.to_buffer_offset(edit.new.start));
+ let mut folds_cursor = self.snapshot.folds.cursor::<Fold>();
+ folds_cursor.seek(
+ &Fold(anchor..Anchor::max()),
+ Bias::Left,
+ &inlay_snapshot.buffer,
+ );
let mut folds = iter::from_fn({
- let buffer = &new_buffer;
+ let inlay_snapshot = &inlay_snapshot;
move || {
- let item = folds_cursor
- .item()
- .map(|f| f.0.start.to_offset(buffer)..f.0.end.to_offset(buffer));
- folds_cursor.next(buffer);
+ let item = folds_cursor.item().map(|f| {
+ let buffer_start = f.0.start.to_offset(&inlay_snapshot.buffer);
+ let buffer_end = f.0.end.to_offset(&inlay_snapshot.buffer);
+ inlay_snapshot.to_inlay_offset(buffer_start)
+ ..inlay_snapshot.to_inlay_offset(buffer_end)
+ });
+ folds_cursor.next(&inlay_snapshot.buffer);
item
}
})
@@ -353,7 +330,7 @@ impl FoldMap {
let mut fold = folds.next().unwrap();
let sum = new_transforms.summary();
- assert!(fold.start >= sum.input.len);
+ assert!(fold.start.0 >= sum.input.len);
while folds
.peek()
@@ -365,9 +342,9 @@ impl FoldMap {
}
}
- if fold.start > sum.input.len {
- let text_summary = new_buffer
- .text_summary_for_range::<TextSummary, _>(sum.input.len..fold.start);
+ if fold.start.0 > sum.input.len {
+ let text_summary = inlay_snapshot
+ .text_summary_for_range(InlayOffset(sum.input.len)..fold.start);
new_transforms.push(
Transform {
summary: TransformSummary {
@@ -386,7 +363,8 @@ impl FoldMap {
Transform {
summary: TransformSummary {
output: TextSummary::from(output_text),
- input: new_buffer.text_summary_for_range(fold.start..fold.end),
+ input: inlay_snapshot
+ .text_summary_for_range(fold.start..fold.end),
},
output_text: Some(output_text),
},
@@ -396,9 +374,9 @@ impl FoldMap {
}
let sum = new_transforms.summary();
- if sum.input.len < edit.new.end {
- let text_summary = new_buffer
- .text_summary_for_range::<TextSummary, _>(sum.input.len..edit.new.end);
+ if sum.input.len < edit.new.end.0 {
+ let text_summary = inlay_snapshot
+ .text_summary_for_range(InlayOffset(sum.input.len)..edit.new.end);
new_transforms.push(
Transform {
summary: TransformSummary {
@@ -414,7 +392,7 @@ impl FoldMap {
new_transforms.append(cursor.suffix(&()), &());
if new_transforms.is_empty() {
- let text_summary = new_buffer.text_summary();
+ let text_summary = inlay_snapshot.text_summary();
new_transforms.push(
Transform {
summary: TransformSummary {
@@ -429,18 +407,21 @@ impl FoldMap {
drop(cursor);
- let mut fold_edits = Vec::with_capacity(buffer_edits.len());
+ let mut fold_edits = Vec::with_capacity(inlay_edits.len());
{
- let mut old_transforms = transforms.cursor::<(usize, FoldOffset)>();
- let mut new_transforms = new_transforms.cursor::<(usize, FoldOffset)>();
+ let mut old_transforms = self
+ .snapshot
+ .transforms
+ .cursor::<(InlayOffset, FoldOffset)>();
+ let mut new_transforms = new_transforms.cursor::<(InlayOffset, FoldOffset)>();
- for mut edit in buffer_edits {
+ for mut edit in inlay_edits {
old_transforms.seek(&edit.old.start, Bias::Left, &());
if old_transforms.item().map_or(false, |t| t.is_fold()) {
edit.old.start = old_transforms.start().0;
}
let old_start =
- old_transforms.start().1 .0 + (edit.old.start - old_transforms.start().0);
+ old_transforms.start().1 .0 + (edit.old.start - old_transforms.start().0).0;
old_transforms.seek_forward(&edit.old.end, Bias::Right, &());
if old_transforms.item().map_or(false, |t| t.is_fold()) {
@@ -448,14 +429,14 @@ impl FoldMap {
edit.old.end = old_transforms.start().0;
}
let old_end =
- old_transforms.start().1 .0 + (edit.old.end - old_transforms.start().0);
+ old_transforms.start().1 .0 + (edit.old.end - old_transforms.start().0).0;
new_transforms.seek(&edit.new.start, Bias::Left, &());
if new_transforms.item().map_or(false, |t| t.is_fold()) {
edit.new.start = new_transforms.start().0;
}
let new_start =
- new_transforms.start().1 .0 + (edit.new.start - new_transforms.start().0);
+ new_transforms.start().1 .0 + (edit.new.start - new_transforms.start().0).0;
new_transforms.seek_forward(&edit.new.end, Bias::Right, &());
if new_transforms.item().map_or(false, |t| t.is_fold()) {
@@ -463,7 +444,7 @@ impl FoldMap {
edit.new.end = new_transforms.start().0;
}
let new_end =
- new_transforms.start().1 .0 + (edit.new.end - new_transforms.start().0);
+ new_transforms.start().1 .0 + (edit.new.end - new_transforms.start().0).0;
fold_edits.push(FoldEdit {
old: FoldOffset(old_start)..FoldOffset(old_end),
@@ -474,9 +455,9 @@ impl FoldMap {
consolidate_fold_edits(&mut fold_edits);
}
- *transforms = new_transforms;
- *self.buffer.lock() = new_buffer;
- self.version.fetch_add(1, SeqCst);
+ self.snapshot.transforms = new_transforms;
+ self.snapshot.inlay_snapshot = inlay_snapshot;
+ self.snapshot.version += 1;
fold_edits
}
}
@@ -486,32 +467,28 @@ impl FoldMap {
pub struct FoldSnapshot {
transforms: SumTree<Transform>,
folds: SumTree<Fold>,
- buffer_snapshot: MultiBufferSnapshot,
+ pub inlay_snapshot: InlaySnapshot,
pub version: usize,
pub ellipses_color: Option<Color>,
}
impl FoldSnapshot {
- pub fn buffer_snapshot(&self) -> &MultiBufferSnapshot {
- &self.buffer_snapshot
- }
-
#[cfg(test)]
pub fn text(&self) -> String {
- self.chunks(FoldOffset(0)..self.len(), false, None)
+ self.chunks(FoldOffset(0)..self.len(), false, None, None, None)
.map(|c| c.text)
.collect()
}
#[cfg(test)]
pub fn fold_count(&self) -> usize {
- self.folds.items(&self.buffer_snapshot).len()
+ self.folds.items(&self.inlay_snapshot.buffer).len()
}
pub fn text_summary_for_range(&self, range: Range<FoldPoint>) -> TextSummary {
let mut summary = TextSummary::default();
- let mut cursor = self.transforms.cursor::<(FoldPoint, Point)>();
+ let mut cursor = self.transforms.cursor::<(FoldPoint, InlayPoint)>();
cursor.seek(&range.start, Bias::Right, &());
if let Some(transform) = cursor.item() {
let start_in_transform = range.start.0 - cursor.start().0 .0;
@@ -522,11 +499,15 @@ impl FoldSnapshot {
[start_in_transform.column as usize..end_in_transform.column as usize],
);
} else {
- let buffer_start = cursor.start().1 + start_in_transform;
- let buffer_end = cursor.start().1 + end_in_transform;
+ let inlay_start = self
+ .inlay_snapshot
+ .to_offset(InlayPoint(cursor.start().1 .0 + start_in_transform));
+ let inlay_end = self
+ .inlay_snapshot
+ .to_offset(InlayPoint(cursor.start().1 .0 + end_in_transform));
summary = self
- .buffer_snapshot
- .text_summary_for_range(buffer_start..buffer_end);
+ .inlay_snapshot
+ .text_summary_for_range(inlay_start..inlay_end);
}
}
@@ -540,11 +521,13 @@ impl FoldSnapshot {
if let Some(output_text) = transform.output_text {
summary += TextSummary::from(&output_text[..end_in_transform.column as usize]);
} else {
- let buffer_start = cursor.start().1;
- let buffer_end = cursor.start().1 + end_in_transform;
+ let inlay_start = self.inlay_snapshot.to_offset(cursor.start().1);
+ let inlay_end = self
+ .inlay_snapshot
+ .to_offset(InlayPoint(cursor.start().1 .0 + end_in_transform));
summary += self
- .buffer_snapshot
- .text_summary_for_range::<TextSummary, _>(buffer_start..buffer_end);
+ .inlay_snapshot
+ .text_summary_for_range(inlay_start..inlay_end);
}
}
}
@@ -552,8 +535,8 @@ impl FoldSnapshot {
summary
}
- pub fn to_fold_point(&self, point: Point, bias: Bias) -> FoldPoint {
- let mut cursor = self.transforms.cursor::<(Point, FoldPoint)>();
+ pub fn to_fold_point(&self, point: InlayPoint, bias: Bias) -> FoldPoint {
+ let mut cursor = self.transforms.cursor::<(InlayPoint, FoldPoint)>();
cursor.seek(&point, Bias::Right, &());
if cursor.item().map_or(false, |t| t.is_fold()) {
if bias == Bias::Left || point == cursor.start().0 {
@@ -562,7 +545,7 @@ impl FoldSnapshot {
cursor.end(&()).1
}
} else {
- let overshoot = point - cursor.start().0;
+ let overshoot = point.0 - cursor.start().0 .0;
FoldPoint(cmp::min(
cursor.start().1 .0 + overshoot,
cursor.end(&()).1 .0,
@@ -590,12 +573,12 @@ impl FoldSnapshot {
}
let fold_point = FoldPoint::new(start_row, 0);
- let mut cursor = self.transforms.cursor::<(FoldPoint, Point)>();
+ let mut cursor = self.transforms.cursor::<(FoldPoint, InlayPoint)>();
cursor.seek(&fold_point, Bias::Left, &());
let overshoot = fold_point.0 - cursor.start().0 .0;
- let buffer_point = cursor.start().1 + overshoot;
- let input_buffer_rows = self.buffer_snapshot.buffer_rows(buffer_point.row);
+ let inlay_point = InlayPoint(cursor.start().1 .0 + overshoot);
+ let input_buffer_rows = self.inlay_snapshot.buffer_rows(inlay_point.row());
FoldBufferRows {
fold_point,
@@ -617,10 +600,10 @@ impl FoldSnapshot {
where
T: ToOffset,
{
- let mut folds = intersecting_folds(&self.buffer_snapshot, &self.folds, range, false);
+ let mut folds = intersecting_folds(&self.inlay_snapshot, &self.folds, range, false);
iter::from_fn(move || {
let item = folds.item().map(|f| &f.0);
- folds.next(&self.buffer_snapshot);
+ folds.next(&self.inlay_snapshot.buffer);
item
})
}
@@ -629,26 +612,39 @@ impl FoldSnapshot {
where
T: ToOffset,
{
- let offset = offset.to_offset(&self.buffer_snapshot);
- let mut cursor = self.transforms.cursor::<usize>();
- cursor.seek(&offset, Bias::Right, &());
+ let buffer_offset = offset.to_offset(&self.inlay_snapshot.buffer);
+ let inlay_offset = self.inlay_snapshot.to_inlay_offset(buffer_offset);
+ let mut cursor = self.transforms.cursor::<InlayOffset>();
+ cursor.seek(&inlay_offset, Bias::Right, &());
cursor.item().map_or(false, |t| t.output_text.is_some())
}
pub fn is_line_folded(&self, buffer_row: u32) -> bool {
- let mut cursor = self.transforms.cursor::<Point>();
- cursor.seek(&Point::new(buffer_row, 0), Bias::Right, &());
- while let Some(transform) = cursor.item() {
- if transform.output_text.is_some() {
- return true;
+ let mut inlay_point = self
+ .inlay_snapshot
+ .to_inlay_point(Point::new(buffer_row, 0));
+ let mut cursor = self.transforms.cursor::<InlayPoint>();
+ cursor.seek(&inlay_point, Bias::Right, &());
+ loop {
+ match cursor.item() {
+ Some(transform) => {
+ let buffer_point = self.inlay_snapshot.to_buffer_point(inlay_point);
+ if buffer_point.row != buffer_row {
+ return false;
+ } else if transform.output_text.is_some() {
+ return true;
+ }
+ }
+ None => return false,
}
- if cursor.end(&()).row == buffer_row {
- cursor.next(&())
+
+ if cursor.end(&()).row() == inlay_point.row() {
+ cursor.next(&());
} else {
- break;
+ inlay_point.0 += Point::new(1, 0);
+ cursor.seek(&inlay_point, Bias::Right, &());
}
}
- false
}
pub fn chunks<'a>(
@@ -656,127 +652,56 @@ impl FoldSnapshot {
range: Range<FoldOffset>,
language_aware: bool,
text_highlights: Option<&'a TextHighlights>,
+ hint_highlights: Option<HighlightStyle>,
+ suggestion_highlights: Option<HighlightStyle>,
) -> FoldChunks<'a> {
- let mut highlight_endpoints = Vec::new();
- let mut transform_cursor = self.transforms.cursor::<(FoldOffset, usize)>();
+ let mut transform_cursor = self.transforms.cursor::<(FoldOffset, InlayOffset)>();
- let buffer_end = {
+ let inlay_end = {
transform_cursor.seek(&range.end, Bias::Right, &());
let overshoot = range.end.0 - transform_cursor.start().0 .0;
- transform_cursor.start().1 + overshoot
+ transform_cursor.start().1 + InlayOffset(overshoot)
};
- let buffer_start = {
+ let inlay_start = {
transform_cursor.seek(&range.start, Bias::Right, &());
let overshoot = range.start.0 - transform_cursor.start().0 .0;
- transform_cursor.start().1 + overshoot
+ transform_cursor.start().1 + InlayOffset(overshoot)
};
- if let Some(text_highlights) = text_highlights {
- if !text_highlights.is_empty() {
- while transform_cursor.start().0 < range.end {
- if !transform_cursor.item().unwrap().is_fold() {
- let transform_start = self
- .buffer_snapshot
- .anchor_after(cmp::max(buffer_start, transform_cursor.start().1));
-
- let transform_end = {
- let overshoot = range.end.0 - transform_cursor.start().0 .0;
- self.buffer_snapshot.anchor_before(cmp::min(
- transform_cursor.end(&()).1,
- transform_cursor.start().1 + overshoot,
- ))
- };
-
- for (tag, highlights) in text_highlights.iter() {
- let style = highlights.0;
- let ranges = &highlights.1;
-
- let start_ix = match ranges.binary_search_by(|probe| {
- let cmp = probe.end.cmp(&transform_start, self.buffer_snapshot());
- if cmp.is_gt() {
- Ordering::Greater
- } else {
- Ordering::Less
- }
- }) {
- Ok(i) | Err(i) => i,
- };
- for range in &ranges[start_ix..] {
- if range
- .start
- .cmp(&transform_end, &self.buffer_snapshot)
- .is_ge()
- {
- break;
- }
-
- highlight_endpoints.push(HighlightEndpoint {
- offset: range.start.to_offset(&self.buffer_snapshot),
- is_start: true,
- tag: *tag,
- style,
- });
- highlight_endpoints.push(HighlightEndpoint {
- offset: range.end.to_offset(&self.buffer_snapshot),
- is_start: false,
- tag: *tag,
- style,
- });
- }
- }
- }
-
- transform_cursor.next(&());
- }
- highlight_endpoints.sort();
- transform_cursor.seek(&range.start, Bias::Right, &());
- }
- }
-
FoldChunks {
transform_cursor,
- buffer_chunks: self
- .buffer_snapshot
- .chunks(buffer_start..buffer_end, language_aware),
- buffer_chunk: None,
- buffer_offset: buffer_start,
+ inlay_chunks: self.inlay_snapshot.chunks(
+ inlay_start..inlay_end,
+ language_aware,
+ text_highlights,
+ hint_highlights,
+ suggestion_highlights,
+ ),
+ inlay_chunk: None,
+ inlay_offset: inlay_start,
output_offset: range.start.0,
max_output_offset: range.end.0,
- highlight_endpoints: highlight_endpoints.into_iter().peekable(),
- active_highlights: Default::default(),
ellipses_color: self.ellipses_color,
}
}
+ pub fn chars_at(&self, start: FoldPoint) -> impl '_ + Iterator<Item = char> {
+ self.chunks(start.to_offset(self)..self.len(), false, None, None, None)
+ .flat_map(|chunk| chunk.text.chars())
+ }
+
#[cfg(test)]
pub fn clip_offset(&self, offset: FoldOffset, bias: Bias) -> FoldOffset {
- let mut cursor = self.transforms.cursor::<(FoldOffset, usize)>();
- cursor.seek(&offset, Bias::Right, &());
- if let Some(transform) = cursor.item() {
- let transform_start = cursor.start().0 .0;
- if transform.output_text.is_some() {
- if offset.0 == transform_start || matches!(bias, Bias::Left) {
- FoldOffset(transform_start)
- } else {
- FoldOffset(cursor.end(&()).0 .0)
- }
- } else {
- let overshoot = offset.0 - transform_start;
- let buffer_offset = cursor.start().1 + overshoot;
- let clipped_buffer_offset = self.buffer_snapshot.clip_offset(buffer_offset, bias);
- FoldOffset(
- (offset.0 as isize + (clipped_buffer_offset as isize - buffer_offset as isize))
- as usize,
- )
- }
+ if offset > self.len() {
+ self.len()
} else {
- FoldOffset(self.transforms.summary().output.len)
+ self.clip_point(offset.to_point(self), bias).to_offset(self)
}
}
pub fn clip_point(&self, point: FoldPoint, bias: Bias) -> FoldPoint {
- let mut cursor = self.transforms.cursor::<(FoldPoint, Point)>();
+ let mut cursor = self.transforms.cursor::<(FoldPoint, InlayPoint)>();
cursor.seek(&point, Bias::Right, &());
if let Some(transform) = cursor.item() {
let transform_start = cursor.start().0 .0;
@@ -787,11 +712,10 @@ impl FoldSnapshot {
FoldPoint(cursor.end(&()).0 .0)
}
} else {
- let overshoot = point.0 - transform_start;
- let buffer_position = cursor.start().1 + overshoot;
- let clipped_buffer_position =
- self.buffer_snapshot.clip_point(buffer_position, bias);
- FoldPoint(cursor.start().0 .0 + (clipped_buffer_position - cursor.start().1))
+ let overshoot = InlayPoint(point.0 - transform_start);
+ let inlay_point = cursor.start().1 + overshoot;
+ let clipped_inlay_point = self.inlay_snapshot.clip_point(inlay_point, bias);
+ FoldPoint(cursor.start().0 .0 + (clipped_inlay_point - cursor.start().1).0)
}
} else {
FoldPoint(self.transforms.summary().output.lines)
@@ -800,7 +724,7 @@ impl FoldSnapshot {
}
fn intersecting_folds<'a, T>(
- buffer: &'a MultiBufferSnapshot,
+ inlay_snapshot: &'a InlaySnapshot,
folds: &'a SumTree<Fold>,
range: Range<T>,
inclusive: bool,
@@ -808,6 +732,7 @@ fn intersecting_folds<'a, T>(
where
T: ToOffset,
{
+ let buffer = &inlay_snapshot.buffer;
let start = buffer.anchor_before(range.start.to_offset(buffer));
let end = buffer.anchor_after(range.end.to_offset(buffer));
let mut cursor = folds.filter::<_, usize>(move |summary| {
@@ -824,7 +749,7 @@ where
cursor
}
-fn consolidate_buffer_edits(edits: &mut Vec<text::Edit<usize>>) {
+fn consolidate_inlay_edits(edits: &mut Vec<InlayEdit>) {
edits.sort_unstable_by(|a, b| {
a.old
.start
@@ -952,7 +877,7 @@ impl Default for FoldSummary {
impl sum_tree::Summary for FoldSummary {
type Context = MultiBufferSnapshot;
- fn add_summary(&mut self, other: &Self, buffer: &MultiBufferSnapshot) {
+ fn add_summary(&mut self, other: &Self, buffer: &Self::Context) {
if other.min_start.cmp(&self.min_start, buffer) == Ordering::Less {
self.min_start = other.min_start.clone();
}
@@ -996,8 +921,8 @@ impl<'a> sum_tree::Dimension<'a, FoldSummary> for usize {
#[derive(Clone)]
pub struct FoldBufferRows<'a> {
- cursor: Cursor<'a, Transform, (FoldPoint, Point)>,
- input_buffer_rows: MultiBufferRows<'a>,
+ cursor: Cursor<'a, Transform, (FoldPoint, InlayPoint)>,
+ input_buffer_rows: InlayBufferRows<'a>,
fold_point: FoldPoint,
}
@@ -1016,7 +941,7 @@ impl<'a> Iterator for FoldBufferRows<'a> {
if self.cursor.item().is_some() {
if traversed_fold {
- self.input_buffer_rows.seek(self.cursor.start().1.row);
+ self.input_buffer_rows.seek(self.cursor.start().1.row());
self.input_buffer_rows.next();
}
*self.fold_point.row_mut() += 1;
@@ -1028,14 +953,12 @@ impl<'a> Iterator for FoldBufferRows<'a> {
}
pub struct FoldChunks<'a> {
- transform_cursor: Cursor<'a, Transform, (FoldOffset, usize)>,
- buffer_chunks: MultiBufferChunks<'a>,
- buffer_chunk: Option<(usize, Chunk<'a>)>,
- buffer_offset: usize,
+ transform_cursor: Cursor<'a, Transform, (FoldOffset, InlayOffset)>,
+ inlay_chunks: InlayChunks<'a>,
+ inlay_chunk: Option<(InlayOffset, Chunk<'a>)>,
+ inlay_offset: InlayOffset,
output_offset: usize,
max_output_offset: usize,
- highlight_endpoints: Peekable<vec::IntoIter<HighlightEndpoint>>,
- active_highlights: BTreeMap<Option<TypeId>, HighlightStyle>,
ellipses_color: Option<Color>,
}
@@ -1052,11 +975,11 @@ impl<'a> Iterator for FoldChunks<'a> {
// If we're in a fold, then return the fold's display text and
// advance the transform and buffer cursors to the end of the fold.
if let Some(output_text) = transform.output_text {
- self.buffer_chunk.take();
- self.buffer_offset += transform.summary.input.len;
- self.buffer_chunks.seek(self.buffer_offset);
+ self.inlay_chunk.take();
+ self.inlay_offset += InlayOffset(transform.summary.input.len);
+ self.inlay_chunks.seek(self.inlay_offset);
- while self.buffer_offset >= self.transform_cursor.end(&()).1
+ while self.inlay_offset >= self.transform_cursor.end(&()).1
&& self.transform_cursor.item().is_some()
{
self.transform_cursor.next(&());
@@ -1073,53 +996,28 @@ impl<'a> Iterator for FoldChunks<'a> {
});
}
- let mut next_highlight_endpoint = usize::MAX;
- while let Some(endpoint) = self.highlight_endpoints.peek().copied() {
- if endpoint.offset <= self.buffer_offset {
- if endpoint.is_start {
- self.active_highlights.insert(endpoint.tag, endpoint.style);
- } else {
- self.active_highlights.remove(&endpoint.tag);
- }
- self.highlight_endpoints.next();
- } else {
- next_highlight_endpoint = endpoint.offset;
- break;
- }
- }
-
// Retrieve a chunk from the current location in the buffer.
- if self.buffer_chunk.is_none() {
- let chunk_offset = self.buffer_chunks.offset();
- self.buffer_chunk = self.buffer_chunks.next().map(|chunk| (chunk_offset, chunk));
+ if self.inlay_chunk.is_none() {
+ let chunk_offset = self.inlay_chunks.offset();
+ self.inlay_chunk = self.inlay_chunks.next().map(|chunk| (chunk_offset, chunk));
}
// Otherwise, take a chunk from the buffer's text.
- if let Some((buffer_chunk_start, mut chunk)) = self.buffer_chunk {
- let buffer_chunk_end = buffer_chunk_start + chunk.text.len();
+ if let Some((buffer_chunk_start, mut chunk)) = self.inlay_chunk {
+ let buffer_chunk_end = buffer_chunk_start + InlayOffset(chunk.text.len());
let transform_end = self.transform_cursor.end(&()).1;
- let chunk_end = buffer_chunk_end
- .min(transform_end)
- .min(next_highlight_endpoint);
+ let chunk_end = buffer_chunk_end.min(transform_end);
chunk.text = &chunk.text
- [self.buffer_offset - buffer_chunk_start..chunk_end - buffer_chunk_start];
-
- if !self.active_highlights.is_empty() {
- let mut highlight_style = HighlightStyle::default();
- for active_highlight in self.active_highlights.values() {
- highlight_style.highlight(*active_highlight);
- }
- chunk.highlight_style = Some(highlight_style);
- }
+ [(self.inlay_offset - buffer_chunk_start).0..(chunk_end - buffer_chunk_start).0];
if chunk_end == transform_end {
self.transform_cursor.next(&());
} else if chunk_end == buffer_chunk_end {
- self.buffer_chunk.take();
+ self.inlay_chunk.take();
}
- self.buffer_offset = chunk_end;
+ self.inlay_offset = chunk_end;
self.output_offset += chunk.text.len();
return Some(chunk);
}
@@ -0,0 +1,1697 @@
+use crate::{
+ multi_buffer::{MultiBufferChunks, MultiBufferRows},
+ Anchor, InlayId, MultiBufferSnapshot, ToOffset,
+};
+use collections::{BTreeMap, BTreeSet, HashMap};
+use gpui::fonts::HighlightStyle;
+use language::{Chunk, Edit, Point, Rope, TextSummary};
+use std::{
+ any::TypeId,
+ cmp,
+ iter::Peekable,
+ ops::{Add, AddAssign, Range, Sub, SubAssign},
+ vec,
+};
+use sum_tree::{Bias, Cursor, SumTree};
+use text::Patch;
+
+use super::TextHighlights;
+
+pub struct InlayMap {
+ snapshot: InlaySnapshot,
+ inlays_by_id: HashMap<InlayId, Inlay>,
+ inlays: Vec<Inlay>,
+}
+
+#[derive(Clone)]
+pub struct InlaySnapshot {
+ pub buffer: MultiBufferSnapshot,
+ transforms: SumTree<Transform>,
+ pub version: usize,
+}
+
+#[derive(Clone, Debug)]
+enum Transform {
+ Isomorphic(TextSummary),
+ Inlay(Inlay),
+}
+
+#[derive(Debug, Clone)]
+pub struct Inlay {
+ pub id: InlayId,
+ pub position: Anchor,
+ pub text: text::Rope,
+}
+
+#[derive(Debug, Clone)]
+pub struct InlayProperties<T> {
+ pub position: Anchor,
+ pub text: T,
+}
+
+impl sum_tree::Item for Transform {
+ type Summary = TransformSummary;
+
+ fn summary(&self) -> Self::Summary {
+ match self {
+ Transform::Isomorphic(summary) => TransformSummary {
+ input: summary.clone(),
+ output: summary.clone(),
+ },
+ Transform::Inlay(inlay) => TransformSummary {
+ input: TextSummary::default(),
+ output: inlay.text.summary(),
+ },
+ }
+ }
+}
+
+#[derive(Clone, Debug, Default)]
+struct TransformSummary {
+ input: TextSummary,
+ output: TextSummary,
+}
+
+impl sum_tree::Summary for TransformSummary {
+ type Context = ();
+
+ fn add_summary(&mut self, other: &Self, _: &()) {
+ self.input += &other.input;
+ self.output += &other.output;
+ }
+}
+
+pub type InlayEdit = Edit<InlayOffset>;
+
+#[derive(Copy, Clone, Debug, Default, Eq, Ord, PartialOrd, PartialEq)]
+pub struct InlayOffset(pub usize);
+
+impl Add for InlayOffset {
+ type Output = Self;
+
+ fn add(self, rhs: Self) -> Self::Output {
+ Self(self.0 + rhs.0)
+ }
+}
+
+impl Sub for InlayOffset {
+ type Output = Self;
+
+ fn sub(self, rhs: Self) -> Self::Output {
+ Self(self.0 - rhs.0)
+ }
+}
+
+impl AddAssign for InlayOffset {
+ fn add_assign(&mut self, rhs: Self) {
+ self.0 += rhs.0;
+ }
+}
+
+impl SubAssign for InlayOffset {
+ fn sub_assign(&mut self, rhs: Self) {
+ self.0 -= rhs.0;
+ }
+}
+
+impl<'a> sum_tree::Dimension<'a, TransformSummary> for InlayOffset {
+ fn add_summary(&mut self, summary: &'a TransformSummary, _: &()) {
+ self.0 += &summary.output.len;
+ }
+}
+
+#[derive(Copy, Clone, Debug, Default, Eq, Ord, PartialOrd, PartialEq)]
+pub struct InlayPoint(pub Point);
+
+impl Add for InlayPoint {
+ type Output = Self;
+
+ fn add(self, rhs: Self) -> Self::Output {
+ Self(self.0 + rhs.0)
+ }
+}
+
+impl Sub for InlayPoint {
+ type Output = Self;
+
+ fn sub(self, rhs: Self) -> Self::Output {
+ Self(self.0 - rhs.0)
+ }
+}
+
+impl<'a> sum_tree::Dimension<'a, TransformSummary> for InlayPoint {
+ fn add_summary(&mut self, summary: &'a TransformSummary, _: &()) {
+ self.0 += &summary.output.lines;
+ }
+}
+
+impl<'a> sum_tree::Dimension<'a, TransformSummary> for usize {
+ fn add_summary(&mut self, summary: &'a TransformSummary, _: &()) {
+ *self += &summary.input.len;
+ }
+}
+
+impl<'a> sum_tree::Dimension<'a, TransformSummary> for Point {
+ fn add_summary(&mut self, summary: &'a TransformSummary, _: &()) {
+ *self += &summary.input.lines;
+ }
+}
+
+#[derive(Clone)]
+pub struct InlayBufferRows<'a> {
+ transforms: Cursor<'a, Transform, (InlayPoint, Point)>,
+ buffer_rows: MultiBufferRows<'a>,
+ inlay_row: u32,
+ max_buffer_row: u32,
+}
+
+#[derive(Copy, Clone, Eq, PartialEq)]
+struct HighlightEndpoint {
+ offset: InlayOffset,
+ is_start: bool,
+ tag: Option<TypeId>,
+ style: HighlightStyle,
+}
+
+impl PartialOrd for HighlightEndpoint {
+ fn partial_cmp(&self, other: &Self) -> Option<cmp::Ordering> {
+ Some(self.cmp(other))
+ }
+}
+
+impl Ord for HighlightEndpoint {
+ fn cmp(&self, other: &Self) -> cmp::Ordering {
+ self.offset
+ .cmp(&other.offset)
+ .then_with(|| other.is_start.cmp(&self.is_start))
+ }
+}
+
+pub struct InlayChunks<'a> {
+ transforms: Cursor<'a, Transform, (InlayOffset, usize)>,
+ buffer_chunks: MultiBufferChunks<'a>,
+ buffer_chunk: Option<Chunk<'a>>,
+ inlay_chunks: Option<text::Chunks<'a>>,
+ output_offset: InlayOffset,
+ max_output_offset: InlayOffset,
+ hint_highlight_style: Option<HighlightStyle>,
+ suggestion_highlight_style: Option<HighlightStyle>,
+ highlight_endpoints: Peekable<vec::IntoIter<HighlightEndpoint>>,
+ active_highlights: BTreeMap<Option<TypeId>, HighlightStyle>,
+ snapshot: &'a InlaySnapshot,
+}
+
+impl<'a> InlayChunks<'a> {
+ pub fn seek(&mut self, offset: InlayOffset) {
+ self.transforms.seek(&offset, Bias::Right, &());
+
+ let buffer_offset = self.snapshot.to_buffer_offset(offset);
+ self.buffer_chunks.seek(buffer_offset);
+ self.inlay_chunks = None;
+ self.buffer_chunk = None;
+ self.output_offset = offset;
+ }
+
+ pub fn offset(&self) -> InlayOffset {
+ self.output_offset
+ }
+}
+
+impl<'a> Iterator for InlayChunks<'a> {
+ type Item = Chunk<'a>;
+
+ fn next(&mut self) -> Option<Self::Item> {
+ if self.output_offset == self.max_output_offset {
+ return None;
+ }
+
+ let mut next_highlight_endpoint = InlayOffset(usize::MAX);
+ while let Some(endpoint) = self.highlight_endpoints.peek().copied() {
+ if endpoint.offset <= self.output_offset {
+ if endpoint.is_start {
+ self.active_highlights.insert(endpoint.tag, endpoint.style);
+ } else {
+ self.active_highlights.remove(&endpoint.tag);
+ }
+ self.highlight_endpoints.next();
+ } else {
+ next_highlight_endpoint = endpoint.offset;
+ break;
+ }
+ }
+
+ let chunk = match self.transforms.item()? {
+ Transform::Isomorphic(_) => {
+ let chunk = self
+ .buffer_chunk
+ .get_or_insert_with(|| self.buffer_chunks.next().unwrap());
+ if chunk.text.is_empty() {
+ *chunk = self.buffer_chunks.next().unwrap();
+ }
+
+ let (prefix, suffix) = chunk.text.split_at(
+ chunk
+ .text
+ .len()
+ .min(self.transforms.end(&()).0 .0 - self.output_offset.0)
+ .min(next_highlight_endpoint.0 - self.output_offset.0),
+ );
+
+ chunk.text = suffix;
+ self.output_offset.0 += prefix.len();
+ let mut prefix = Chunk {
+ text: prefix,
+ ..chunk.clone()
+ };
+ if !self.active_highlights.is_empty() {
+ let mut highlight_style = HighlightStyle::default();
+ for active_highlight in self.active_highlights.values() {
+ highlight_style.highlight(*active_highlight);
+ }
+ prefix.highlight_style = Some(highlight_style);
+ }
+ prefix
+ }
+ Transform::Inlay(inlay) => {
+ let inlay_chunks = self.inlay_chunks.get_or_insert_with(|| {
+ let start = self.output_offset - self.transforms.start().0;
+ let end = cmp::min(self.max_output_offset, self.transforms.end(&()).0)
+ - self.transforms.start().0;
+ inlay.text.chunks_in_range(start.0..end.0)
+ });
+
+ let chunk = inlay_chunks.next().unwrap();
+ self.output_offset.0 += chunk.len();
+ let highlight_style = match inlay.id {
+ InlayId::Suggestion(_) => self.suggestion_highlight_style,
+ InlayId::Hint(_) => self.hint_highlight_style,
+ };
+ Chunk {
+ text: chunk,
+ highlight_style,
+ ..Default::default()
+ }
+ }
+ };
+
+ if self.output_offset == self.transforms.end(&()).0 {
+ self.inlay_chunks = None;
+ self.transforms.next(&());
+ }
+
+ Some(chunk)
+ }
+}
+
+impl<'a> InlayBufferRows<'a> {
+ pub fn seek(&mut self, row: u32) {
+ let inlay_point = InlayPoint::new(row, 0);
+ self.transforms.seek(&inlay_point, Bias::Left, &());
+
+ let mut buffer_point = self.transforms.start().1;
+ let buffer_row = if row == 0 {
+ 0
+ } else {
+ match self.transforms.item() {
+ Some(Transform::Isomorphic(_)) => {
+ buffer_point += inlay_point.0 - self.transforms.start().0 .0;
+ buffer_point.row
+ }
+ _ => cmp::min(buffer_point.row + 1, self.max_buffer_row),
+ }
+ };
+ self.inlay_row = inlay_point.row();
+ self.buffer_rows.seek(buffer_row);
+ }
+}
+
+impl<'a> Iterator for InlayBufferRows<'a> {
+ type Item = Option<u32>;
+
+ fn next(&mut self) -> Option<Self::Item> {
+ let buffer_row = if self.inlay_row == 0 {
+ self.buffer_rows.next().unwrap()
+ } else {
+ match self.transforms.item()? {
+ Transform::Inlay(_) => None,
+ Transform::Isomorphic(_) => self.buffer_rows.next().unwrap(),
+ }
+ };
+
+ self.inlay_row += 1;
+ self.transforms
+ .seek_forward(&InlayPoint::new(self.inlay_row, 0), Bias::Left, &());
+
+ Some(buffer_row)
+ }
+}
+
+impl InlayPoint {
+ pub fn new(row: u32, column: u32) -> Self {
+ Self(Point::new(row, column))
+ }
+
+ pub fn row(self) -> u32 {
+ self.0.row
+ }
+}
+
+impl InlayMap {
+ pub fn new(buffer: MultiBufferSnapshot) -> (Self, InlaySnapshot) {
+ let version = 0;
+ let snapshot = InlaySnapshot {
+ buffer: buffer.clone(),
+ transforms: SumTree::from_iter(Some(Transform::Isomorphic(buffer.text_summary())), &()),
+ version,
+ };
+
+ (
+ Self {
+ snapshot: snapshot.clone(),
+ inlays_by_id: HashMap::default(),
+ inlays: Vec::new(),
+ },
+ snapshot,
+ )
+ }
+
+ pub fn sync(
+ &mut self,
+ buffer_snapshot: MultiBufferSnapshot,
+ mut buffer_edits: Vec<text::Edit<usize>>,
+ ) -> (InlaySnapshot, Vec<InlayEdit>) {
+ let mut snapshot = &mut self.snapshot;
+
+ if buffer_edits.is_empty() {
+ if snapshot.buffer.trailing_excerpt_update_count()
+ != buffer_snapshot.trailing_excerpt_update_count()
+ {
+ buffer_edits.push(Edit {
+ old: snapshot.buffer.len()..snapshot.buffer.len(),
+ new: buffer_snapshot.len()..buffer_snapshot.len(),
+ });
+ }
+ }
+
+ if buffer_edits.is_empty() {
+ if snapshot.buffer.edit_count() != buffer_snapshot.edit_count()
+ || snapshot.buffer.parse_count() != buffer_snapshot.parse_count()
+ || snapshot.buffer.diagnostics_update_count()
+ != buffer_snapshot.diagnostics_update_count()
+ || snapshot.buffer.git_diff_update_count()
+ != buffer_snapshot.git_diff_update_count()
+ || snapshot.buffer.trailing_excerpt_update_count()
+ != buffer_snapshot.trailing_excerpt_update_count()
+ {
+ snapshot.version += 1;
+ }
+
+ snapshot.buffer = buffer_snapshot;
+ (snapshot.clone(), Vec::new())
+ } else {
+ let mut inlay_edits = Patch::default();
+ let mut new_transforms = SumTree::new();
+ let mut cursor = snapshot.transforms.cursor::<(usize, InlayOffset)>();
+ let mut buffer_edits_iter = buffer_edits.iter().peekable();
+ while let Some(buffer_edit) = buffer_edits_iter.next() {
+ new_transforms.append(cursor.slice(&buffer_edit.old.start, Bias::Left, &()), &());
+ if let Some(Transform::Isomorphic(transform)) = cursor.item() {
+ if cursor.end(&()).0 == buffer_edit.old.start {
+ push_isomorphic(&mut new_transforms, transform.clone());
+ cursor.next(&());
+ }
+ }
+
+ // Remove all the inlays and transforms contained by the edit.
+ let old_start =
+ cursor.start().1 + InlayOffset(buffer_edit.old.start - cursor.start().0);
+ cursor.seek(&buffer_edit.old.end, Bias::Right, &());
+ let old_end =
+ cursor.start().1 + InlayOffset(buffer_edit.old.end - cursor.start().0);
+
+ // Push the unchanged prefix.
+ let prefix_start = new_transforms.summary().input.len;
+ let prefix_end = buffer_edit.new.start;
+ push_isomorphic(
+ &mut new_transforms,
+ buffer_snapshot.text_summary_for_range(prefix_start..prefix_end),
+ );
+ let new_start = InlayOffset(new_transforms.summary().output.len);
+
+ let start_ix = match self.inlays.binary_search_by(|probe| {
+ probe
+ .position
+ .to_offset(&buffer_snapshot)
+ .cmp(&buffer_edit.new.start)
+ .then(std::cmp::Ordering::Greater)
+ }) {
+ Ok(ix) | Err(ix) => ix,
+ };
+
+ for inlay in &self.inlays[start_ix..] {
+ let buffer_offset = inlay.position.to_offset(&buffer_snapshot);
+ if buffer_offset > buffer_edit.new.end {
+ break;
+ }
+
+ let prefix_start = new_transforms.summary().input.len;
+ let prefix_end = buffer_offset;
+ push_isomorphic(
+ &mut new_transforms,
+ buffer_snapshot.text_summary_for_range(prefix_start..prefix_end),
+ );
+
+ if inlay.position.is_valid(&buffer_snapshot) {
+ new_transforms.push(Transform::Inlay(inlay.clone()), &());
+ }
+ }
+
+ // Apply the rest of the edit.
+ let transform_start = new_transforms.summary().input.len;
+ push_isomorphic(
+ &mut new_transforms,
+ buffer_snapshot.text_summary_for_range(transform_start..buffer_edit.new.end),
+ );
+ let new_end = InlayOffset(new_transforms.summary().output.len);
+ inlay_edits.push(Edit {
+ old: old_start..old_end,
+ new: new_start..new_end,
+ });
+
+ // If the next edit doesn't intersect the current isomorphic transform, then
+ // we can push its remainder.
+ if buffer_edits_iter
+ .peek()
+ .map_or(true, |edit| edit.old.start >= cursor.end(&()).0)
+ {
+ let transform_start = new_transforms.summary().input.len;
+ let transform_end =
+ buffer_edit.new.end + (cursor.end(&()).0 - buffer_edit.old.end);
+ push_isomorphic(
+ &mut new_transforms,
+ buffer_snapshot.text_summary_for_range(transform_start..transform_end),
+ );
+ cursor.next(&());
+ }
+ }
+
+ new_transforms.append(cursor.suffix(&()), &());
+ if new_transforms.is_empty() {
+ new_transforms.push(Transform::Isomorphic(Default::default()), &());
+ }
+
+ drop(cursor);
+ snapshot.transforms = new_transforms;
+ snapshot.version += 1;
+ snapshot.buffer = buffer_snapshot;
+ snapshot.check_invariants();
+
+ (snapshot.clone(), inlay_edits.into_inner())
+ }
+ }
+
+ pub fn splice<T: Into<Rope>>(
+ &mut self,
+ to_remove: Vec<InlayId>,
+ to_insert: Vec<(InlayId, InlayProperties<T>)>,
+ ) -> (InlaySnapshot, Vec<InlayEdit>) {
+ let snapshot = &mut self.snapshot;
+ let mut edits = BTreeSet::new();
+
+ self.inlays.retain(|inlay| !to_remove.contains(&inlay.id));
+ for inlay_id in to_remove {
+ if let Some(inlay) = self.inlays_by_id.remove(&inlay_id) {
+ let offset = inlay.position.to_offset(&snapshot.buffer);
+ edits.insert(offset);
+ }
+ }
+
+ for (existing_id, properties) in to_insert {
+ let inlay = Inlay {
+ id: existing_id,
+ position: properties.position,
+ text: properties.text.into(),
+ };
+
+ // Avoid inserting empty inlays.
+ if inlay.text.is_empty() {
+ continue;
+ }
+
+ self.inlays_by_id.insert(inlay.id, inlay.clone());
+ match self
+ .inlays
+ .binary_search_by(|probe| probe.position.cmp(&inlay.position, &snapshot.buffer))
+ {
+ Ok(ix) | Err(ix) => {
+ self.inlays.insert(ix, inlay.clone());
+ }
+ }
+
+ let offset = inlay.position.to_offset(&snapshot.buffer);
+ edits.insert(offset);
+ }
+
+ let buffer_edits = edits
+ .into_iter()
+ .map(|offset| Edit {
+ old: offset..offset,
+ new: offset..offset,
+ })
+ .collect();
+ let buffer_snapshot = snapshot.buffer.clone();
+ drop(snapshot);
+ let (snapshot, edits) = self.sync(buffer_snapshot, buffer_edits);
+ (snapshot, edits)
+ }
+
+ pub fn current_inlays(&self) -> impl Iterator<Item = &Inlay> {
+ self.inlays.iter()
+ }
+
+ #[cfg(test)]
+ pub(crate) fn randomly_mutate(
+ &mut self,
+ next_inlay_id: &mut usize,
+ rng: &mut rand::rngs::StdRng,
+ ) -> (InlaySnapshot, Vec<InlayEdit>) {
+ use rand::prelude::*;
+ use util::post_inc;
+
+ let mut to_remove = Vec::new();
+ let mut to_insert = Vec::new();
+ let snapshot = &mut self.snapshot;
+ for i in 0..rng.gen_range(1..=5) {
+ if self.inlays.is_empty() || rng.gen() {
+ let position = snapshot.buffer.random_byte_range(0, rng).start;
+ let bias = if rng.gen() { Bias::Left } else { Bias::Right };
+ let len = if rng.gen_bool(0.01) {
+ 0
+ } else {
+ rng.gen_range(1..=5)
+ };
+ let text = util::RandomCharIter::new(&mut *rng)
+ .filter(|ch| *ch != '\r')
+ .take(len)
+ .collect::<String>();
+ log::info!(
+ "creating inlay at buffer offset {} with bias {:?} and text {:?}",
+ position,
+ bias,
+ text
+ );
+
+ let inlay_id = if i % 2 == 0 {
+ InlayId::Hint(post_inc(next_inlay_id))
+ } else {
+ InlayId::Suggestion(post_inc(next_inlay_id))
+ };
+ to_insert.push((
+ inlay_id,
+ InlayProperties {
+ position: snapshot.buffer.anchor_at(position, bias),
+ text,
+ },
+ ));
+ } else {
+ to_remove.push(*self.inlays_by_id.keys().choose(rng).unwrap());
+ }
+ }
+ log::info!("removing inlays: {:?}", to_remove);
+
+ drop(snapshot);
+ let (snapshot, edits) = self.splice(to_remove, to_insert);
+ (snapshot, edits)
+ }
+}
+
+impl InlaySnapshot {
+ pub fn to_point(&self, offset: InlayOffset) -> InlayPoint {
+ let mut cursor = self
+ .transforms
+ .cursor::<(InlayOffset, (InlayPoint, usize))>();
+ cursor.seek(&offset, Bias::Right, &());
+ let overshoot = offset.0 - cursor.start().0 .0;
+ match cursor.item() {
+ Some(Transform::Isomorphic(_)) => {
+ let buffer_offset_start = cursor.start().1 .1;
+ let buffer_offset_end = buffer_offset_start + overshoot;
+ let buffer_start = self.buffer.offset_to_point(buffer_offset_start);
+ let buffer_end = self.buffer.offset_to_point(buffer_offset_end);
+ InlayPoint(cursor.start().1 .0 .0 + (buffer_end - buffer_start))
+ }
+ Some(Transform::Inlay(inlay)) => {
+ let overshoot = inlay.text.offset_to_point(overshoot);
+ InlayPoint(cursor.start().1 .0 .0 + overshoot)
+ }
+ None => self.max_point(),
+ }
+ }
+
+ pub fn len(&self) -> InlayOffset {
+ InlayOffset(self.transforms.summary().output.len)
+ }
+
+ pub fn max_point(&self) -> InlayPoint {
+ InlayPoint(self.transforms.summary().output.lines)
+ }
+
+ pub fn to_offset(&self, point: InlayPoint) -> InlayOffset {
+ let mut cursor = self
+ .transforms
+ .cursor::<(InlayPoint, (InlayOffset, Point))>();
+ cursor.seek(&point, Bias::Right, &());
+ let overshoot = point.0 - cursor.start().0 .0;
+ match cursor.item() {
+ Some(Transform::Isomorphic(_)) => {
+ let buffer_point_start = cursor.start().1 .1;
+ let buffer_point_end = buffer_point_start + overshoot;
+ let buffer_offset_start = self.buffer.point_to_offset(buffer_point_start);
+ let buffer_offset_end = self.buffer.point_to_offset(buffer_point_end);
+ InlayOffset(cursor.start().1 .0 .0 + (buffer_offset_end - buffer_offset_start))
+ }
+ Some(Transform::Inlay(inlay)) => {
+ let overshoot = inlay.text.point_to_offset(overshoot);
+ InlayOffset(cursor.start().1 .0 .0 + overshoot)
+ }
+ None => self.len(),
+ }
+ }
+
+ pub fn to_buffer_point(&self, point: InlayPoint) -> Point {
+ let mut cursor = self.transforms.cursor::<(InlayPoint, Point)>();
+ cursor.seek(&point, Bias::Right, &());
+ match cursor.item() {
+ Some(Transform::Isomorphic(_)) => {
+ let overshoot = point.0 - cursor.start().0 .0;
+ cursor.start().1 + overshoot
+ }
+ Some(Transform::Inlay(_)) => cursor.start().1,
+ None => self.buffer.max_point(),
+ }
+ }
+
+ pub fn to_buffer_offset(&self, offset: InlayOffset) -> usize {
+ let mut cursor = self.transforms.cursor::<(InlayOffset, usize)>();
+ cursor.seek(&offset, Bias::Right, &());
+ match cursor.item() {
+ Some(Transform::Isomorphic(_)) => {
+ let overshoot = offset - cursor.start().0;
+ cursor.start().1 + overshoot.0
+ }
+ Some(Transform::Inlay(_)) => cursor.start().1,
+ None => self.buffer.len(),
+ }
+ }
+
+ pub fn to_inlay_offset(&self, offset: usize) -> InlayOffset {
+ let mut cursor = self.transforms.cursor::<(usize, InlayOffset)>();
+ cursor.seek(&offset, Bias::Left, &());
+ loop {
+ match cursor.item() {
+ Some(Transform::Isomorphic(_)) => {
+ if offset == cursor.end(&()).0 {
+ while let Some(Transform::Inlay(inlay)) = cursor.next_item() {
+ if inlay.position.bias() == Bias::Right {
+ break;
+ } else {
+ cursor.next(&());
+ }
+ }
+ return cursor.end(&()).1;
+ } else {
+ let overshoot = offset - cursor.start().0;
+ return InlayOffset(cursor.start().1 .0 + overshoot);
+ }
+ }
+ Some(Transform::Inlay(inlay)) => {
+ if inlay.position.bias() == Bias::Left {
+ cursor.next(&());
+ } else {
+ return cursor.start().1;
+ }
+ }
+ None => {
+ return self.len();
+ }
+ }
+ }
+ }
+
+ pub fn to_inlay_point(&self, point: Point) -> InlayPoint {
+ let mut cursor = self.transforms.cursor::<(Point, InlayPoint)>();
+ cursor.seek(&point, Bias::Left, &());
+ loop {
+ match cursor.item() {
+ Some(Transform::Isomorphic(_)) => {
+ if point == cursor.end(&()).0 {
+ while let Some(Transform::Inlay(inlay)) = cursor.next_item() {
+ if inlay.position.bias() == Bias::Right {
+ break;
+ } else {
+ cursor.next(&());
+ }
+ }
+ return cursor.end(&()).1;
+ } else {
+ let overshoot = point - cursor.start().0;
+ return InlayPoint(cursor.start().1 .0 + overshoot);
+ }
+ }
+ Some(Transform::Inlay(inlay)) => {
+ if inlay.position.bias() == Bias::Left {
+ cursor.next(&());
+ } else {
+ return cursor.start().1;
+ }
+ }
+ None => {
+ return self.max_point();
+ }
+ }
+ }
+ }
+
+ pub fn clip_point(&self, mut point: InlayPoint, mut bias: Bias) -> InlayPoint {
+ let mut cursor = self.transforms.cursor::<(InlayPoint, Point)>();
+ cursor.seek(&point, Bias::Left, &());
+ loop {
+ match cursor.item() {
+ Some(Transform::Isomorphic(transform)) => {
+ if cursor.start().0 == point {
+ if let Some(Transform::Inlay(inlay)) = cursor.prev_item() {
+ if inlay.position.bias() == Bias::Left {
+ return point;
+ } else if bias == Bias::Left {
+ cursor.prev(&());
+ } else if transform.first_line_chars == 0 {
+ point.0 += Point::new(1, 0);
+ } else {
+ point.0 += Point::new(0, 1);
+ }
+ } else {
+ return point;
+ }
+ } else if cursor.end(&()).0 == point {
+ if let Some(Transform::Inlay(inlay)) = cursor.next_item() {
+ if inlay.position.bias() == Bias::Right {
+ return point;
+ } else if bias == Bias::Right {
+ cursor.next(&());
+ } else if point.0.column == 0 {
+ point.0.row -= 1;
+ point.0.column = self.line_len(point.0.row);
+ } else {
+ point.0.column -= 1;
+ }
+ } else {
+ return point;
+ }
+ } else {
+ let overshoot = point.0 - cursor.start().0 .0;
+ let buffer_point = cursor.start().1 + overshoot;
+ let clipped_buffer_point = self.buffer.clip_point(buffer_point, bias);
+ let clipped_overshoot = clipped_buffer_point - cursor.start().1;
+ let clipped_point = InlayPoint(cursor.start().0 .0 + clipped_overshoot);
+ if clipped_point == point {
+ return clipped_point;
+ } else {
+ point = clipped_point;
+ }
+ }
+ }
+ Some(Transform::Inlay(inlay)) => {
+ if point == cursor.start().0 && inlay.position.bias() == Bias::Right {
+ match cursor.prev_item() {
+ Some(Transform::Inlay(inlay)) => {
+ if inlay.position.bias() == Bias::Left {
+ return point;
+ }
+ }
+ _ => return point,
+ }
+ } else if point == cursor.end(&()).0 && inlay.position.bias() == Bias::Left {
+ match cursor.next_item() {
+ Some(Transform::Inlay(inlay)) => {
+ if inlay.position.bias() == Bias::Right {
+ return point;
+ }
+ }
+ _ => return point,
+ }
+ }
+
+ if bias == Bias::Left {
+ point = cursor.start().0;
+ cursor.prev(&());
+ } else {
+ cursor.next(&());
+ point = cursor.start().0;
+ }
+ }
+ None => {
+ bias = bias.invert();
+ if bias == Bias::Left {
+ point = cursor.start().0;
+ cursor.prev(&());
+ } else {
+ cursor.next(&());
+ point = cursor.start().0;
+ }
+ }
+ }
+ }
+ }
+
+ pub fn text_summary(&self) -> TextSummary {
+ self.transforms.summary().output.clone()
+ }
+
+ pub fn text_summary_for_range(&self, range: Range<InlayOffset>) -> TextSummary {
+ let mut summary = TextSummary::default();
+
+ let mut cursor = self.transforms.cursor::<(InlayOffset, usize)>();
+ cursor.seek(&range.start, Bias::Right, &());
+
+ let overshoot = range.start.0 - cursor.start().0 .0;
+ match cursor.item() {
+ Some(Transform::Isomorphic(_)) => {
+ let buffer_start = cursor.start().1;
+ let suffix_start = buffer_start + overshoot;
+ let suffix_end =
+ buffer_start + (cmp::min(cursor.end(&()).0, range.end).0 - cursor.start().0 .0);
+ summary = self.buffer.text_summary_for_range(suffix_start..suffix_end);
+ cursor.next(&());
+ }
+ Some(Transform::Inlay(inlay)) => {
+ let suffix_start = overshoot;
+ let suffix_end = cmp::min(cursor.end(&()).0, range.end).0 - cursor.start().0 .0;
+ summary = inlay.text.cursor(suffix_start).summary(suffix_end);
+ cursor.next(&());
+ }
+ None => {}
+ }
+
+ if range.end > cursor.start().0 {
+ summary += cursor
+ .summary::<_, TransformSummary>(&range.end, Bias::Right, &())
+ .output;
+
+ let overshoot = range.end.0 - cursor.start().0 .0;
+ match cursor.item() {
+ Some(Transform::Isomorphic(_)) => {
+ let prefix_start = cursor.start().1;
+ let prefix_end = prefix_start + overshoot;
+ summary += self
+ .buffer
+ .text_summary_for_range::<TextSummary, _>(prefix_start..prefix_end);
+ }
+ Some(Transform::Inlay(inlay)) => {
+ let prefix_end = overshoot;
+ summary += inlay.text.cursor(0).summary::<TextSummary>(prefix_end);
+ }
+ None => {}
+ }
+ }
+
+ summary
+ }
+
+ pub fn buffer_rows<'a>(&'a self, row: u32) -> InlayBufferRows<'a> {
+ let mut cursor = self.transforms.cursor::<(InlayPoint, Point)>();
+ let inlay_point = InlayPoint::new(row, 0);
+ cursor.seek(&inlay_point, Bias::Left, &());
+
+ let max_buffer_row = self.buffer.max_point().row;
+ let mut buffer_point = cursor.start().1;
+ let buffer_row = if row == 0 {
+ 0
+ } else {
+ match cursor.item() {
+ Some(Transform::Isomorphic(_)) => {
+ buffer_point += inlay_point.0 - cursor.start().0 .0;
+ buffer_point.row
+ }
+ _ => cmp::min(buffer_point.row + 1, max_buffer_row),
+ }
+ };
+
+ InlayBufferRows {
+ transforms: cursor,
+ inlay_row: inlay_point.row(),
+ buffer_rows: self.buffer.buffer_rows(buffer_row),
+ max_buffer_row,
+ }
+ }
+
+ pub fn line_len(&self, row: u32) -> u32 {
+ let line_start = self.to_offset(InlayPoint::new(row, 0)).0;
+ let line_end = if row >= self.max_point().row() {
+ self.len().0
+ } else {
+ self.to_offset(InlayPoint::new(row + 1, 0)).0 - 1
+ };
+ (line_end - line_start) as u32
+ }
+
+ pub fn chunks<'a>(
+ &'a self,
+ range: Range<InlayOffset>,
+ language_aware: bool,
+ text_highlights: Option<&'a TextHighlights>,
+ hint_highlights: Option<HighlightStyle>,
+ suggestion_highlights: Option<HighlightStyle>,
+ ) -> InlayChunks<'a> {
+ let mut cursor = self.transforms.cursor::<(InlayOffset, usize)>();
+ cursor.seek(&range.start, Bias::Right, &());
+
+ let mut highlight_endpoints = Vec::new();
+ if let Some(text_highlights) = text_highlights {
+ if !text_highlights.is_empty() {
+ while cursor.start().0 < range.end {
+ if true {
+ let transform_start = self.buffer.anchor_after(
+ self.to_buffer_offset(cmp::max(range.start, cursor.start().0)),
+ );
+
+ let transform_end = {
+ let overshoot = InlayOffset(range.end.0 - cursor.start().0 .0);
+ self.buffer.anchor_before(self.to_buffer_offset(cmp::min(
+ cursor.end(&()).0,
+ cursor.start().0 + overshoot,
+ )))
+ };
+
+ for (tag, highlights) in text_highlights.iter() {
+ let style = highlights.0;
+ let ranges = &highlights.1;
+
+ let start_ix = match ranges.binary_search_by(|probe| {
+ let cmp = probe.end.cmp(&transform_start, &self.buffer);
+ if cmp.is_gt() {
+ cmp::Ordering::Greater
+ } else {
+ cmp::Ordering::Less
+ }
+ }) {
+ Ok(i) | Err(i) => i,
+ };
+ for range in &ranges[start_ix..] {
+ if range.start.cmp(&transform_end, &self.buffer).is_ge() {
+ break;
+ }
+
+ highlight_endpoints.push(HighlightEndpoint {
+ offset: self
+ .to_inlay_offset(range.start.to_offset(&self.buffer)),
+ is_start: true,
+ tag: *tag,
+ style,
+ });
+ highlight_endpoints.push(HighlightEndpoint {
+ offset: self.to_inlay_offset(range.end.to_offset(&self.buffer)),
+ is_start: false,
+ tag: *tag,
+ style,
+ });
+ }
+ }
+ }
+
+ cursor.next(&());
+ }
+ highlight_endpoints.sort();
+ cursor.seek(&range.start, Bias::Right, &());
+ }
+ }
+
+ let buffer_range = self.to_buffer_offset(range.start)..self.to_buffer_offset(range.end);
+ let buffer_chunks = self.buffer.chunks(buffer_range, language_aware);
+
+ InlayChunks {
+ transforms: cursor,
+ buffer_chunks,
+ inlay_chunks: None,
+ buffer_chunk: None,
+ output_offset: range.start,
+ max_output_offset: range.end,
+ hint_highlight_style: hint_highlights,
+ suggestion_highlight_style: suggestion_highlights,
+ highlight_endpoints: highlight_endpoints.into_iter().peekable(),
+ active_highlights: Default::default(),
+ snapshot: self,
+ }
+ }
+
+ #[cfg(test)]
+ pub fn text(&self) -> String {
+ self.chunks(Default::default()..self.len(), false, None, None, None)
+ .map(|chunk| chunk.text)
+ .collect()
+ }
+
+ fn check_invariants(&self) {
+ #[cfg(any(debug_assertions, feature = "test-support"))]
+ {
+ assert_eq!(self.transforms.summary().input, self.buffer.text_summary());
+ let mut transforms = self.transforms.iter().peekable();
+ while let Some(transform) = transforms.next() {
+ let transform_is_isomorphic = matches!(transform, Transform::Isomorphic(_));
+ if let Some(next_transform) = transforms.peek() {
+ let next_transform_is_isomorphic =
+ matches!(next_transform, Transform::Isomorphic(_));
+ assert!(
+ !transform_is_isomorphic || !next_transform_is_isomorphic,
+ "two adjacent isomorphic transforms"
+ );
+ }
+ }
+ }
+ }
+}
+
+fn push_isomorphic(sum_tree: &mut SumTree<Transform>, summary: TextSummary) {
+ if summary.len == 0 {
+ return;
+ }
+
+ let mut summary = Some(summary);
+ sum_tree.update_last(
+ |transform| {
+ if let Transform::Isomorphic(transform) = transform {
+ *transform += summary.take().unwrap();
+ }
+ },
+ &(),
+ );
+
+ if let Some(summary) = summary {
+ sum_tree.push(Transform::Isomorphic(summary), &());
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use crate::{InlayId, MultiBuffer};
+ use gpui::AppContext;
+ use rand::prelude::*;
+ use settings::SettingsStore;
+ use std::{cmp::Reverse, env, sync::Arc};
+ use sum_tree::TreeMap;
+ use text::Patch;
+ use util::post_inc;
+
+ #[gpui::test]
+ fn test_basic_inlays(cx: &mut AppContext) {
+ let buffer = MultiBuffer::build_simple("abcdefghi", cx);
+ let buffer_edits = buffer.update(cx, |buffer, _| buffer.subscribe());
+ let (mut inlay_map, inlay_snapshot) = InlayMap::new(buffer.read(cx).snapshot(cx));
+ assert_eq!(inlay_snapshot.text(), "abcdefghi");
+ let mut next_inlay_id = 0;
+
+ let (inlay_snapshot, _) = inlay_map.splice(
+ Vec::new(),
+ vec![(
+ InlayId::Hint(post_inc(&mut next_inlay_id)),
+ InlayProperties {
+ position: buffer.read(cx).snapshot(cx).anchor_after(3),
+ text: "|123|",
+ },
+ )],
+ );
+ assert_eq!(inlay_snapshot.text(), "abc|123|defghi");
+ assert_eq!(
+ inlay_snapshot.to_inlay_point(Point::new(0, 0)),
+ InlayPoint::new(0, 0)
+ );
+ assert_eq!(
+ inlay_snapshot.to_inlay_point(Point::new(0, 1)),
+ InlayPoint::new(0, 1)
+ );
+ assert_eq!(
+ inlay_snapshot.to_inlay_point(Point::new(0, 2)),
+ InlayPoint::new(0, 2)
+ );
+ assert_eq!(
+ inlay_snapshot.to_inlay_point(Point::new(0, 3)),
+ InlayPoint::new(0, 3)
+ );
+ assert_eq!(
+ inlay_snapshot.to_inlay_point(Point::new(0, 4)),
+ InlayPoint::new(0, 9)
+ );
+ assert_eq!(
+ inlay_snapshot.to_inlay_point(Point::new(0, 5)),
+ InlayPoint::new(0, 10)
+ );
+ assert_eq!(
+ inlay_snapshot.clip_point(InlayPoint::new(0, 0), Bias::Left),
+ InlayPoint::new(0, 0)
+ );
+ assert_eq!(
+ inlay_snapshot.clip_point(InlayPoint::new(0, 0), Bias::Right),
+ InlayPoint::new(0, 0)
+ );
+ assert_eq!(
+ inlay_snapshot.clip_point(InlayPoint::new(0, 3), Bias::Left),
+ InlayPoint::new(0, 3)
+ );
+ assert_eq!(
+ inlay_snapshot.clip_point(InlayPoint::new(0, 3), Bias::Right),
+ InlayPoint::new(0, 3)
+ );
+ assert_eq!(
+ inlay_snapshot.clip_point(InlayPoint::new(0, 4), Bias::Left),
+ InlayPoint::new(0, 3)
+ );
+ assert_eq!(
+ inlay_snapshot.clip_point(InlayPoint::new(0, 4), Bias::Right),
+ InlayPoint::new(0, 9)
+ );
+
+ // Edits before or after the inlay should not affect it.
+ buffer.update(cx, |buffer, cx| {
+ buffer.edit([(2..3, "x"), (3..3, "y"), (4..4, "z")], None, cx)
+ });
+ let (inlay_snapshot, _) = inlay_map.sync(
+ buffer.read(cx).snapshot(cx),
+ buffer_edits.consume().into_inner(),
+ );
+ assert_eq!(inlay_snapshot.text(), "abxy|123|dzefghi");
+
+ // An edit surrounding the inlay should invalidate it.
+ buffer.update(cx, |buffer, cx| buffer.edit([(4..5, "D")], None, cx));
+ let (inlay_snapshot, _) = inlay_map.sync(
+ buffer.read(cx).snapshot(cx),
+ buffer_edits.consume().into_inner(),
+ );
+ assert_eq!(inlay_snapshot.text(), "abxyDzefghi");
+
+ let (inlay_snapshot, _) = inlay_map.splice(
+ Vec::new(),
+ vec![
+ (
+ InlayId::Hint(post_inc(&mut next_inlay_id)),
+ InlayProperties {
+ position: buffer.read(cx).snapshot(cx).anchor_before(3),
+ text: "|123|",
+ },
+ ),
+ (
+ InlayId::Suggestion(post_inc(&mut next_inlay_id)),
+ InlayProperties {
+ position: buffer.read(cx).snapshot(cx).anchor_after(3),
+ text: "|456|",
+ },
+ ),
+ ],
+ );
+ assert_eq!(inlay_snapshot.text(), "abx|123||456|yDzefghi");
+
+ // Edits ending where the inlay starts should not move it if it has a left bias.
+ buffer.update(cx, |buffer, cx| buffer.edit([(3..3, "JKL")], None, cx));
+ let (inlay_snapshot, _) = inlay_map.sync(
+ buffer.read(cx).snapshot(cx),
+ buffer_edits.consume().into_inner(),
+ );
+ assert_eq!(inlay_snapshot.text(), "abx|123|JKL|456|yDzefghi");
+
+ assert_eq!(
+ inlay_snapshot.clip_point(InlayPoint::new(0, 0), Bias::Left),
+ InlayPoint::new(0, 0)
+ );
+ assert_eq!(
+ inlay_snapshot.clip_point(InlayPoint::new(0, 0), Bias::Right),
+ InlayPoint::new(0, 0)
+ );
+
+ assert_eq!(
+ inlay_snapshot.clip_point(InlayPoint::new(0, 1), Bias::Left),
+ InlayPoint::new(0, 1)
+ );
+ assert_eq!(
+ inlay_snapshot.clip_point(InlayPoint::new(0, 1), Bias::Right),
+ InlayPoint::new(0, 1)
+ );
+
+ assert_eq!(
+ inlay_snapshot.clip_point(InlayPoint::new(0, 2), Bias::Left),
+ InlayPoint::new(0, 2)
+ );
+ assert_eq!(
+ inlay_snapshot.clip_point(InlayPoint::new(0, 2), Bias::Right),
+ InlayPoint::new(0, 2)
+ );
+
+ assert_eq!(
+ inlay_snapshot.clip_point(InlayPoint::new(0, 3), Bias::Left),
+ InlayPoint::new(0, 2)
+ );
+ assert_eq!(
+ inlay_snapshot.clip_point(InlayPoint::new(0, 3), Bias::Right),
+ InlayPoint::new(0, 8)
+ );
+
+ assert_eq!(
+ inlay_snapshot.clip_point(InlayPoint::new(0, 4), Bias::Left),
+ InlayPoint::new(0, 2)
+ );
+ assert_eq!(
+ inlay_snapshot.clip_point(InlayPoint::new(0, 4), Bias::Right),
+ InlayPoint::new(0, 8)
+ );
+
+ assert_eq!(
+ inlay_snapshot.clip_point(InlayPoint::new(0, 5), Bias::Left),
+ InlayPoint::new(0, 2)
+ );
+ assert_eq!(
+ inlay_snapshot.clip_point(InlayPoint::new(0, 5), Bias::Right),
+ InlayPoint::new(0, 8)
+ );
+
+ assert_eq!(
+ inlay_snapshot.clip_point(InlayPoint::new(0, 6), Bias::Left),
+ InlayPoint::new(0, 2)
+ );
+ assert_eq!(
+ inlay_snapshot.clip_point(InlayPoint::new(0, 6), Bias::Right),
+ InlayPoint::new(0, 8)
+ );
+
+ assert_eq!(
+ inlay_snapshot.clip_point(InlayPoint::new(0, 7), Bias::Left),
+ InlayPoint::new(0, 2)
+ );
+ assert_eq!(
+ inlay_snapshot.clip_point(InlayPoint::new(0, 7), Bias::Right),
+ InlayPoint::new(0, 8)
+ );
+
+ assert_eq!(
+ inlay_snapshot.clip_point(InlayPoint::new(0, 8), Bias::Left),
+ InlayPoint::new(0, 8)
+ );
+ assert_eq!(
+ inlay_snapshot.clip_point(InlayPoint::new(0, 8), Bias::Right),
+ InlayPoint::new(0, 8)
+ );
+
+ assert_eq!(
+ inlay_snapshot.clip_point(InlayPoint::new(0, 9), Bias::Left),
+ InlayPoint::new(0, 9)
+ );
+ assert_eq!(
+ inlay_snapshot.clip_point(InlayPoint::new(0, 9), Bias::Right),
+ InlayPoint::new(0, 9)
+ );
+
+ assert_eq!(
+ inlay_snapshot.clip_point(InlayPoint::new(0, 10), Bias::Left),
+ InlayPoint::new(0, 10)
+ );
+ assert_eq!(
+ inlay_snapshot.clip_point(InlayPoint::new(0, 10), Bias::Right),
+ InlayPoint::new(0, 10)
+ );
+
+ assert_eq!(
+ inlay_snapshot.clip_point(InlayPoint::new(0, 11), Bias::Left),
+ InlayPoint::new(0, 11)
+ );
+ assert_eq!(
+ inlay_snapshot.clip_point(InlayPoint::new(0, 11), Bias::Right),
+ InlayPoint::new(0, 11)
+ );
+
+ assert_eq!(
+ inlay_snapshot.clip_point(InlayPoint::new(0, 12), Bias::Left),
+ InlayPoint::new(0, 11)
+ );
+ assert_eq!(
+ inlay_snapshot.clip_point(InlayPoint::new(0, 12), Bias::Right),
+ InlayPoint::new(0, 17)
+ );
+
+ assert_eq!(
+ inlay_snapshot.clip_point(InlayPoint::new(0, 13), Bias::Left),
+ InlayPoint::new(0, 11)
+ );
+ assert_eq!(
+ inlay_snapshot.clip_point(InlayPoint::new(0, 13), Bias::Right),
+ InlayPoint::new(0, 17)
+ );
+
+ assert_eq!(
+ inlay_snapshot.clip_point(InlayPoint::new(0, 14), Bias::Left),
+ InlayPoint::new(0, 11)
+ );
+ assert_eq!(
+ inlay_snapshot.clip_point(InlayPoint::new(0, 14), Bias::Right),
+ InlayPoint::new(0, 17)
+ );
+
+ assert_eq!(
+ inlay_snapshot.clip_point(InlayPoint::new(0, 15), Bias::Left),
+ InlayPoint::new(0, 11)
+ );
+ assert_eq!(
+ inlay_snapshot.clip_point(InlayPoint::new(0, 15), Bias::Right),
+ InlayPoint::new(0, 17)
+ );
+
+ assert_eq!(
+ inlay_snapshot.clip_point(InlayPoint::new(0, 16), Bias::Left),
+ InlayPoint::new(0, 11)
+ );
+ assert_eq!(
+ inlay_snapshot.clip_point(InlayPoint::new(0, 16), Bias::Right),
+ InlayPoint::new(0, 17)
+ );
+
+ assert_eq!(
+ inlay_snapshot.clip_point(InlayPoint::new(0, 17), Bias::Left),
+ InlayPoint::new(0, 17)
+ );
+ assert_eq!(
+ inlay_snapshot.clip_point(InlayPoint::new(0, 17), Bias::Right),
+ InlayPoint::new(0, 17)
+ );
+
+ assert_eq!(
+ inlay_snapshot.clip_point(InlayPoint::new(0, 18), Bias::Left),
+ InlayPoint::new(0, 18)
+ );
+ assert_eq!(
+ inlay_snapshot.clip_point(InlayPoint::new(0, 18), Bias::Right),
+ InlayPoint::new(0, 18)
+ );
+
+ // The inlays can be manually removed.
+ let (inlay_snapshot, _) = inlay_map
+ .splice::<String>(inlay_map.inlays_by_id.keys().copied().collect(), Vec::new());
+ assert_eq!(inlay_snapshot.text(), "abxJKLyDzefghi");
+ }
+
+ #[gpui::test]
+ fn test_inlay_buffer_rows(cx: &mut AppContext) {
+ let buffer = MultiBuffer::build_simple("abc\ndef\nghi", cx);
+ let (mut inlay_map, inlay_snapshot) = InlayMap::new(buffer.read(cx).snapshot(cx));
+ assert_eq!(inlay_snapshot.text(), "abc\ndef\nghi");
+ let mut next_inlay_id = 0;
+
+ let (inlay_snapshot, _) = inlay_map.splice(
+ Vec::new(),
+ vec![
+ (
+ InlayId::Hint(post_inc(&mut next_inlay_id)),
+ InlayProperties {
+ position: buffer.read(cx).snapshot(cx).anchor_before(0),
+ text: "|123|\n",
+ },
+ ),
+ (
+ InlayId::Hint(post_inc(&mut next_inlay_id)),
+ InlayProperties {
+ position: buffer.read(cx).snapshot(cx).anchor_before(4),
+ text: "|456|",
+ },
+ ),
+ (
+ InlayId::Suggestion(post_inc(&mut next_inlay_id)),
+ InlayProperties {
+ position: buffer.read(cx).snapshot(cx).anchor_before(7),
+ text: "\n|567|\n",
+ },
+ ),
+ ],
+ );
+ assert_eq!(inlay_snapshot.text(), "|123|\nabc\n|456|def\n|567|\n\nghi");
+ assert_eq!(
+ inlay_snapshot.buffer_rows(0).collect::<Vec<_>>(),
+ vec![Some(0), None, Some(1), None, None, Some(2)]
+ );
+ }
+
+ #[gpui::test(iterations = 100)]
+ fn test_random_inlays(cx: &mut AppContext, mut rng: StdRng) {
+ init_test(cx);
+
+ let operations = env::var("OPERATIONS")
+ .map(|i| i.parse().expect("invalid `OPERATIONS` variable"))
+ .unwrap_or(10);
+
+ let len = rng.gen_range(0..30);
+ let buffer = if rng.gen() {
+ let text = util::RandomCharIter::new(&mut rng)
+ .take(len)
+ .collect::<String>();
+ MultiBuffer::build_simple(&text, cx)
+ } else {
+ MultiBuffer::build_random(&mut rng, cx)
+ };
+ let mut buffer_snapshot = buffer.read(cx).snapshot(cx);
+ let mut next_inlay_id = 0;
+ log::info!("buffer text: {:?}", buffer_snapshot.text());
+
+ let mut highlights = TreeMap::default();
+ let highlight_count = rng.gen_range(0_usize..10);
+ let mut highlight_ranges = (0..highlight_count)
+ .map(|_| buffer_snapshot.random_byte_range(0, &mut rng))
+ .collect::<Vec<_>>();
+ highlight_ranges.sort_by_key(|range| (range.start, Reverse(range.end)));
+ log::info!("highlighting ranges {:?}", highlight_ranges);
+ let highlight_ranges = highlight_ranges
+ .into_iter()
+ .map(|range| {
+ buffer_snapshot.anchor_before(range.start)..buffer_snapshot.anchor_after(range.end)
+ })
+ .collect::<Vec<_>>();
+
+ highlights.insert(
+ Some(TypeId::of::<()>()),
+ Arc::new((HighlightStyle::default(), highlight_ranges)),
+ );
+
+ let (mut inlay_map, mut inlay_snapshot) = InlayMap::new(buffer_snapshot.clone());
+ for _ in 0..operations {
+ let mut inlay_edits = Patch::default();
+
+ let mut prev_inlay_text = inlay_snapshot.text();
+ let mut buffer_edits = Vec::new();
+ match rng.gen_range(0..=100) {
+ 0..=50 => {
+ let (snapshot, edits) = inlay_map.randomly_mutate(&mut next_inlay_id, &mut rng);
+ log::info!("mutated text: {:?}", snapshot.text());
+ inlay_edits = Patch::new(edits);
+ }
+ _ => buffer.update(cx, |buffer, cx| {
+ let subscription = buffer.subscribe();
+ let edit_count = rng.gen_range(1..=5);
+ buffer.randomly_mutate(&mut rng, edit_count, cx);
+ buffer_snapshot = buffer.snapshot(cx);
+ let edits = subscription.consume().into_inner();
+ log::info!("editing {:?}", edits);
+ buffer_edits.extend(edits);
+ }),
+ };
+
+ let (new_inlay_snapshot, new_inlay_edits) =
+ inlay_map.sync(buffer_snapshot.clone(), buffer_edits);
+ inlay_snapshot = new_inlay_snapshot;
+ inlay_edits = inlay_edits.compose(new_inlay_edits);
+
+ log::info!("buffer text: {:?}", buffer_snapshot.text());
+ log::info!("inlay text: {:?}", inlay_snapshot.text());
+
+ let inlays = inlay_map
+ .inlays
+ .iter()
+ .filter(|inlay| inlay.position.is_valid(&buffer_snapshot))
+ .map(|inlay| {
+ let offset = inlay.position.to_offset(&buffer_snapshot);
+ (offset, inlay.clone())
+ })
+ .collect::<Vec<_>>();
+ let mut expected_text = Rope::from(buffer_snapshot.text().as_str());
+ for (offset, inlay) in inlays.into_iter().rev() {
+ expected_text.replace(offset..offset, &inlay.text.to_string());
+ }
+ assert_eq!(inlay_snapshot.text(), expected_text.to_string());
+
+ let expected_buffer_rows = inlay_snapshot.buffer_rows(0).collect::<Vec<_>>();
+ assert_eq!(
+ expected_buffer_rows.len() as u32,
+ expected_text.max_point().row + 1
+ );
+ for row_start in 0..expected_buffer_rows.len() {
+ assert_eq!(
+ inlay_snapshot
+ .buffer_rows(row_start as u32)
+ .collect::<Vec<_>>(),
+ &expected_buffer_rows[row_start..],
+ "incorrect buffer rows starting at {}",
+ row_start
+ );
+ }
+
+ for _ in 0..5 {
+ let mut end = rng.gen_range(0..=inlay_snapshot.len().0);
+ end = expected_text.clip_offset(end, Bias::Right);
+ let mut start = rng.gen_range(0..=end);
+ start = expected_text.clip_offset(start, Bias::Right);
+
+ let actual_text = inlay_snapshot
+ .chunks(
+ InlayOffset(start)..InlayOffset(end),
+ false,
+ Some(&highlights),
+ None,
+ None,
+ )
+ .map(|chunk| chunk.text)
+ .collect::<String>();
+ assert_eq!(
+ actual_text,
+ expected_text.slice(start..end).to_string(),
+ "incorrect text in range {:?}",
+ start..end
+ );
+
+ assert_eq!(
+ inlay_snapshot.text_summary_for_range(InlayOffset(start)..InlayOffset(end)),
+ expected_text.slice(start..end).summary()
+ );
+ }
+
+ for edit in inlay_edits {
+ prev_inlay_text.replace_range(
+ edit.new.start.0..edit.new.start.0 + edit.old_len().0,
+ &inlay_snapshot.text()[edit.new.start.0..edit.new.end.0],
+ );
+ }
+ assert_eq!(prev_inlay_text, inlay_snapshot.text());
+
+ assert_eq!(expected_text.max_point(), inlay_snapshot.max_point().0);
+ assert_eq!(expected_text.len(), inlay_snapshot.len().0);
+
+ let mut buffer_point = Point::default();
+ let mut inlay_point = inlay_snapshot.to_inlay_point(buffer_point);
+ let mut buffer_chars = buffer_snapshot.chars_at(0);
+ loop {
+ // Ensure conversion from buffer coordinates to inlay coordinates
+ // is consistent.
+ let buffer_offset = buffer_snapshot.point_to_offset(buffer_point);
+ assert_eq!(
+ inlay_snapshot.to_point(inlay_snapshot.to_inlay_offset(buffer_offset)),
+ inlay_point
+ );
+
+ // No matter which bias we clip an inlay point with, it doesn't move
+ // because it was constructed from a buffer point.
+ assert_eq!(
+ inlay_snapshot.clip_point(inlay_point, Bias::Left),
+ inlay_point,
+ "invalid inlay point for buffer point {:?} when clipped left",
+ buffer_point
+ );
+ assert_eq!(
+ inlay_snapshot.clip_point(inlay_point, Bias::Right),
+ inlay_point,
+ "invalid inlay point for buffer point {:?} when clipped right",
+ buffer_point
+ );
+
+ if let Some(ch) = buffer_chars.next() {
+ if ch == '\n' {
+ buffer_point += Point::new(1, 0);
+ } else {
+ buffer_point += Point::new(0, ch.len_utf8() as u32);
+ }
+
+ // Ensure that moving forward in the buffer always moves the inlay point forward as well.
+ let new_inlay_point = inlay_snapshot.to_inlay_point(buffer_point);
+ assert!(new_inlay_point > inlay_point);
+ inlay_point = new_inlay_point;
+ } else {
+ break;
+ }
+ }
+
+ let mut inlay_point = InlayPoint::default();
+ let mut inlay_offset = InlayOffset::default();
+ for ch in expected_text.chars() {
+ assert_eq!(
+ inlay_snapshot.to_offset(inlay_point),
+ inlay_offset,
+ "invalid to_offset({:?})",
+ inlay_point
+ );
+ assert_eq!(
+ inlay_snapshot.to_point(inlay_offset),
+ inlay_point,
+ "invalid to_point({:?})",
+ inlay_offset
+ );
+
+ let mut bytes = [0; 4];
+ for byte in ch.encode_utf8(&mut bytes).as_bytes() {
+ inlay_offset.0 += 1;
+ if *byte == b'\n' {
+ inlay_point.0 += Point::new(1, 0);
+ } else {
+ inlay_point.0 += Point::new(0, 1);
+ }
+
+ let clipped_left_point = inlay_snapshot.clip_point(inlay_point, Bias::Left);
+ let clipped_right_point = inlay_snapshot.clip_point(inlay_point, Bias::Right);
+ assert!(
+ clipped_left_point <= clipped_right_point,
+ "inlay point {:?} when clipped left is greater than when clipped right ({:?} > {:?})",
+ inlay_point,
+ clipped_left_point,
+ clipped_right_point
+ );
+
+ // Ensure the clipped points are at valid text locations.
+ assert_eq!(
+ clipped_left_point.0,
+ expected_text.clip_point(clipped_left_point.0, Bias::Left)
+ );
+ assert_eq!(
+ clipped_right_point.0,
+ expected_text.clip_point(clipped_right_point.0, Bias::Right)
+ );
+
+ // Ensure the clipped points never overshoot the end of the map.
+ assert!(clipped_left_point <= inlay_snapshot.max_point());
+ assert!(clipped_right_point <= inlay_snapshot.max_point());
+
+ // Ensure the clipped points are at valid buffer locations.
+ assert_eq!(
+ inlay_snapshot
+ .to_inlay_point(inlay_snapshot.to_buffer_point(clipped_left_point)),
+ clipped_left_point,
+ "to_buffer_point({:?}) = {:?}",
+ clipped_left_point,
+ inlay_snapshot.to_buffer_point(clipped_left_point),
+ );
+ assert_eq!(
+ inlay_snapshot
+ .to_inlay_point(inlay_snapshot.to_buffer_point(clipped_right_point)),
+ clipped_right_point,
+ "to_buffer_point({:?}) = {:?}",
+ clipped_right_point,
+ inlay_snapshot.to_buffer_point(clipped_right_point),
+ );
+ }
+ }
+ }
+ }
+
+ fn init_test(cx: &mut AppContext) {
+ cx.set_global(SettingsStore::test(cx));
+ theme::init((), cx);
+ }
+}
@@ -1,871 +0,0 @@
-use super::{
- fold_map::{FoldBufferRows, FoldChunks, FoldEdit, FoldOffset, FoldPoint, FoldSnapshot},
- TextHighlights,
-};
-use crate::{MultiBufferSnapshot, ToPoint};
-use gpui::fonts::HighlightStyle;
-use language::{Bias, Chunk, Edit, Patch, Point, Rope, TextSummary};
-use parking_lot::Mutex;
-use std::{
- cmp,
- ops::{Add, AddAssign, Range, Sub},
-};
-use util::post_inc;
-
-pub type SuggestionEdit = Edit<SuggestionOffset>;
-
-#[derive(Copy, Clone, Debug, Default, Eq, Ord, PartialOrd, PartialEq)]
-pub struct SuggestionOffset(pub usize);
-
-impl Add for SuggestionOffset {
- type Output = Self;
-
- fn add(self, rhs: Self) -> Self::Output {
- Self(self.0 + rhs.0)
- }
-}
-
-impl Sub for SuggestionOffset {
- type Output = Self;
-
- fn sub(self, rhs: Self) -> Self::Output {
- Self(self.0 - rhs.0)
- }
-}
-
-impl AddAssign for SuggestionOffset {
- fn add_assign(&mut self, rhs: Self) {
- self.0 += rhs.0;
- }
-}
-
-#[derive(Copy, Clone, Debug, Default, Eq, Ord, PartialOrd, PartialEq)]
-pub struct SuggestionPoint(pub Point);
-
-impl SuggestionPoint {
- pub fn new(row: u32, column: u32) -> Self {
- Self(Point::new(row, column))
- }
-
- pub fn row(self) -> u32 {
- self.0.row
- }
-
- pub fn column(self) -> u32 {
- self.0.column
- }
-}
-
-#[derive(Clone, Debug)]
-pub struct Suggestion<T> {
- pub position: T,
- pub text: Rope,
-}
-
-pub struct SuggestionMap(Mutex<SuggestionSnapshot>);
-
-impl SuggestionMap {
- pub fn new(fold_snapshot: FoldSnapshot) -> (Self, SuggestionSnapshot) {
- let snapshot = SuggestionSnapshot {
- fold_snapshot,
- suggestion: None,
- version: 0,
- };
- (Self(Mutex::new(snapshot.clone())), snapshot)
- }
-
- pub fn replace<T>(
- &self,
- new_suggestion: Option<Suggestion<T>>,
- fold_snapshot: FoldSnapshot,
- fold_edits: Vec<FoldEdit>,
- ) -> (
- SuggestionSnapshot,
- Vec<SuggestionEdit>,
- Option<Suggestion<FoldOffset>>,
- )
- where
- T: ToPoint,
- {
- let new_suggestion = new_suggestion.map(|new_suggestion| {
- let buffer_point = new_suggestion
- .position
- .to_point(fold_snapshot.buffer_snapshot());
- let fold_point = fold_snapshot.to_fold_point(buffer_point, Bias::Left);
- let fold_offset = fold_point.to_offset(&fold_snapshot);
- Suggestion {
- position: fold_offset,
- text: new_suggestion.text,
- }
- });
-
- let (_, edits) = self.sync(fold_snapshot, fold_edits);
- let mut snapshot = self.0.lock();
-
- let mut patch = Patch::new(edits);
- let old_suggestion = snapshot.suggestion.take();
- if let Some(suggestion) = &old_suggestion {
- patch = patch.compose([SuggestionEdit {
- old: SuggestionOffset(suggestion.position.0)
- ..SuggestionOffset(suggestion.position.0 + suggestion.text.len()),
- new: SuggestionOffset(suggestion.position.0)
- ..SuggestionOffset(suggestion.position.0),
- }]);
- }
-
- if let Some(suggestion) = new_suggestion.as_ref() {
- patch = patch.compose([SuggestionEdit {
- old: SuggestionOffset(suggestion.position.0)
- ..SuggestionOffset(suggestion.position.0),
- new: SuggestionOffset(suggestion.position.0)
- ..SuggestionOffset(suggestion.position.0 + suggestion.text.len()),
- }]);
- }
-
- snapshot.suggestion = new_suggestion;
- snapshot.version += 1;
- (snapshot.clone(), patch.into_inner(), old_suggestion)
- }
-
- pub fn sync(
- &self,
- fold_snapshot: FoldSnapshot,
- fold_edits: Vec<FoldEdit>,
- ) -> (SuggestionSnapshot, Vec<SuggestionEdit>) {
- let mut snapshot = self.0.lock();
-
- if snapshot.fold_snapshot.version != fold_snapshot.version {
- snapshot.version += 1;
- }
-
- let mut suggestion_edits = Vec::new();
-
- let mut suggestion_old_len = 0;
- let mut suggestion_new_len = 0;
- for fold_edit in fold_edits {
- let start = fold_edit.new.start;
- let end = FoldOffset(start.0 + fold_edit.old_len().0);
- if let Some(suggestion) = snapshot.suggestion.as_mut() {
- if end <= suggestion.position {
- suggestion.position.0 += fold_edit.new_len().0;
- suggestion.position.0 -= fold_edit.old_len().0;
- } else if start > suggestion.position {
- suggestion_old_len = suggestion.text.len();
- suggestion_new_len = suggestion_old_len;
- } else {
- suggestion_old_len = suggestion.text.len();
- snapshot.suggestion.take();
- suggestion_edits.push(SuggestionEdit {
- old: SuggestionOffset(fold_edit.old.start.0)
- ..SuggestionOffset(fold_edit.old.end.0 + suggestion_old_len),
- new: SuggestionOffset(fold_edit.new.start.0)
- ..SuggestionOffset(fold_edit.new.end.0),
- });
- continue;
- }
- }
-
- suggestion_edits.push(SuggestionEdit {
- old: SuggestionOffset(fold_edit.old.start.0 + suggestion_old_len)
- ..SuggestionOffset(fold_edit.old.end.0 + suggestion_old_len),
- new: SuggestionOffset(fold_edit.new.start.0 + suggestion_new_len)
- ..SuggestionOffset(fold_edit.new.end.0 + suggestion_new_len),
- });
- }
- snapshot.fold_snapshot = fold_snapshot;
-
- (snapshot.clone(), suggestion_edits)
- }
-
- pub fn has_suggestion(&self) -> bool {
- let snapshot = self.0.lock();
- snapshot.suggestion.is_some()
- }
-}
-
-#[derive(Clone)]
-pub struct SuggestionSnapshot {
- pub fold_snapshot: FoldSnapshot,
- pub suggestion: Option<Suggestion<FoldOffset>>,
- pub version: usize,
-}
-
-impl SuggestionSnapshot {
- pub fn buffer_snapshot(&self) -> &MultiBufferSnapshot {
- self.fold_snapshot.buffer_snapshot()
- }
-
- pub fn max_point(&self) -> SuggestionPoint {
- if let Some(suggestion) = self.suggestion.as_ref() {
- let suggestion_point = suggestion.position.to_point(&self.fold_snapshot);
- let mut max_point = suggestion_point.0;
- max_point += suggestion.text.max_point();
- max_point += self.fold_snapshot.max_point().0 - suggestion_point.0;
- SuggestionPoint(max_point)
- } else {
- SuggestionPoint(self.fold_snapshot.max_point().0)
- }
- }
-
- pub fn len(&self) -> SuggestionOffset {
- if let Some(suggestion) = self.suggestion.as_ref() {
- let mut len = suggestion.position.0;
- len += suggestion.text.len();
- len += self.fold_snapshot.len().0 - suggestion.position.0;
- SuggestionOffset(len)
- } else {
- SuggestionOffset(self.fold_snapshot.len().0)
- }
- }
-
- pub fn line_len(&self, row: u32) -> u32 {
- if let Some(suggestion) = &self.suggestion {
- let suggestion_start = suggestion.position.to_point(&self.fold_snapshot).0;
- let suggestion_end = suggestion_start + suggestion.text.max_point();
-
- if row < suggestion_start.row {
- self.fold_snapshot.line_len(row)
- } else if row > suggestion_end.row {
- self.fold_snapshot
- .line_len(suggestion_start.row + (row - suggestion_end.row))
- } else {
- let mut result = suggestion.text.line_len(row - suggestion_start.row);
- if row == suggestion_start.row {
- result += suggestion_start.column;
- }
- if row == suggestion_end.row {
- result +=
- self.fold_snapshot.line_len(suggestion_start.row) - suggestion_start.column;
- }
- result
- }
- } else {
- self.fold_snapshot.line_len(row)
- }
- }
-
- pub fn clip_point(&self, point: SuggestionPoint, bias: Bias) -> SuggestionPoint {
- if let Some(suggestion) = self.suggestion.as_ref() {
- let suggestion_start = suggestion.position.to_point(&self.fold_snapshot).0;
- let suggestion_end = suggestion_start + suggestion.text.max_point();
- if point.0 <= suggestion_start {
- SuggestionPoint(self.fold_snapshot.clip_point(FoldPoint(point.0), bias).0)
- } else if point.0 > suggestion_end {
- let fold_point = self.fold_snapshot.clip_point(
- FoldPoint(suggestion_start + (point.0 - suggestion_end)),
- bias,
- );
- let suggestion_point = suggestion_end + (fold_point.0 - suggestion_start);
- if bias == Bias::Left && suggestion_point == suggestion_end {
- SuggestionPoint(suggestion_start)
- } else {
- SuggestionPoint(suggestion_point)
- }
- } else if bias == Bias::Left || suggestion_start == self.fold_snapshot.max_point().0 {
- SuggestionPoint(suggestion_start)
- } else {
- let fold_point = if self.fold_snapshot.line_len(suggestion_start.row)
- > suggestion_start.column
- {
- FoldPoint(suggestion_start + Point::new(0, 1))
- } else {
- FoldPoint(suggestion_start + Point::new(1, 0))
- };
- let clipped_fold_point = self.fold_snapshot.clip_point(fold_point, bias);
- SuggestionPoint(suggestion_end + (clipped_fold_point.0 - suggestion_start))
- }
- } else {
- SuggestionPoint(self.fold_snapshot.clip_point(FoldPoint(point.0), bias).0)
- }
- }
-
- pub fn to_offset(&self, point: SuggestionPoint) -> SuggestionOffset {
- if let Some(suggestion) = self.suggestion.as_ref() {
- let suggestion_start = suggestion.position.to_point(&self.fold_snapshot).0;
- let suggestion_end = suggestion_start + suggestion.text.max_point();
-
- if point.0 <= suggestion_start {
- SuggestionOffset(FoldPoint(point.0).to_offset(&self.fold_snapshot).0)
- } else if point.0 > suggestion_end {
- let fold_offset = FoldPoint(suggestion_start + (point.0 - suggestion_end))
- .to_offset(&self.fold_snapshot);
- SuggestionOffset(fold_offset.0 + suggestion.text.len())
- } else {
- let offset_in_suggestion =
- suggestion.text.point_to_offset(point.0 - suggestion_start);
- SuggestionOffset(suggestion.position.0 + offset_in_suggestion)
- }
- } else {
- SuggestionOffset(FoldPoint(point.0).to_offset(&self.fold_snapshot).0)
- }
- }
-
- pub fn to_point(&self, offset: SuggestionOffset) -> SuggestionPoint {
- if let Some(suggestion) = self.suggestion.as_ref() {
- let suggestion_point_start = suggestion.position.to_point(&self.fold_snapshot).0;
- if offset.0 <= suggestion.position.0 {
- SuggestionPoint(FoldOffset(offset.0).to_point(&self.fold_snapshot).0)
- } else if offset.0 > (suggestion.position.0 + suggestion.text.len()) {
- let fold_point = FoldOffset(offset.0 - suggestion.text.len())
- .to_point(&self.fold_snapshot)
- .0;
-
- SuggestionPoint(
- suggestion_point_start
- + suggestion.text.max_point()
- + (fold_point - suggestion_point_start),
- )
- } else {
- let point_in_suggestion = suggestion
- .text
- .offset_to_point(offset.0 - suggestion.position.0);
- SuggestionPoint(suggestion_point_start + point_in_suggestion)
- }
- } else {
- SuggestionPoint(FoldOffset(offset.0).to_point(&self.fold_snapshot).0)
- }
- }
-
- pub fn to_fold_point(&self, point: SuggestionPoint) -> FoldPoint {
- if let Some(suggestion) = self.suggestion.as_ref() {
- let suggestion_start = suggestion.position.to_point(&self.fold_snapshot).0;
- let suggestion_end = suggestion_start + suggestion.text.max_point();
-
- if point.0 <= suggestion_start {
- FoldPoint(point.0)
- } else if point.0 > suggestion_end {
- FoldPoint(suggestion_start + (point.0 - suggestion_end))
- } else {
- FoldPoint(suggestion_start)
- }
- } else {
- FoldPoint(point.0)
- }
- }
-
- pub fn to_suggestion_point(&self, point: FoldPoint) -> SuggestionPoint {
- if let Some(suggestion) = self.suggestion.as_ref() {
- let suggestion_start = suggestion.position.to_point(&self.fold_snapshot).0;
-
- if point.0 <= suggestion_start {
- SuggestionPoint(point.0)
- } else {
- let suggestion_end = suggestion_start + suggestion.text.max_point();
- SuggestionPoint(suggestion_end + (point.0 - suggestion_start))
- }
- } else {
- SuggestionPoint(point.0)
- }
- }
-
- pub fn text_summary_for_range(&self, range: Range<SuggestionPoint>) -> TextSummary {
- if let Some(suggestion) = self.suggestion.as_ref() {
- let suggestion_start = suggestion.position.to_point(&self.fold_snapshot).0;
- let suggestion_end = suggestion_start + suggestion.text.max_point();
- let mut summary = TextSummary::default();
-
- let prefix_range =
- cmp::min(range.start.0, suggestion_start)..cmp::min(range.end.0, suggestion_start);
- if prefix_range.start < prefix_range.end {
- summary += self.fold_snapshot.text_summary_for_range(
- FoldPoint(prefix_range.start)..FoldPoint(prefix_range.end),
- );
- }
-
- let suggestion_range =
- cmp::max(range.start.0, suggestion_start)..cmp::min(range.end.0, suggestion_end);
- if suggestion_range.start < suggestion_range.end {
- let point_range = suggestion_range.start - suggestion_start
- ..suggestion_range.end - suggestion_start;
- let offset_range = suggestion.text.point_to_offset(point_range.start)
- ..suggestion.text.point_to_offset(point_range.end);
- summary += suggestion
- .text
- .cursor(offset_range.start)
- .summary::<TextSummary>(offset_range.end);
- }
-
- let suffix_range = cmp::max(range.start.0, suggestion_end)..range.end.0;
- if suffix_range.start < suffix_range.end {
- let start = suggestion_start + (suffix_range.start - suggestion_end);
- let end = suggestion_start + (suffix_range.end - suggestion_end);
- summary += self
- .fold_snapshot
- .text_summary_for_range(FoldPoint(start)..FoldPoint(end));
- }
-
- summary
- } else {
- self.fold_snapshot
- .text_summary_for_range(FoldPoint(range.start.0)..FoldPoint(range.end.0))
- }
- }
-
- pub fn chars_at(&self, start: SuggestionPoint) -> impl '_ + Iterator<Item = char> {
- let start = self.to_offset(start);
- self.chunks(start..self.len(), false, None, None)
- .flat_map(|chunk| chunk.text.chars())
- }
-
- pub fn chunks<'a>(
- &'a self,
- range: Range<SuggestionOffset>,
- language_aware: bool,
- text_highlights: Option<&'a TextHighlights>,
- suggestion_highlight: Option<HighlightStyle>,
- ) -> SuggestionChunks<'a> {
- if let Some(suggestion) = self.suggestion.as_ref() {
- let suggestion_range =
- suggestion.position.0..suggestion.position.0 + suggestion.text.len();
-
- let prefix_chunks = if range.start.0 < suggestion_range.start {
- Some(self.fold_snapshot.chunks(
- FoldOffset(range.start.0)
- ..cmp::min(FoldOffset(suggestion_range.start), FoldOffset(range.end.0)),
- language_aware,
- text_highlights,
- ))
- } else {
- None
- };
-
- let clipped_suggestion_range = cmp::max(range.start.0, suggestion_range.start)
- ..cmp::min(range.end.0, suggestion_range.end);
- let suggestion_chunks = if clipped_suggestion_range.start < clipped_suggestion_range.end
- {
- let start = clipped_suggestion_range.start - suggestion_range.start;
- let end = clipped_suggestion_range.end - suggestion_range.start;
- Some(suggestion.text.chunks_in_range(start..end))
- } else {
- None
- };
-
- let suffix_chunks = if range.end.0 > suggestion_range.end {
- let start = cmp::max(suggestion_range.end, range.start.0) - suggestion_range.len();
- let end = range.end.0 - suggestion_range.len();
- Some(self.fold_snapshot.chunks(
- FoldOffset(start)..FoldOffset(end),
- language_aware,
- text_highlights,
- ))
- } else {
- None
- };
-
- SuggestionChunks {
- prefix_chunks,
- suggestion_chunks,
- suffix_chunks,
- highlight_style: suggestion_highlight,
- }
- } else {
- SuggestionChunks {
- prefix_chunks: Some(self.fold_snapshot.chunks(
- FoldOffset(range.start.0)..FoldOffset(range.end.0),
- language_aware,
- text_highlights,
- )),
- suggestion_chunks: None,
- suffix_chunks: None,
- highlight_style: None,
- }
- }
- }
-
- pub fn buffer_rows<'a>(&'a self, row: u32) -> SuggestionBufferRows<'a> {
- let suggestion_range = if let Some(suggestion) = self.suggestion.as_ref() {
- let start = suggestion.position.to_point(&self.fold_snapshot).0;
- let end = start + suggestion.text.max_point();
- start.row..end.row
- } else {
- u32::MAX..u32::MAX
- };
-
- let fold_buffer_rows = if row <= suggestion_range.start {
- self.fold_snapshot.buffer_rows(row)
- } else if row > suggestion_range.end {
- self.fold_snapshot
- .buffer_rows(row - (suggestion_range.end - suggestion_range.start))
- } else {
- let mut rows = self.fold_snapshot.buffer_rows(suggestion_range.start);
- rows.next();
- rows
- };
-
- SuggestionBufferRows {
- current_row: row,
- suggestion_row_start: suggestion_range.start,
- suggestion_row_end: suggestion_range.end,
- fold_buffer_rows,
- }
- }
-
- #[cfg(test)]
- pub fn text(&self) -> String {
- self.chunks(Default::default()..self.len(), false, None, None)
- .map(|chunk| chunk.text)
- .collect()
- }
-}
-
-pub struct SuggestionChunks<'a> {
- prefix_chunks: Option<FoldChunks<'a>>,
- suggestion_chunks: Option<text::Chunks<'a>>,
- suffix_chunks: Option<FoldChunks<'a>>,
- highlight_style: Option<HighlightStyle>,
-}
-
-impl<'a> Iterator for SuggestionChunks<'a> {
- type Item = Chunk<'a>;
-
- fn next(&mut self) -> Option<Self::Item> {
- if let Some(chunks) = self.prefix_chunks.as_mut() {
- if let Some(chunk) = chunks.next() {
- return Some(chunk);
- } else {
- self.prefix_chunks = None;
- }
- }
-
- if let Some(chunks) = self.suggestion_chunks.as_mut() {
- if let Some(chunk) = chunks.next() {
- return Some(Chunk {
- text: chunk,
- highlight_style: self.highlight_style,
- ..Default::default()
- });
- } else {
- self.suggestion_chunks = None;
- }
- }
-
- if let Some(chunks) = self.suffix_chunks.as_mut() {
- if let Some(chunk) = chunks.next() {
- return Some(chunk);
- } else {
- self.suffix_chunks = None;
- }
- }
-
- None
- }
-}
-
-#[derive(Clone)]
-pub struct SuggestionBufferRows<'a> {
- current_row: u32,
- suggestion_row_start: u32,
- suggestion_row_end: u32,
- fold_buffer_rows: FoldBufferRows<'a>,
-}
-
-impl<'a> Iterator for SuggestionBufferRows<'a> {
- type Item = Option<u32>;
-
- fn next(&mut self) -> Option<Self::Item> {
- let row = post_inc(&mut self.current_row);
- if row <= self.suggestion_row_start || row > self.suggestion_row_end {
- self.fold_buffer_rows.next()
- } else {
- Some(None)
- }
- }
-}
-
-#[cfg(test)]
-mod tests {
- use super::*;
- use crate::{display_map::fold_map::FoldMap, MultiBuffer};
- use gpui::AppContext;
- use rand::{prelude::StdRng, Rng};
- use settings::SettingsStore;
- use std::{
- env,
- ops::{Bound, RangeBounds},
- };
-
- #[gpui::test]
- fn test_basic(cx: &mut AppContext) {
- let buffer = MultiBuffer::build_simple("abcdefghi", cx);
- let buffer_edits = buffer.update(cx, |buffer, _| buffer.subscribe());
- let (mut fold_map, fold_snapshot) = FoldMap::new(buffer.read(cx).snapshot(cx));
- let (suggestion_map, suggestion_snapshot) = SuggestionMap::new(fold_snapshot.clone());
- assert_eq!(suggestion_snapshot.text(), "abcdefghi");
-
- let (suggestion_snapshot, _, _) = suggestion_map.replace(
- Some(Suggestion {
- position: 3,
- text: "123\n456".into(),
- }),
- fold_snapshot,
- Default::default(),
- );
- assert_eq!(suggestion_snapshot.text(), "abc123\n456defghi");
-
- buffer.update(cx, |buffer, cx| {
- buffer.edit(
- [(0..0, "ABC"), (3..3, "DEF"), (4..4, "GHI"), (9..9, "JKL")],
- None,
- cx,
- )
- });
- let (fold_snapshot, fold_edits) = fold_map.read(
- buffer.read(cx).snapshot(cx),
- buffer_edits.consume().into_inner(),
- );
- let (suggestion_snapshot, _) = suggestion_map.sync(fold_snapshot.clone(), fold_edits);
- assert_eq!(suggestion_snapshot.text(), "ABCabcDEF123\n456dGHIefghiJKL");
-
- let (mut fold_map_writer, _, _) =
- fold_map.write(buffer.read(cx).snapshot(cx), Default::default());
- let (fold_snapshot, fold_edits) = fold_map_writer.fold([0..3]);
- let (suggestion_snapshot, _) = suggestion_map.sync(fold_snapshot, fold_edits);
- assert_eq!(suggestion_snapshot.text(), "β―abcDEF123\n456dGHIefghiJKL");
-
- let (mut fold_map_writer, _, _) =
- fold_map.write(buffer.read(cx).snapshot(cx), Default::default());
- let (fold_snapshot, fold_edits) = fold_map_writer.fold([6..10]);
- let (suggestion_snapshot, _) = suggestion_map.sync(fold_snapshot, fold_edits);
- assert_eq!(suggestion_snapshot.text(), "β―abcβ―GHIefghiJKL");
- }
-
- #[gpui::test(iterations = 100)]
- fn test_random_suggestions(cx: &mut AppContext, mut rng: StdRng) {
- init_test(cx);
-
- let operations = env::var("OPERATIONS")
- .map(|i| i.parse().expect("invalid `OPERATIONS` variable"))
- .unwrap_or(10);
-
- let len = rng.gen_range(0..30);
- let buffer = if rng.gen() {
- let text = util::RandomCharIter::new(&mut rng)
- .take(len)
- .collect::<String>();
- MultiBuffer::build_simple(&text, cx)
- } else {
- MultiBuffer::build_random(&mut rng, cx)
- };
- let mut buffer_snapshot = buffer.read(cx).snapshot(cx);
- log::info!("buffer text: {:?}", buffer_snapshot.text());
-
- let (mut fold_map, mut fold_snapshot) = FoldMap::new(buffer_snapshot.clone());
- let (suggestion_map, mut suggestion_snapshot) = SuggestionMap::new(fold_snapshot.clone());
-
- for _ in 0..operations {
- let mut suggestion_edits = Patch::default();
-
- let mut prev_suggestion_text = suggestion_snapshot.text();
- let mut buffer_edits = Vec::new();
- match rng.gen_range(0..=100) {
- 0..=29 => {
- let (_, edits) = suggestion_map.randomly_mutate(&mut rng);
- suggestion_edits = suggestion_edits.compose(edits);
- }
- 30..=59 => {
- for (new_fold_snapshot, fold_edits) in fold_map.randomly_mutate(&mut rng) {
- fold_snapshot = new_fold_snapshot;
- let (_, edits) = suggestion_map.sync(fold_snapshot.clone(), fold_edits);
- suggestion_edits = suggestion_edits.compose(edits);
- }
- }
- _ => buffer.update(cx, |buffer, cx| {
- let subscription = buffer.subscribe();
- let edit_count = rng.gen_range(1..=5);
- buffer.randomly_mutate(&mut rng, edit_count, cx);
- buffer_snapshot = buffer.snapshot(cx);
- let edits = subscription.consume().into_inner();
- log::info!("editing {:?}", edits);
- buffer_edits.extend(edits);
- }),
- };
-
- let (new_fold_snapshot, fold_edits) =
- fold_map.read(buffer_snapshot.clone(), buffer_edits);
- fold_snapshot = new_fold_snapshot;
- let (new_suggestion_snapshot, edits) =
- suggestion_map.sync(fold_snapshot.clone(), fold_edits);
- suggestion_snapshot = new_suggestion_snapshot;
- suggestion_edits = suggestion_edits.compose(edits);
-
- log::info!("buffer text: {:?}", buffer_snapshot.text());
- log::info!("folds text: {:?}", fold_snapshot.text());
- log::info!("suggestions text: {:?}", suggestion_snapshot.text());
-
- let mut expected_text = Rope::from(fold_snapshot.text().as_str());
- let mut expected_buffer_rows = fold_snapshot.buffer_rows(0).collect::<Vec<_>>();
- if let Some(suggestion) = suggestion_snapshot.suggestion.as_ref() {
- expected_text.replace(
- suggestion.position.0..suggestion.position.0,
- &suggestion.text.to_string(),
- );
- let suggestion_start = suggestion.position.to_point(&fold_snapshot).0;
- let suggestion_end = suggestion_start + suggestion.text.max_point();
- expected_buffer_rows.splice(
- (suggestion_start.row + 1) as usize..(suggestion_start.row + 1) as usize,
- (0..suggestion_end.row - suggestion_start.row).map(|_| None),
- );
- }
- assert_eq!(suggestion_snapshot.text(), expected_text.to_string());
- for row_start in 0..expected_buffer_rows.len() {
- assert_eq!(
- suggestion_snapshot
- .buffer_rows(row_start as u32)
- .collect::<Vec<_>>(),
- &expected_buffer_rows[row_start..],
- "incorrect buffer rows starting at {}",
- row_start
- );
- }
-
- for _ in 0..5 {
- let mut end = rng.gen_range(0..=suggestion_snapshot.len().0);
- end = expected_text.clip_offset(end, Bias::Right);
- let mut start = rng.gen_range(0..=end);
- start = expected_text.clip_offset(start, Bias::Right);
-
- let actual_text = suggestion_snapshot
- .chunks(
- SuggestionOffset(start)..SuggestionOffset(end),
- false,
- None,
- None,
- )
- .map(|chunk| chunk.text)
- .collect::<String>();
- assert_eq!(
- actual_text,
- expected_text.slice(start..end).to_string(),
- "incorrect text in range {:?}",
- start..end
- );
-
- let start_point = SuggestionPoint(expected_text.offset_to_point(start));
- let end_point = SuggestionPoint(expected_text.offset_to_point(end));
- assert_eq!(
- suggestion_snapshot.text_summary_for_range(start_point..end_point),
- expected_text.slice(start..end).summary()
- );
- }
-
- for edit in suggestion_edits.into_inner() {
- prev_suggestion_text.replace_range(
- edit.new.start.0..edit.new.start.0 + edit.old_len().0,
- &suggestion_snapshot.text()[edit.new.start.0..edit.new.end.0],
- );
- }
- assert_eq!(prev_suggestion_text, suggestion_snapshot.text());
-
- assert_eq!(expected_text.max_point(), suggestion_snapshot.max_point().0);
- assert_eq!(expected_text.len(), suggestion_snapshot.len().0);
-
- let mut suggestion_point = SuggestionPoint::default();
- let mut suggestion_offset = SuggestionOffset::default();
- for ch in expected_text.chars() {
- assert_eq!(
- suggestion_snapshot.to_offset(suggestion_point),
- suggestion_offset,
- "invalid to_offset({:?})",
- suggestion_point
- );
- assert_eq!(
- suggestion_snapshot.to_point(suggestion_offset),
- suggestion_point,
- "invalid to_point({:?})",
- suggestion_offset
- );
- assert_eq!(
- suggestion_snapshot
- .to_suggestion_point(suggestion_snapshot.to_fold_point(suggestion_point)),
- suggestion_snapshot.clip_point(suggestion_point, Bias::Left),
- );
-
- let mut bytes = [0; 4];
- for byte in ch.encode_utf8(&mut bytes).as_bytes() {
- suggestion_offset.0 += 1;
- if *byte == b'\n' {
- suggestion_point.0 += Point::new(1, 0);
- } else {
- suggestion_point.0 += Point::new(0, 1);
- }
-
- let clipped_left_point =
- suggestion_snapshot.clip_point(suggestion_point, Bias::Left);
- let clipped_right_point =
- suggestion_snapshot.clip_point(suggestion_point, Bias::Right);
- assert!(
- clipped_left_point <= clipped_right_point,
- "clipped left point {:?} is greater than clipped right point {:?}",
- clipped_left_point,
- clipped_right_point
- );
- assert_eq!(
- clipped_left_point.0,
- expected_text.clip_point(clipped_left_point.0, Bias::Left)
- );
- assert_eq!(
- clipped_right_point.0,
- expected_text.clip_point(clipped_right_point.0, Bias::Right)
- );
- assert!(clipped_left_point <= suggestion_snapshot.max_point());
- assert!(clipped_right_point <= suggestion_snapshot.max_point());
-
- if let Some(suggestion) = suggestion_snapshot.suggestion.as_ref() {
- let suggestion_start = suggestion.position.to_point(&fold_snapshot).0;
- let suggestion_end = suggestion_start + suggestion.text.max_point();
- let invalid_range = (
- Bound::Excluded(suggestion_start),
- Bound::Included(suggestion_end),
- );
- assert!(
- !invalid_range.contains(&clipped_left_point.0),
- "clipped left point {:?} is inside invalid suggestion range {:?}",
- clipped_left_point,
- invalid_range
- );
- assert!(
- !invalid_range.contains(&clipped_right_point.0),
- "clipped right point {:?} is inside invalid suggestion range {:?}",
- clipped_right_point,
- invalid_range
- );
- }
- }
- }
- }
- }
-
- fn init_test(cx: &mut AppContext) {
- cx.set_global(SettingsStore::test(cx));
- theme::init((), cx);
- }
-
- impl SuggestionMap {
- pub fn randomly_mutate(
- &self,
- rng: &mut impl Rng,
- ) -> (SuggestionSnapshot, Vec<SuggestionEdit>) {
- let fold_snapshot = self.0.lock().fold_snapshot.clone();
- let new_suggestion = if rng.gen_bool(0.3) {
- None
- } else {
- let index = rng.gen_range(0..=fold_snapshot.buffer_snapshot().len());
- let len = rng.gen_range(0..30);
- Some(Suggestion {
- position: index,
- text: util::RandomCharIter::new(rng)
- .take(len)
- .filter(|ch| *ch != '\r')
- .collect::<String>()
- .as_str()
- .into(),
- })
- };
-
- log::info!("replacing suggestion with {:?}", new_suggestion);
- let (snapshot, edits, _) =
- self.replace(new_suggestion, fold_snapshot, Default::default());
- (snapshot, edits)
- }
- }
-}
@@ -1,80 +1,76 @@
use super::{
- suggestion_map::{self, SuggestionChunks, SuggestionEdit, SuggestionPoint, SuggestionSnapshot},
+ fold_map::{self, FoldChunks, FoldEdit, FoldPoint, FoldSnapshot},
TextHighlights,
};
use crate::MultiBufferSnapshot;
use gpui::fonts::HighlightStyle;
use language::{Chunk, Point};
-use parking_lot::Mutex;
use std::{cmp, mem, num::NonZeroU32, ops::Range};
use sum_tree::Bias;
const MAX_EXPANSION_COLUMN: u32 = 256;
-pub struct TabMap(Mutex<TabSnapshot>);
+pub struct TabMap(TabSnapshot);
impl TabMap {
- pub fn new(input: SuggestionSnapshot, tab_size: NonZeroU32) -> (Self, TabSnapshot) {
+ pub fn new(fold_snapshot: FoldSnapshot, tab_size: NonZeroU32) -> (Self, TabSnapshot) {
let snapshot = TabSnapshot {
- suggestion_snapshot: input,
+ fold_snapshot,
tab_size,
max_expansion_column: MAX_EXPANSION_COLUMN,
version: 0,
};
- (Self(Mutex::new(snapshot.clone())), snapshot)
+ (Self(snapshot.clone()), snapshot)
}
#[cfg(test)]
- pub fn set_max_expansion_column(&self, column: u32) -> TabSnapshot {
- self.0.lock().max_expansion_column = column;
- self.0.lock().clone()
+ pub fn set_max_expansion_column(&mut self, column: u32) -> TabSnapshot {
+ self.0.max_expansion_column = column;
+ self.0.clone()
}
pub fn sync(
- &self,
- suggestion_snapshot: SuggestionSnapshot,
- mut suggestion_edits: Vec<SuggestionEdit>,
+ &mut self,
+ fold_snapshot: FoldSnapshot,
+ mut fold_edits: Vec<FoldEdit>,
tab_size: NonZeroU32,
) -> (TabSnapshot, Vec<TabEdit>) {
- let mut old_snapshot = self.0.lock();
+ let old_snapshot = &mut self.0;
let mut new_snapshot = TabSnapshot {
- suggestion_snapshot,
+ fold_snapshot,
tab_size,
max_expansion_column: old_snapshot.max_expansion_column,
version: old_snapshot.version,
};
- if old_snapshot.suggestion_snapshot.version != new_snapshot.suggestion_snapshot.version {
+ if old_snapshot.fold_snapshot.version != new_snapshot.fold_snapshot.version {
new_snapshot.version += 1;
}
- let mut tab_edits = Vec::with_capacity(suggestion_edits.len());
+ let mut tab_edits = Vec::with_capacity(fold_edits.len());
if old_snapshot.tab_size == new_snapshot.tab_size {
// Expand each edit to include the next tab on the same line as the edit,
// and any subsequent tabs on that line that moved across the tab expansion
// boundary.
- for suggestion_edit in &mut suggestion_edits {
- let old_end = old_snapshot
- .suggestion_snapshot
- .to_point(suggestion_edit.old.end);
- let old_end_row_successor_offset =
- old_snapshot.suggestion_snapshot.to_offset(cmp::min(
- SuggestionPoint::new(old_end.row() + 1, 0),
- old_snapshot.suggestion_snapshot.max_point(),
- ));
- let new_end = new_snapshot
- .suggestion_snapshot
- .to_point(suggestion_edit.new.end);
+ for fold_edit in &mut fold_edits {
+ let old_end = fold_edit.old.end.to_point(&old_snapshot.fold_snapshot);
+ let old_end_row_successor_offset = cmp::min(
+ FoldPoint::new(old_end.row() + 1, 0),
+ old_snapshot.fold_snapshot.max_point(),
+ )
+ .to_offset(&old_snapshot.fold_snapshot);
+ let new_end = fold_edit.new.end.to_point(&new_snapshot.fold_snapshot);
let mut offset_from_edit = 0;
let mut first_tab_offset = None;
let mut last_tab_with_changed_expansion_offset = None;
- 'outer: for chunk in old_snapshot.suggestion_snapshot.chunks(
- suggestion_edit.old.end..old_end_row_successor_offset,
+ 'outer: for chunk in old_snapshot.fold_snapshot.chunks(
+ fold_edit.old.end..old_end_row_successor_offset,
false,
None,
None,
+ None,
) {
for (ix, _) in chunk.text.match_indices('\t') {
let offset_from_edit = offset_from_edit + (ix as u32);
@@ -102,39 +98,31 @@ impl TabMap {
}
if let Some(offset) = last_tab_with_changed_expansion_offset.or(first_tab_offset) {
- suggestion_edit.old.end.0 += offset as usize + 1;
- suggestion_edit.new.end.0 += offset as usize + 1;
+ fold_edit.old.end.0 += offset as usize + 1;
+ fold_edit.new.end.0 += offset as usize + 1;
}
}
// Combine any edits that overlap due to the expansion.
let mut ix = 1;
- while ix < suggestion_edits.len() {
- let (prev_edits, next_edits) = suggestion_edits.split_at_mut(ix);
+ while ix < fold_edits.len() {
+ let (prev_edits, next_edits) = fold_edits.split_at_mut(ix);
let prev_edit = prev_edits.last_mut().unwrap();
let edit = &next_edits[0];
if prev_edit.old.end >= edit.old.start {
prev_edit.old.end = edit.old.end;
prev_edit.new.end = edit.new.end;
- suggestion_edits.remove(ix);
+ fold_edits.remove(ix);
} else {
ix += 1;
}
}
- for suggestion_edit in suggestion_edits {
- let old_start = old_snapshot
- .suggestion_snapshot
- .to_point(suggestion_edit.old.start);
- let old_end = old_snapshot
- .suggestion_snapshot
- .to_point(suggestion_edit.old.end);
- let new_start = new_snapshot
- .suggestion_snapshot
- .to_point(suggestion_edit.new.start);
- let new_end = new_snapshot
- .suggestion_snapshot
- .to_point(suggestion_edit.new.end);
+ for fold_edit in fold_edits {
+ let old_start = fold_edit.old.start.to_point(&old_snapshot.fold_snapshot);
+ let old_end = fold_edit.old.end.to_point(&old_snapshot.fold_snapshot);
+ let new_start = fold_edit.new.start.to_point(&new_snapshot.fold_snapshot);
+ let new_end = fold_edit.new.end.to_point(&new_snapshot.fold_snapshot);
tab_edits.push(TabEdit {
old: old_snapshot.to_tab_point(old_start)..old_snapshot.to_tab_point(old_end),
new: new_snapshot.to_tab_point(new_start)..new_snapshot.to_tab_point(new_end),
@@ -155,7 +143,7 @@ impl TabMap {
#[derive(Clone)]
pub struct TabSnapshot {
- pub suggestion_snapshot: SuggestionSnapshot,
+ pub fold_snapshot: FoldSnapshot,
pub tab_size: NonZeroU32,
pub max_expansion_column: u32,
pub version: usize,
@@ -163,18 +151,15 @@ pub struct TabSnapshot {
impl TabSnapshot {
pub fn buffer_snapshot(&self) -> &MultiBufferSnapshot {
- self.suggestion_snapshot.buffer_snapshot()
+ &self.fold_snapshot.inlay_snapshot.buffer
}
pub fn line_len(&self, row: u32) -> u32 {
let max_point = self.max_point();
if row < max_point.row() {
- self.to_tab_point(SuggestionPoint::new(
- row,
- self.suggestion_snapshot.line_len(row),
- ))
- .0
- .column
+ self.to_tab_point(FoldPoint::new(row, self.fold_snapshot.line_len(row)))
+ .0
+ .column
} else {
max_point.column()
}
@@ -185,10 +170,10 @@ impl TabSnapshot {
}
pub fn text_summary_for_range(&self, range: Range<TabPoint>) -> TextSummary {
- let input_start = self.to_suggestion_point(range.start, Bias::Left).0;
- let input_end = self.to_suggestion_point(range.end, Bias::Right).0;
+ let input_start = self.to_fold_point(range.start, Bias::Left).0;
+ let input_end = self.to_fold_point(range.end, Bias::Right).0;
let input_summary = self
- .suggestion_snapshot
+ .fold_snapshot
.text_summary_for_range(input_start..input_end);
let mut first_line_chars = 0;
@@ -198,7 +183,7 @@ impl TabSnapshot {
self.max_point()
};
for c in self
- .chunks(range.start..line_end, false, None, None)
+ .chunks(range.start..line_end, false, None, None, None)
.flat_map(|chunk| chunk.text.chars())
{
if c == '\n' {
@@ -217,6 +202,7 @@ impl TabSnapshot {
false,
None,
None,
+ None,
)
.flat_map(|chunk| chunk.text.chars())
{
@@ -238,15 +224,17 @@ impl TabSnapshot {
range: Range<TabPoint>,
language_aware: bool,
text_highlights: Option<&'a TextHighlights>,
- suggestion_highlight: Option<HighlightStyle>,
+ hint_highlights: Option<HighlightStyle>,
+ suggestion_highlights: Option<HighlightStyle>,
) -> TabChunks<'a> {
let (input_start, expanded_char_column, to_next_stop) =
- self.to_suggestion_point(range.start, Bias::Left);
+ self.to_fold_point(range.start, Bias::Left);
let input_column = input_start.column();
- let input_start = self.suggestion_snapshot.to_offset(input_start);
+ let input_start = input_start.to_offset(&self.fold_snapshot);
let input_end = self
- .suggestion_snapshot
- .to_offset(self.to_suggestion_point(range.end, Bias::Right).0);
+ .to_fold_point(range.end, Bias::Right)
+ .0
+ .to_offset(&self.fold_snapshot);
let to_next_stop = if range.start.0 + Point::new(0, to_next_stop) > range.end.0 {
range.end.column() - range.start.column()
} else {
@@ -254,11 +242,12 @@ impl TabSnapshot {
};
TabChunks {
- suggestion_chunks: self.suggestion_snapshot.chunks(
+ fold_chunks: self.fold_snapshot.chunks(
input_start..input_end,
language_aware,
text_highlights,
- suggestion_highlight,
+ hint_highlights,
+ suggestion_highlights,
),
input_column,
column: expanded_char_column,
@@ -275,63 +264,58 @@ impl TabSnapshot {
}
}
- pub fn buffer_rows(&self, row: u32) -> suggestion_map::SuggestionBufferRows {
- self.suggestion_snapshot.buffer_rows(row)
+ pub fn buffer_rows(&self, row: u32) -> fold_map::FoldBufferRows<'_> {
+ self.fold_snapshot.buffer_rows(row)
}
#[cfg(test)]
pub fn text(&self) -> String {
- self.chunks(TabPoint::zero()..self.max_point(), false, None, None)
+ self.chunks(TabPoint::zero()..self.max_point(), false, None, None, None)
.map(|chunk| chunk.text)
.collect()
}
pub fn max_point(&self) -> TabPoint {
- self.to_tab_point(self.suggestion_snapshot.max_point())
+ self.to_tab_point(self.fold_snapshot.max_point())
}
pub fn clip_point(&self, point: TabPoint, bias: Bias) -> TabPoint {
self.to_tab_point(
- self.suggestion_snapshot
- .clip_point(self.to_suggestion_point(point, bias).0, bias),
+ self.fold_snapshot
+ .clip_point(self.to_fold_point(point, bias).0, bias),
)
}
- pub fn to_tab_point(&self, input: SuggestionPoint) -> TabPoint {
- let chars = self
- .suggestion_snapshot
- .chars_at(SuggestionPoint::new(input.row(), 0));
+ pub fn to_tab_point(&self, input: FoldPoint) -> TabPoint {
+ let chars = self.fold_snapshot.chars_at(FoldPoint::new(input.row(), 0));
let expanded = self.expand_tabs(chars, input.column());
TabPoint::new(input.row(), expanded)
}
- pub fn to_suggestion_point(&self, output: TabPoint, bias: Bias) -> (SuggestionPoint, u32, u32) {
- let chars = self
- .suggestion_snapshot
- .chars_at(SuggestionPoint::new(output.row(), 0));
+ pub fn to_fold_point(&self, output: TabPoint, bias: Bias) -> (FoldPoint, u32, u32) {
+ let chars = self.fold_snapshot.chars_at(FoldPoint::new(output.row(), 0));
let expanded = output.column();
let (collapsed, expanded_char_column, to_next_stop) =
self.collapse_tabs(chars, expanded, bias);
(
- SuggestionPoint::new(output.row(), collapsed as u32),
+ FoldPoint::new(output.row(), collapsed as u32),
expanded_char_column,
to_next_stop,
)
}
pub fn make_tab_point(&self, point: Point, bias: Bias) -> TabPoint {
- let fold_point = self
- .suggestion_snapshot
- .fold_snapshot
- .to_fold_point(point, bias);
- let suggestion_point = self.suggestion_snapshot.to_suggestion_point(fold_point);
- self.to_tab_point(suggestion_point)
+ let inlay_point = self.fold_snapshot.inlay_snapshot.to_inlay_point(point);
+ let fold_point = self.fold_snapshot.to_fold_point(inlay_point, bias);
+ self.to_tab_point(fold_point)
}
pub fn to_point(&self, point: TabPoint, bias: Bias) -> Point {
- let suggestion_point = self.to_suggestion_point(point, bias).0;
- let fold_point = self.suggestion_snapshot.to_fold_point(suggestion_point);
- fold_point.to_buffer_point(&self.suggestion_snapshot.fold_snapshot)
+ let fold_point = self.to_fold_point(point, bias).0;
+ let inlay_point = fold_point.to_inlay_point(&self.fold_snapshot);
+ self.fold_snapshot
+ .inlay_snapshot
+ .to_buffer_point(inlay_point)
}
fn expand_tabs(&self, chars: impl Iterator<Item = char>, column: u32) -> u32 {
@@ -490,7 +474,7 @@ impl<'a> std::ops::AddAssign<&'a Self> for TextSummary {
const SPACES: &str = " ";
pub struct TabChunks<'a> {
- suggestion_chunks: SuggestionChunks<'a>,
+ fold_chunks: FoldChunks<'a>,
chunk: Chunk<'a>,
column: u32,
max_expansion_column: u32,
@@ -506,7 +490,7 @@ impl<'a> Iterator for TabChunks<'a> {
fn next(&mut self) -> Option<Self::Item> {
if self.chunk.text.is_empty() {
- if let Some(chunk) = self.suggestion_chunks.next() {
+ if let Some(chunk) = self.fold_chunks.next() {
self.chunk = chunk;
if self.inside_leading_tab {
self.chunk.text = &self.chunk.text[1..];
@@ -574,7 +558,7 @@ impl<'a> Iterator for TabChunks<'a> {
mod tests {
use super::*;
use crate::{
- display_map::{fold_map::FoldMap, suggestion_map::SuggestionMap},
+ display_map::{fold_map::FoldMap, inlay_map::InlayMap},
MultiBuffer,
};
use rand::{prelude::StdRng, Rng};
@@ -583,9 +567,9 @@ mod tests {
fn test_expand_tabs(cx: &mut gpui::AppContext) {
let buffer = MultiBuffer::build_simple("", cx);
let buffer_snapshot = buffer.read(cx).snapshot(cx);
- let (_, fold_snapshot) = FoldMap::new(buffer_snapshot.clone());
- let (_, suggestion_snapshot) = SuggestionMap::new(fold_snapshot);
- let (_, tab_snapshot) = TabMap::new(suggestion_snapshot, 4.try_into().unwrap());
+ let (_, inlay_snapshot) = InlayMap::new(buffer_snapshot.clone());
+ let (_, fold_snapshot) = FoldMap::new(inlay_snapshot);
+ let (_, tab_snapshot) = TabMap::new(fold_snapshot, 4.try_into().unwrap());
assert_eq!(tab_snapshot.expand_tabs("\t".chars(), 0), 0);
assert_eq!(tab_snapshot.expand_tabs("\t".chars(), 1), 4);
@@ -600,9 +584,9 @@ mod tests {
let buffer = MultiBuffer::build_simple(input, cx);
let buffer_snapshot = buffer.read(cx).snapshot(cx);
- let (_, fold_snapshot) = FoldMap::new(buffer_snapshot.clone());
- let (_, suggestion_snapshot) = SuggestionMap::new(fold_snapshot);
- let (_, mut tab_snapshot) = TabMap::new(suggestion_snapshot, 4.try_into().unwrap());
+ let (_, inlay_snapshot) = InlayMap::new(buffer_snapshot.clone());
+ let (_, fold_snapshot) = FoldMap::new(inlay_snapshot);
+ let (_, mut tab_snapshot) = TabMap::new(fold_snapshot, 4.try_into().unwrap());
tab_snapshot.max_expansion_column = max_expansion_column;
assert_eq!(tab_snapshot.text(), output);
@@ -615,6 +599,7 @@ mod tests {
false,
None,
None,
+ None,
)
.map(|c| c.text)
.collect::<String>(),
@@ -626,16 +611,16 @@ mod tests {
let input_point = Point::new(0, ix as u32);
let output_point = Point::new(0, output.find(c).unwrap() as u32);
assert_eq!(
- tab_snapshot.to_tab_point(SuggestionPoint(input_point)),
+ tab_snapshot.to_tab_point(FoldPoint(input_point)),
TabPoint(output_point),
"to_tab_point({input_point:?})"
);
assert_eq!(
tab_snapshot
- .to_suggestion_point(TabPoint(output_point), Bias::Left)
+ .to_fold_point(TabPoint(output_point), Bias::Left)
.0,
- SuggestionPoint(input_point),
- "to_suggestion_point({output_point:?})"
+ FoldPoint(input_point),
+ "to_fold_point({output_point:?})"
);
}
}
@@ -648,9 +633,9 @@ mod tests {
let buffer = MultiBuffer::build_simple(input, cx);
let buffer_snapshot = buffer.read(cx).snapshot(cx);
- let (_, fold_snapshot) = FoldMap::new(buffer_snapshot.clone());
- let (_, suggestion_snapshot) = SuggestionMap::new(fold_snapshot);
- let (_, mut tab_snapshot) = TabMap::new(suggestion_snapshot, 4.try_into().unwrap());
+ let (_, inlay_snapshot) = InlayMap::new(buffer_snapshot.clone());
+ let (_, fold_snapshot) = FoldMap::new(inlay_snapshot);
+ let (_, mut tab_snapshot) = TabMap::new(fold_snapshot, 4.try_into().unwrap());
tab_snapshot.max_expansion_column = max_expansion_column;
assert_eq!(tab_snapshot.text(), input);
@@ -662,9 +647,9 @@ mod tests {
let buffer = MultiBuffer::build_simple(&input, cx);
let buffer_snapshot = buffer.read(cx).snapshot(cx);
- let (_, fold_snapshot) = FoldMap::new(buffer_snapshot.clone());
- let (_, suggestion_snapshot) = SuggestionMap::new(fold_snapshot);
- let (_, tab_snapshot) = TabMap::new(suggestion_snapshot, 4.try_into().unwrap());
+ let (_, inlay_snapshot) = InlayMap::new(buffer_snapshot.clone());
+ let (_, fold_snapshot) = FoldMap::new(inlay_snapshot);
+ let (_, tab_snapshot) = TabMap::new(fold_snapshot, 4.try_into().unwrap());
assert_eq!(
chunks(&tab_snapshot, TabPoint::zero()),
@@ -689,7 +674,7 @@ mod tests {
let mut chunks = Vec::new();
let mut was_tab = false;
let mut text = String::new();
- for chunk in snapshot.chunks(start..snapshot.max_point(), false, None, None) {
+ for chunk in snapshot.chunks(start..snapshot.max_point(), false, None, None, None) {
if chunk.is_tab != was_tab {
if !text.is_empty() {
chunks.push((mem::take(&mut text), was_tab));
@@ -721,15 +706,16 @@ mod tests {
let buffer_snapshot = buffer.read(cx).snapshot(cx);
log::info!("Buffer text: {:?}", buffer_snapshot.text());
- let (mut fold_map, _) = FoldMap::new(buffer_snapshot.clone());
+ let (mut inlay_map, inlay_snapshot) = InlayMap::new(buffer_snapshot.clone());
+ log::info!("InlayMap text: {:?}", inlay_snapshot.text());
+ let (mut fold_map, _) = FoldMap::new(inlay_snapshot.clone());
fold_map.randomly_mutate(&mut rng);
- let (fold_snapshot, _) = fold_map.read(buffer_snapshot, vec![]);
+ let (fold_snapshot, _) = fold_map.read(inlay_snapshot, vec![]);
log::info!("FoldMap text: {:?}", fold_snapshot.text());
- let (suggestion_map, _) = SuggestionMap::new(fold_snapshot);
- let (suggestion_snapshot, _) = suggestion_map.randomly_mutate(&mut rng);
- log::info!("SuggestionMap text: {:?}", suggestion_snapshot.text());
+ let (inlay_snapshot, _) = inlay_map.randomly_mutate(&mut 0, &mut rng);
+ log::info!("InlayMap text: {:?}", inlay_snapshot.text());
- let (tab_map, _) = TabMap::new(suggestion_snapshot.clone(), tab_size);
+ let (mut tab_map, _) = TabMap::new(fold_snapshot.clone(), tab_size);
let tabs_snapshot = tab_map.set_max_expansion_column(32);
let text = text::Rope::from(tabs_snapshot.text().as_str());
@@ -757,7 +743,7 @@ mod tests {
let expected_summary = TextSummary::from(expected_text.as_str());
assert_eq!(
tabs_snapshot
- .chunks(start..end, false, None, None)
+ .chunks(start..end, false, None, None, None)
.map(|c| c.text)
.collect::<String>(),
expected_text,
@@ -767,7 +753,7 @@ mod tests {
);
let mut actual_summary = tabs_snapshot.text_summary_for_range(start..end);
- if tab_size.get() > 1 && suggestion_snapshot.text().contains('\t') {
+ if tab_size.get() > 1 && inlay_snapshot.text().contains('\t') {
actual_summary.longest_row = expected_summary.longest_row;
actual_summary.longest_row_chars = expected_summary.longest_row_chars;
}
@@ -1,5 +1,5 @@
use super::{
- suggestion_map::SuggestionBufferRows,
+ fold_map::FoldBufferRows,
tab_map::{self, TabEdit, TabPoint, TabSnapshot},
TextHighlights,
};
@@ -65,7 +65,7 @@ pub struct WrapChunks<'a> {
#[derive(Clone)]
pub struct WrapBufferRows<'a> {
- input_buffer_rows: SuggestionBufferRows<'a>,
+ input_buffer_rows: FoldBufferRows<'a>,
input_buffer_row: Option<u32>,
output_row: u32,
soft_wrapped: bool,
@@ -446,6 +446,7 @@ impl WrapSnapshot {
false,
None,
None,
+ None,
);
let mut edit_transforms = Vec::<Transform>::new();
for _ in edit.new_rows.start..edit.new_rows.end {
@@ -575,7 +576,8 @@ impl WrapSnapshot {
rows: Range<u32>,
language_aware: bool,
text_highlights: Option<&'a TextHighlights>,
- suggestion_highlight: Option<HighlightStyle>,
+ hint_highlights: Option<HighlightStyle>,
+ suggestion_highlights: Option<HighlightStyle>,
) -> WrapChunks<'a> {
let output_start = WrapPoint::new(rows.start, 0);
let output_end = WrapPoint::new(rows.end, 0);
@@ -593,7 +595,8 @@ impl WrapSnapshot {
input_start..input_end,
language_aware,
text_highlights,
- suggestion_highlight,
+ hint_highlights,
+ suggestion_highlights,
),
input_chunk: Default::default(),
output_position: output_start,
@@ -757,28 +760,18 @@ impl WrapSnapshot {
}
let text = language::Rope::from(self.text().as_str());
- let input_buffer_rows = self.buffer_snapshot().buffer_rows(0).collect::<Vec<_>>();
+ let mut input_buffer_rows = self.tab_snapshot.buffer_rows(0);
let mut expected_buffer_rows = Vec::new();
- let mut prev_fold_row = 0;
+ let mut prev_tab_row = 0;
for display_row in 0..=self.max_point().row() {
let tab_point = self.to_tab_point(WrapPoint::new(display_row, 0));
- let suggestion_point = self
- .tab_snapshot
- .to_suggestion_point(tab_point, Bias::Left)
- .0;
- let fold_point = self
- .tab_snapshot
- .suggestion_snapshot
- .to_fold_point(suggestion_point);
- if fold_point.row() == prev_fold_row && display_row != 0 {
+ if tab_point.row() == prev_tab_row && display_row != 0 {
expected_buffer_rows.push(None);
} else {
- let buffer_point = fold_point
- .to_buffer_point(&self.tab_snapshot.suggestion_snapshot.fold_snapshot);
- expected_buffer_rows.push(input_buffer_rows[buffer_point.row as usize]);
- prev_fold_row = fold_point.row();
+ expected_buffer_rows.push(input_buffer_rows.next().unwrap());
}
+ prev_tab_row = tab_point.row();
assert_eq!(self.line_len(display_row), text.line_len(display_row));
}
@@ -1038,7 +1031,7 @@ fn consolidate_wrap_edits(edits: &mut Vec<WrapEdit>) {
mod tests {
use super::*;
use crate::{
- display_map::{fold_map::FoldMap, suggestion_map::SuggestionMap, tab_map::TabMap},
+ display_map::{fold_map::FoldMap, inlay_map::InlayMap, tab_map::TabMap},
MultiBuffer,
};
use gpui::test::observe;
@@ -1089,11 +1082,11 @@ mod tests {
});
let mut buffer_snapshot = buffer.read_with(cx, |buffer, cx| buffer.snapshot(cx));
log::info!("Buffer text: {:?}", buffer_snapshot.text());
- let (mut fold_map, fold_snapshot) = FoldMap::new(buffer_snapshot.clone());
+ let (mut inlay_map, inlay_snapshot) = InlayMap::new(buffer_snapshot.clone());
+ log::info!("InlayMap text: {:?}", inlay_snapshot.text());
+ let (mut fold_map, fold_snapshot) = FoldMap::new(inlay_snapshot.clone());
log::info!("FoldMap text: {:?}", fold_snapshot.text());
- let (suggestion_map, suggestion_snapshot) = SuggestionMap::new(fold_snapshot.clone());
- log::info!("SuggestionMap text: {:?}", suggestion_snapshot.text());
- let (tab_map, _) = TabMap::new(suggestion_snapshot.clone(), tab_size);
+ let (mut tab_map, _) = TabMap::new(fold_snapshot.clone(), tab_size);
let tabs_snapshot = tab_map.set_max_expansion_column(32);
log::info!("TabMap text: {:?}", tabs_snapshot.text());
@@ -1122,6 +1115,7 @@ mod tests {
);
log::info!("Wrapped text: {:?}", actual_text);
+ let mut next_inlay_id = 0;
let mut edits = Vec::new();
for _i in 0..operations {
log::info!("{} ==============================================", _i);
@@ -1139,10 +1133,8 @@ mod tests {
}
20..=39 => {
for (fold_snapshot, fold_edits) in fold_map.randomly_mutate(&mut rng) {
- let (suggestion_snapshot, suggestion_edits) =
- suggestion_map.sync(fold_snapshot, fold_edits);
let (tabs_snapshot, tab_edits) =
- tab_map.sync(suggestion_snapshot, suggestion_edits, tab_size);
+ tab_map.sync(fold_snapshot, fold_edits, tab_size);
let (mut snapshot, wrap_edits) =
wrap_map.update(cx, |map, cx| map.sync(tabs_snapshot, tab_edits, cx));
snapshot.check_invariants();
@@ -1151,10 +1143,11 @@ mod tests {
}
}
40..=59 => {
- let (suggestion_snapshot, suggestion_edits) =
- suggestion_map.randomly_mutate(&mut rng);
+ let (inlay_snapshot, inlay_edits) =
+ inlay_map.randomly_mutate(&mut next_inlay_id, &mut rng);
+ let (fold_snapshot, fold_edits) = fold_map.read(inlay_snapshot, inlay_edits);
let (tabs_snapshot, tab_edits) =
- tab_map.sync(suggestion_snapshot, suggestion_edits, tab_size);
+ tab_map.sync(fold_snapshot, fold_edits, tab_size);
let (mut snapshot, wrap_edits) =
wrap_map.update(cx, |map, cx| map.sync(tabs_snapshot, tab_edits, cx));
snapshot.check_invariants();
@@ -1173,13 +1166,12 @@ mod tests {
}
log::info!("Buffer text: {:?}", buffer_snapshot.text());
- let (fold_snapshot, fold_edits) = fold_map.read(buffer_snapshot.clone(), buffer_edits);
+ let (inlay_snapshot, inlay_edits) =
+ inlay_map.sync(buffer_snapshot.clone(), buffer_edits);
+ log::info!("InlayMap text: {:?}", inlay_snapshot.text());
+ let (fold_snapshot, fold_edits) = fold_map.read(inlay_snapshot, inlay_edits);
log::info!("FoldMap text: {:?}", fold_snapshot.text());
- let (suggestion_snapshot, suggestion_edits) =
- suggestion_map.sync(fold_snapshot, fold_edits);
- log::info!("SuggestionMap text: {:?}", suggestion_snapshot.text());
- let (tabs_snapshot, tab_edits) =
- tab_map.sync(suggestion_snapshot, suggestion_edits, tab_size);
+ let (tabs_snapshot, tab_edits) = tab_map.sync(fold_snapshot, fold_edits, tab_size);
log::info!("TabMap text: {:?}", tabs_snapshot.text());
let unwrapped_text = tabs_snapshot.text();
@@ -1227,7 +1219,7 @@ mod tests {
if tab_size.get() == 1
|| !wrapped_snapshot
.tab_snapshot
- .suggestion_snapshot
+ .fold_snapshot
.text()
.contains('\t')
{
@@ -1328,8 +1320,14 @@ mod tests {
}
pub fn text_chunks(&self, wrap_row: u32) -> impl Iterator<Item = &str> {
- self.chunks(wrap_row..self.max_point().row() + 1, false, None, None)
- .map(|h| h.text)
+ self.chunks(
+ wrap_row..self.max_point().row() + 1,
+ false,
+ None,
+ None,
+ None,
+ )
+ .map(|h| h.text)
}
fn verify_chunks(&mut self, rng: &mut impl Rng) {
@@ -1352,7 +1350,7 @@ mod tests {
}
let actual_text = self
- .chunks(start_row..end_row, true, None, None)
+ .chunks(start_row..end_row, true, None, None, None)
.map(|c| c.text)
.collect::<String>();
assert_eq!(
@@ -2,6 +2,7 @@ mod blink_manager;
pub mod display_map;
mod editor_settings;
mod element;
+mod inlay_hint_cache;
mod git;
mod highlight_matching_bracket;
@@ -52,11 +53,12 @@ use gpui::{
};
use highlight_matching_bracket::refresh_matching_bracket_highlights;
use hover_popover::{hide_hover, HoverState};
+use inlay_hint_cache::{InlayHintCache, InlaySplice, InvalidationStrategy};
pub use items::MAX_TAB_TITLE_LEN;
use itertools::Itertools;
pub use language::{char_kind, CharKind};
use language::{
- language_settings::{self, all_language_settings},
+ language_settings::{self, all_language_settings, InlayHintSettings},
AutoindentMode, BracketPair, Buffer, CodeAction, CodeLabel, Completion, CursorShape,
Diagnostic, DiagnosticSeverity, File, IndentKind, IndentSize, Language, OffsetRangeExt,
OffsetUtf16, Point, Selection, SelectionGoal, TransactionId,
@@ -64,11 +66,12 @@ use language::{
use link_go_to_definition::{
hide_link_definition, show_link_definition, LinkDefinitionKind, LinkGoToDefinitionState,
};
+use log::error;
+use multi_buffer::ToOffsetUtf16;
pub use multi_buffer::{
Anchor, AnchorRangeExt, ExcerptId, ExcerptRange, MultiBuffer, MultiBufferSnapshot, ToOffset,
ToPoint,
};
-use multi_buffer::{MultiBufferChunks, ToOffsetUtf16};
use ordered_float::OrderedFloat;
use project::{FormatTrigger, Location, LocationLink, Project, ProjectPath, ProjectTransaction};
use scroll::{
@@ -85,12 +88,13 @@ use std::{
cmp::{self, Ordering, Reverse},
mem,
num::NonZeroU32,
- ops::{Deref, DerefMut, Range},
+ ops::{ControlFlow, Deref, DerefMut, Range},
path::Path,
sync::Arc,
time::{Duration, Instant},
};
pub use sum_tree::Bias;
+use text::Rope;
use theme::{DiagnosticStyle, Theme, ThemeSettings};
use util::{post_inc, RangeExt, ResultExt, TryFutureExt};
use workspace::{ItemNavHistory, ViewId, Workspace};
@@ -180,6 +184,12 @@ pub struct GutterHover {
pub hovered: bool,
}
+#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
+pub enum InlayId {
+ Suggestion(usize),
+ Hint(usize),
+}
+
actions!(
editor,
[
@@ -535,6 +545,8 @@ pub struct Editor {
gutter_hovered: bool,
link_go_to_definition_state: LinkGoToDefinitionState,
copilot_state: CopilotState,
+ inlay_hint_cache: InlayHintCache,
+ next_inlay_id: usize,
_subscriptions: Vec<Subscription>,
}
@@ -1056,6 +1068,7 @@ pub struct CopilotState {
cycled: bool,
completions: Vec<copilot::Completion>,
active_completion_index: usize,
+ suggestion: Option<Inlay>,
}
impl Default for CopilotState {
@@ -1067,6 +1080,7 @@ impl Default for CopilotState {
completions: Default::default(),
active_completion_index: 0,
cycled: false,
+ suggestion: None,
}
}
}
@@ -1181,6 +1195,14 @@ enum GotoDefinitionKind {
Type,
}
+#[derive(Debug, Copy, Clone)]
+enum InlayRefreshReason {
+ SettingsChange(InlayHintSettings),
+ NewLinesShown,
+ ExcerptEdited,
+ RefreshRequested,
+}
+
impl Editor {
pub fn single_line(
field_editor_style: Option<Arc<GetFieldEditorTheme>>,
@@ -1282,15 +1304,28 @@ impl Editor {
let soft_wrap_mode_override =
(mode == EditorMode::SingleLine).then(|| language_settings::SoftWrap::None);
- let mut project_subscription = None;
- if mode == EditorMode::Full && buffer.read(cx).is_singleton() {
+ let mut project_subscriptions = Vec::new();
+ if mode == EditorMode::Full {
if let Some(project) = project.as_ref() {
- project_subscription = Some(cx.observe(project, |_, _, cx| {
- cx.emit(Event::TitleChanged);
- }))
+ if buffer.read(cx).is_singleton() {
+ project_subscriptions.push(cx.observe(project, |_, _, cx| {
+ cx.emit(Event::TitleChanged);
+ }));
+ }
+ project_subscriptions.push(cx.subscribe(project, |editor, _, event, cx| {
+ if let project::Event::RefreshInlays = event {
+ editor.refresh_inlays(InlayRefreshReason::RefreshRequested, cx);
+ };
+ }));
}
}
+ let inlay_hint_settings = inlay_hint_settings(
+ selections.newest_anchor().head(),
+ &buffer.read(cx).snapshot(cx),
+ cx,
+ );
+
let mut this = Self {
handle: cx.weak_handle(),
buffer: buffer.clone(),
@@ -1324,6 +1359,7 @@ impl Editor {
.add_view(|cx| context_menu::ContextMenu::new(editor_view_id, cx)),
completion_tasks: Default::default(),
next_completion_id: 0,
+ next_inlay_id: 0,
available_code_actions: Default::default(),
code_actions_task: Default::default(),
document_highlights_task: Default::default(),
@@ -1340,6 +1376,7 @@ impl Editor {
hover_state: Default::default(),
link_go_to_definition_state: Default::default(),
copilot_state: Default::default(),
+ inlay_hint_cache: InlayHintCache::new(inlay_hint_settings),
gutter_hovered: false,
_subscriptions: vec![
cx.observe(&buffer, Self::on_buffer_changed),
@@ -1350,9 +1387,7 @@ impl Editor {
],
};
- if let Some(project_subscription) = project_subscription {
- this._subscriptions.push(project_subscription);
- }
+ this._subscriptions.extend(project_subscriptions);
this.end_selection(cx);
this.scroll_manager.show_scrollbar(cx);
@@ -1873,7 +1908,7 @@ impl Editor {
s.set_pending(pending, mode);
});
} else {
- log::error!("update_selection dispatched with no pending selection");
+ error!("update_selection dispatched with no pending selection");
return;
}
@@ -2577,6 +2612,106 @@ impl Editor {
}
}
+ fn refresh_inlays(&mut self, reason: InlayRefreshReason, cx: &mut ViewContext<Self>) {
+ if self.project.is_none() || self.mode != EditorMode::Full {
+ return;
+ }
+
+ let invalidate_cache = match reason {
+ InlayRefreshReason::SettingsChange(new_settings) => {
+ match self.inlay_hint_cache.update_settings(
+ &self.buffer,
+ new_settings,
+ self.visible_inlay_hints(cx),
+ cx,
+ ) {
+ ControlFlow::Break(Some(InlaySplice {
+ to_remove,
+ to_insert,
+ })) => {
+ self.splice_inlay_hints(to_remove, to_insert, cx);
+ return;
+ }
+ ControlFlow::Break(None) => return,
+ ControlFlow::Continue(()) => InvalidationStrategy::RefreshRequested,
+ }
+ }
+ InlayRefreshReason::NewLinesShown => InvalidationStrategy::None,
+ InlayRefreshReason::ExcerptEdited => InvalidationStrategy::ExcerptEdited,
+ InlayRefreshReason::RefreshRequested => InvalidationStrategy::RefreshRequested,
+ };
+
+ self.inlay_hint_cache.refresh_inlay_hints(
+ self.excerpt_visible_offsets(cx),
+ invalidate_cache,
+ cx,
+ )
+ }
+
+ fn visible_inlay_hints(&self, cx: &ViewContext<'_, '_, Editor>) -> Vec<Inlay> {
+ self.display_map
+ .read(cx)
+ .current_inlays()
+ .filter(move |inlay| {
+ Some(inlay.id) != self.copilot_state.suggestion.as_ref().map(|h| h.id)
+ })
+ .cloned()
+ .collect()
+ }
+
+ fn excerpt_visible_offsets(
+ &self,
+ cx: &mut ViewContext<'_, '_, Editor>,
+ ) -> HashMap<ExcerptId, (ModelHandle<Buffer>, Range<usize>)> {
+ let multi_buffer = self.buffer().read(cx);
+ let multi_buffer_snapshot = multi_buffer.snapshot(cx);
+ let multi_buffer_visible_start = self
+ .scroll_manager
+ .anchor()
+ .anchor
+ .to_point(&multi_buffer_snapshot);
+ let multi_buffer_visible_end = multi_buffer_snapshot.clip_point(
+ multi_buffer_visible_start
+ + Point::new(self.visible_line_count().unwrap_or(0.).ceil() as u32, 0),
+ Bias::Left,
+ );
+ let multi_buffer_visible_range = multi_buffer_visible_start..multi_buffer_visible_end;
+ multi_buffer
+ .range_to_buffer_ranges(multi_buffer_visible_range, cx)
+ .into_iter()
+ .filter(|(_, excerpt_visible_range, _)| !excerpt_visible_range.is_empty())
+ .map(|(buffer, excerpt_visible_range, excerpt_id)| {
+ (excerpt_id, (buffer, excerpt_visible_range))
+ })
+ .collect()
+ }
+
+ fn splice_inlay_hints(
+ &self,
+ to_remove: Vec<InlayId>,
+ to_insert: Vec<(Anchor, InlayId, project::InlayHint)>,
+ cx: &mut ViewContext<Self>,
+ ) {
+ let buffer = self.buffer.read(cx).read(cx);
+ let new_inlays = to_insert
+ .into_iter()
+ .map(|(position, id, hint)| {
+ let mut text = hint.text();
+ if hint.padding_right {
+ text.push(' ');
+ }
+ if hint.padding_left {
+ text.insert(0, ' ');
+ }
+ (id, InlayProperties { position, text })
+ })
+ .collect();
+ drop(buffer);
+ self.display_map.update(cx, |display_map, cx| {
+ display_map.splice_inlays(to_remove, new_inlays, cx);
+ });
+ }
+
fn trigger_on_type_formatting(
&self,
input: String,
@@ -3227,10 +3362,7 @@ impl Editor {
}
fn accept_copilot_suggestion(&mut self, cx: &mut ViewContext<Self>) -> bool {
- if let Some(suggestion) = self
- .display_map
- .update(cx, |map, cx| map.replace_suggestion::<usize>(None, cx))
- {
+ if let Some(suggestion) = self.take_active_copilot_suggestion(cx) {
if let Some((copilot, completion)) =
Copilot::global(cx).zip(self.copilot_state.active_completion())
{
@@ -3249,7 +3381,7 @@ impl Editor {
}
fn discard_copilot_suggestion(&mut self, cx: &mut ViewContext<Self>) -> bool {
- if self.has_active_copilot_suggestion(cx) {
+ if let Some(suggestion) = self.take_active_copilot_suggestion(cx) {
if let Some(copilot) = Copilot::global(cx) {
copilot
.update(cx, |copilot, cx| {
@@ -3260,8 +3392,9 @@ impl Editor {
self.report_copilot_event(None, false, cx)
}
- self.display_map
- .update(cx, |map, cx| map.replace_suggestion::<usize>(None, cx));
+ self.display_map.update(cx, |map, cx| {
+ map.splice_inlays::<&str>(vec![suggestion.id], Vec::new(), cx)
+ });
cx.notify();
true
} else {
@@ -3282,7 +3415,26 @@ impl Editor {
}
fn has_active_copilot_suggestion(&self, cx: &AppContext) -> bool {
- self.display_map.read(cx).has_suggestion()
+ if let Some(suggestion) = self.copilot_state.suggestion.as_ref() {
+ let buffer = self.buffer.read(cx).read(cx);
+ suggestion.position.is_valid(&buffer)
+ } else {
+ false
+ }
+ }
+
+ fn take_active_copilot_suggestion(&mut self, cx: &mut ViewContext<Self>) -> Option<Inlay> {
+ let suggestion = self.copilot_state.suggestion.take()?;
+ self.display_map.update(cx, |map, cx| {
+ map.splice_inlays::<&str>(vec![suggestion.id], Default::default(), cx);
+ });
+ let buffer = self.buffer.read(cx).read(cx);
+
+ if suggestion.position.is_valid(&buffer) {
+ Some(suggestion)
+ } else {
+ None
+ }
}
fn update_visible_copilot_suggestion(&mut self, cx: &mut ViewContext<Self>) {
@@ -3299,14 +3451,27 @@ impl Editor {
.copilot_state
.text_for_active_completion(cursor, &snapshot)
{
+ let text = Rope::from(text);
+ let mut to_remove = Vec::new();
+ if let Some(suggestion) = self.copilot_state.suggestion.take() {
+ to_remove.push(suggestion.id);
+ }
+
+ let suggestion_inlay_id = InlayId::Suggestion(post_inc(&mut self.next_inlay_id));
+ let to_insert = vec![(
+ suggestion_inlay_id,
+ InlayProperties {
+ position: cursor,
+ text: text.clone(),
+ },
+ )];
self.display_map.update(cx, move |map, cx| {
- map.replace_suggestion(
- Some(Suggestion {
- position: cursor,
- text: text.trim_end().into(),
- }),
- cx,
- )
+ map.splice_inlays(to_remove, to_insert, cx)
+ });
+ self.copilot_state.suggestion = Some(Inlay {
+ id: suggestion_inlay_id,
+ position: cursor,
+ text,
});
cx.notify();
} else {
@@ -6641,7 +6806,7 @@ impl Editor {
if let Some((_, end_selections)) = self.selection_history.transaction_mut(tx_id) {
*end_selections = Some(self.selections.disjoint_anchors());
} else {
- log::error!("unexpectedly ended a transaction that wasn't started by this editor");
+ error!("unexpectedly ended a transaction that wasn't started by this editor");
}
cx.emit(Event::Edited);
@@ -7103,6 +7268,7 @@ impl Editor {
self.update_visible_copilot_suggestion(cx);
}
cx.emit(Event::BufferEdited);
+ self.refresh_inlays(InlayRefreshReason::ExcerptEdited, cx);
}
multi_buffer::Event::ExcerptsAdded {
buffer,
@@ -7127,7 +7293,7 @@ impl Editor {
self.refresh_active_diagnostics(cx);
}
_ => {}
- }
+ };
}
fn on_display_map_changed(&mut self, _: ModelHandle<DisplayMap>, cx: &mut ViewContext<Self>) {
@@ -7136,6 +7302,14 @@ impl Editor {
fn settings_changed(&mut self, cx: &mut ViewContext<Self>) {
self.refresh_copilot_suggestions(true, cx);
+ self.refresh_inlays(
+ InlayRefreshReason::SettingsChange(inlay_hint_settings(
+ self.selections.newest_anchor().head(),
+ &self.buffer.read(cx).snapshot(cx),
+ cx,
+ )),
+ cx,
+ );
}
pub fn set_searchable(&mut self, searchable: bool) {
@@ -7425,6 +7599,23 @@ impl Editor {
let Some(lines) = serde_json::to_string_pretty(&lines).log_err() else { return; };
cx.write_to_clipboard(ClipboardItem::new(lines));
}
+
+ pub fn inlay_hint_cache(&self) -> &InlayHintCache {
+ &self.inlay_hint_cache
+ }
+}
+
+fn inlay_hint_settings(
+ location: Anchor,
+ snapshot: &MultiBufferSnapshot,
+ cx: &mut ViewContext<'_, '_, Editor>,
+) -> InlayHintSettings {
+ let file = snapshot.file_at(location);
+ let language = snapshot.language_at(location);
+ let settings = all_language_settings(file, cx);
+ settings
+ .language(language.map(|l| l.name()).as_deref())
+ .inlay_hints
}
fn consume_contiguous_rows(
@@ -1392,7 +1392,12 @@ impl EditorElement {
} else {
let style = &self.style;
let chunks = snapshot
- .chunks(rows.clone(), true, Some(style.theme.suggestion))
+ .chunks(
+ rows.clone(),
+ true,
+ Some(style.theme.hint),
+ Some(style.theme.suggestion),
+ )
.map(|chunk| {
let mut highlight_style = chunk
.syntax_highlight_id
@@ -1921,7 +1926,7 @@ impl Element<Editor> for EditorElement {
let em_advance = style.text.em_advance(cx.font_cache());
let overscroll = vec2f(em_width, 0.);
let snapshot = {
- editor.set_visible_line_count(size.y() / line_height);
+ editor.set_visible_line_count(size.y() / line_height, cx);
let editor_width = text_width - gutter_margin - overscroll.x() - em_width;
let wrap_width = match editor.soft_wrap_mode(cx) {
@@ -0,0 +1,2021 @@
+use std::{
+ cmp,
+ ops::{ControlFlow, Range},
+ sync::Arc,
+};
+
+use crate::{
+ display_map::Inlay, Anchor, Editor, ExcerptId, InlayId, MultiBuffer, MultiBufferSnapshot,
+};
+use anyhow::Context;
+use clock::Global;
+use gpui::{ModelHandle, Task, ViewContext};
+use language::{language_settings::InlayHintKind, Buffer, BufferSnapshot};
+use log::error;
+use parking_lot::RwLock;
+use project::InlayHint;
+
+use collections::{hash_map, HashMap, HashSet};
+use language::language_settings::InlayHintSettings;
+use util::post_inc;
+
+pub struct InlayHintCache {
+ pub hints: HashMap<ExcerptId, Arc<RwLock<CachedExcerptHints>>>,
+ pub allowed_hint_kinds: HashSet<Option<InlayHintKind>>,
+ pub version: usize,
+ pub enabled: bool,
+ update_tasks: HashMap<ExcerptId, UpdateTask>,
+}
+
+#[derive(Debug)]
+pub struct CachedExcerptHints {
+ version: usize,
+ buffer_version: Global,
+ buffer_id: u64,
+ pub hints: Vec<(InlayId, InlayHint)>,
+}
+
+#[derive(Debug, Clone, Copy)]
+pub enum InvalidationStrategy {
+ RefreshRequested,
+ ExcerptEdited,
+ None,
+}
+
+#[derive(Debug, Default)]
+pub struct InlaySplice {
+ pub to_remove: Vec<InlayId>,
+ pub to_insert: Vec<(Anchor, InlayId, InlayHint)>,
+}
+
+struct UpdateTask {
+ invalidate: InvalidationStrategy,
+ cache_version: usize,
+ task: RunningTask,
+ pending_refresh: Option<ExcerptQuery>,
+}
+
+struct RunningTask {
+ _task: Task<()>,
+ is_running_rx: smol::channel::Receiver<()>,
+}
+
+#[derive(Debug)]
+struct ExcerptHintsUpdate {
+ excerpt_id: ExcerptId,
+ remove_from_visible: Vec<InlayId>,
+ remove_from_cache: HashSet<InlayId>,
+ add_to_cache: HashSet<InlayHint>,
+}
+
+#[derive(Debug, Clone, Copy)]
+struct ExcerptQuery {
+ buffer_id: u64,
+ excerpt_id: ExcerptId,
+ dimensions: ExcerptDimensions,
+ cache_version: usize,
+ invalidate: InvalidationStrategy,
+}
+
+#[derive(Debug, Clone, Copy)]
+struct ExcerptDimensions {
+ excerpt_range_start: language::Anchor,
+ excerpt_range_end: language::Anchor,
+ excerpt_visible_range_start: language::Anchor,
+ excerpt_visible_range_end: language::Anchor,
+}
+
+struct HintFetchRanges {
+ visible_range: Range<language::Anchor>,
+ other_ranges: Vec<Range<language::Anchor>>,
+}
+
+impl InvalidationStrategy {
+ fn should_invalidate(&self) -> bool {
+ matches!(
+ self,
+ InvalidationStrategy::RefreshRequested | InvalidationStrategy::ExcerptEdited
+ )
+ }
+}
+
+impl ExcerptQuery {
+ fn hints_fetch_ranges(&self, buffer: &BufferSnapshot) -> HintFetchRanges {
+ let visible_range =
+ self.dimensions.excerpt_visible_range_start..self.dimensions.excerpt_visible_range_end;
+ let mut other_ranges = Vec::new();
+ if self
+ .dimensions
+ .excerpt_range_start
+ .cmp(&visible_range.start, buffer)
+ .is_lt()
+ {
+ let mut end = visible_range.start;
+ end.offset -= 1;
+ other_ranges.push(self.dimensions.excerpt_range_start..end);
+ }
+ if self
+ .dimensions
+ .excerpt_range_end
+ .cmp(&visible_range.end, buffer)
+ .is_gt()
+ {
+ let mut start = visible_range.end;
+ start.offset += 1;
+ other_ranges.push(start..self.dimensions.excerpt_range_end);
+ }
+
+ HintFetchRanges {
+ visible_range,
+ other_ranges: other_ranges.into_iter().map(|range| range).collect(),
+ }
+ }
+}
+
+impl InlayHintCache {
+ pub fn new(inlay_hint_settings: InlayHintSettings) -> Self {
+ Self {
+ allowed_hint_kinds: inlay_hint_settings.enabled_inlay_hint_kinds(),
+ enabled: inlay_hint_settings.enabled,
+ hints: HashMap::default(),
+ update_tasks: HashMap::default(),
+ version: 0,
+ }
+ }
+
+ pub fn update_settings(
+ &mut self,
+ multi_buffer: &ModelHandle<MultiBuffer>,
+ new_hint_settings: InlayHintSettings,
+ visible_hints: Vec<Inlay>,
+ cx: &mut ViewContext<Editor>,
+ ) -> ControlFlow<Option<InlaySplice>> {
+ let new_allowed_hint_kinds = new_hint_settings.enabled_inlay_hint_kinds();
+ match (self.enabled, new_hint_settings.enabled) {
+ (false, false) => {
+ self.allowed_hint_kinds = new_allowed_hint_kinds;
+ ControlFlow::Break(None)
+ }
+ (true, true) => {
+ if new_allowed_hint_kinds == self.allowed_hint_kinds {
+ ControlFlow::Break(None)
+ } else {
+ let new_splice = self.new_allowed_hint_kinds_splice(
+ multi_buffer,
+ &visible_hints,
+ &new_allowed_hint_kinds,
+ cx,
+ );
+ if new_splice.is_some() {
+ self.version += 1;
+ self.update_tasks.clear();
+ self.allowed_hint_kinds = new_allowed_hint_kinds;
+ }
+ ControlFlow::Break(new_splice)
+ }
+ }
+ (true, false) => {
+ self.enabled = new_hint_settings.enabled;
+ self.allowed_hint_kinds = new_allowed_hint_kinds;
+ if self.hints.is_empty() {
+ ControlFlow::Break(None)
+ } else {
+ self.clear();
+ ControlFlow::Break(Some(InlaySplice {
+ to_remove: visible_hints.iter().map(|inlay| inlay.id).collect(),
+ to_insert: Vec::new(),
+ }))
+ }
+ }
+ (false, true) => {
+ self.enabled = new_hint_settings.enabled;
+ self.allowed_hint_kinds = new_allowed_hint_kinds;
+ ControlFlow::Continue(())
+ }
+ }
+ }
+
+ pub fn refresh_inlay_hints(
+ &mut self,
+ mut excerpts_to_query: HashMap<ExcerptId, (ModelHandle<Buffer>, Range<usize>)>,
+ invalidate: InvalidationStrategy,
+ cx: &mut ViewContext<Editor>,
+ ) {
+ if !self.enabled || excerpts_to_query.is_empty() {
+ return;
+ }
+ let update_tasks = &mut self.update_tasks;
+ if invalidate.should_invalidate() {
+ update_tasks
+ .retain(|task_excerpt_id, _| excerpts_to_query.contains_key(task_excerpt_id));
+ }
+ let cache_version = self.version;
+ excerpts_to_query.retain(|visible_excerpt_id, _| {
+ match update_tasks.entry(*visible_excerpt_id) {
+ hash_map::Entry::Occupied(o) => match o.get().cache_version.cmp(&cache_version) {
+ cmp::Ordering::Less => true,
+ cmp::Ordering::Equal => invalidate.should_invalidate(),
+ cmp::Ordering::Greater => false,
+ },
+ hash_map::Entry::Vacant(_) => true,
+ }
+ });
+
+ cx.spawn(|editor, mut cx| async move {
+ editor
+ .update(&mut cx, |editor, cx| {
+ spawn_new_update_tasks(editor, excerpts_to_query, invalidate, cache_version, cx)
+ })
+ .ok();
+ })
+ .detach();
+ }
+
+ fn new_allowed_hint_kinds_splice(
+ &self,
+ multi_buffer: &ModelHandle<MultiBuffer>,
+ visible_hints: &[Inlay],
+ new_kinds: &HashSet<Option<InlayHintKind>>,
+ cx: &mut ViewContext<Editor>,
+ ) -> Option<InlaySplice> {
+ let old_kinds = &self.allowed_hint_kinds;
+ if new_kinds == old_kinds {
+ return None;
+ }
+
+ let mut to_remove = Vec::new();
+ let mut to_insert = Vec::new();
+ let mut shown_hints_to_remove = visible_hints.iter().fold(
+ HashMap::<ExcerptId, Vec<(Anchor, InlayId)>>::default(),
+ |mut current_hints, inlay| {
+ current_hints
+ .entry(inlay.position.excerpt_id)
+ .or_default()
+ .push((inlay.position, inlay.id));
+ current_hints
+ },
+ );
+
+ let multi_buffer = multi_buffer.read(cx);
+ let multi_buffer_snapshot = multi_buffer.snapshot(cx);
+
+ for (excerpt_id, excerpt_cached_hints) in &self.hints {
+ let shown_excerpt_hints_to_remove =
+ shown_hints_to_remove.entry(*excerpt_id).or_default();
+ let excerpt_cached_hints = excerpt_cached_hints.read();
+ let mut excerpt_cache = excerpt_cached_hints.hints.iter().fuse().peekable();
+ shown_excerpt_hints_to_remove.retain(|(shown_anchor, shown_hint_id)| {
+ let Some(buffer) = shown_anchor
+ .buffer_id
+ .and_then(|buffer_id| multi_buffer.buffer(buffer_id)) else { return false };
+ let buffer_snapshot = buffer.read(cx).snapshot();
+ loop {
+ match excerpt_cache.peek() {
+ Some((cached_hint_id, cached_hint)) => {
+ if cached_hint_id == shown_hint_id {
+ excerpt_cache.next();
+ return !new_kinds.contains(&cached_hint.kind);
+ }
+
+ match cached_hint
+ .position
+ .cmp(&shown_anchor.text_anchor, &buffer_snapshot)
+ {
+ cmp::Ordering::Less | cmp::Ordering::Equal => {
+ if !old_kinds.contains(&cached_hint.kind)
+ && new_kinds.contains(&cached_hint.kind)
+ {
+ to_insert.push((
+ multi_buffer_snapshot.anchor_in_excerpt(
+ *excerpt_id,
+ cached_hint.position,
+ ),
+ *cached_hint_id,
+ cached_hint.clone(),
+ ));
+ }
+ excerpt_cache.next();
+ }
+ cmp::Ordering::Greater => return true,
+ }
+ }
+ None => return true,
+ }
+ }
+ });
+
+ for (cached_hint_id, maybe_missed_cached_hint) in excerpt_cache {
+ let cached_hint_kind = maybe_missed_cached_hint.kind;
+ if !old_kinds.contains(&cached_hint_kind) && new_kinds.contains(&cached_hint_kind) {
+ to_insert.push((
+ multi_buffer_snapshot
+ .anchor_in_excerpt(*excerpt_id, maybe_missed_cached_hint.position),
+ *cached_hint_id,
+ maybe_missed_cached_hint.clone(),
+ ));
+ }
+ }
+ }
+
+ to_remove.extend(
+ shown_hints_to_remove
+ .into_values()
+ .flatten()
+ .map(|(_, hint_id)| hint_id),
+ );
+ if to_remove.is_empty() && to_insert.is_empty() {
+ None
+ } else {
+ Some(InlaySplice {
+ to_remove,
+ to_insert,
+ })
+ }
+ }
+
+ fn clear(&mut self) {
+ self.version += 1;
+ self.update_tasks.clear();
+ self.hints.clear();
+ }
+}
+
+fn spawn_new_update_tasks(
+ editor: &mut Editor,
+ excerpts_to_query: HashMap<ExcerptId, (ModelHandle<Buffer>, Range<usize>)>,
+ invalidate: InvalidationStrategy,
+ update_cache_version: usize,
+ cx: &mut ViewContext<'_, '_, Editor>,
+) {
+ let visible_hints = Arc::new(editor.visible_inlay_hints(cx));
+ for (excerpt_id, (buffer_handle, excerpt_visible_range)) in excerpts_to_query {
+ if !excerpt_visible_range.is_empty() {
+ let buffer = buffer_handle.read(cx);
+ let buffer_snapshot = buffer.snapshot();
+ let cached_excerpt_hints = editor.inlay_hint_cache.hints.get(&excerpt_id).cloned();
+ if let Some(cached_excerpt_hints) = &cached_excerpt_hints {
+ let new_task_buffer_version = buffer_snapshot.version();
+ let cached_excerpt_hints = cached_excerpt_hints.read();
+ let cached_buffer_version = &cached_excerpt_hints.buffer_version;
+ if cached_excerpt_hints.version > update_cache_version
+ || cached_buffer_version.changed_since(new_task_buffer_version)
+ {
+ return;
+ }
+ if !new_task_buffer_version.changed_since(&cached_buffer_version)
+ && !matches!(invalidate, InvalidationStrategy::RefreshRequested)
+ {
+ return;
+ }
+ };
+
+ let buffer_id = buffer.remote_id();
+ let excerpt_visible_range_start = buffer.anchor_before(excerpt_visible_range.start);
+ let excerpt_visible_range_end = buffer.anchor_after(excerpt_visible_range.end);
+
+ let (multi_buffer_snapshot, full_excerpt_range) =
+ editor.buffer.update(cx, |multi_buffer, cx| {
+ let multi_buffer_snapshot = multi_buffer.snapshot(cx);
+ (
+ multi_buffer_snapshot,
+ multi_buffer
+ .excerpts_for_buffer(&buffer_handle, cx)
+ .into_iter()
+ .find(|(id, _)| id == &excerpt_id)
+ .map(|(_, range)| range.context),
+ )
+ });
+
+ if let Some(full_excerpt_range) = full_excerpt_range {
+ let query = ExcerptQuery {
+ buffer_id,
+ excerpt_id,
+ dimensions: ExcerptDimensions {
+ excerpt_range_start: full_excerpt_range.start,
+ excerpt_range_end: full_excerpt_range.end,
+ excerpt_visible_range_start,
+ excerpt_visible_range_end,
+ },
+ cache_version: update_cache_version,
+ invalidate,
+ };
+
+ let new_update_task = |is_refresh_after_regular_task| {
+ new_update_task(
+ query,
+ multi_buffer_snapshot,
+ buffer_snapshot,
+ Arc::clone(&visible_hints),
+ cached_excerpt_hints,
+ is_refresh_after_regular_task,
+ cx,
+ )
+ };
+ match editor.inlay_hint_cache.update_tasks.entry(excerpt_id) {
+ hash_map::Entry::Occupied(mut o) => {
+ let update_task = o.get_mut();
+ match (update_task.invalidate, invalidate) {
+ (_, InvalidationStrategy::None) => {}
+ (
+ InvalidationStrategy::ExcerptEdited,
+ InvalidationStrategy::RefreshRequested,
+ ) if !update_task.task.is_running_rx.is_closed() => {
+ update_task.pending_refresh = Some(query);
+ }
+ _ => {
+ o.insert(UpdateTask {
+ invalidate,
+ cache_version: query.cache_version,
+ task: new_update_task(false),
+ pending_refresh: None,
+ });
+ }
+ }
+ }
+ hash_map::Entry::Vacant(v) => {
+ v.insert(UpdateTask {
+ invalidate,
+ cache_version: query.cache_version,
+ task: new_update_task(false),
+ pending_refresh: None,
+ });
+ }
+ }
+ }
+ }
+ }
+}
+
+fn new_update_task(
+ query: ExcerptQuery,
+ multi_buffer_snapshot: MultiBufferSnapshot,
+ buffer_snapshot: BufferSnapshot,
+ visible_hints: Arc<Vec<Inlay>>,
+ cached_excerpt_hints: Option<Arc<RwLock<CachedExcerptHints>>>,
+ is_refresh_after_regular_task: bool,
+ cx: &mut ViewContext<'_, '_, Editor>,
+) -> RunningTask {
+ let hints_fetch_ranges = query.hints_fetch_ranges(&buffer_snapshot);
+ let (is_running_tx, is_running_rx) = smol::channel::bounded(1);
+ let _task = cx.spawn(|editor, mut cx| async move {
+ let _is_running_tx = is_running_tx;
+ let create_update_task = |range| {
+ fetch_and_update_hints(
+ editor.clone(),
+ multi_buffer_snapshot.clone(),
+ buffer_snapshot.clone(),
+ Arc::clone(&visible_hints),
+ cached_excerpt_hints.as_ref().map(Arc::clone),
+ query,
+ range,
+ cx.clone(),
+ )
+ };
+
+ if is_refresh_after_regular_task {
+ let visible_range_has_updates =
+ match create_update_task(hints_fetch_ranges.visible_range).await {
+ Ok(updated) => updated,
+ Err(e) => {
+ error!("inlay hint visible range update task failed: {e:#}");
+ return;
+ }
+ };
+
+ if visible_range_has_updates {
+ let other_update_results = futures::future::join_all(
+ hints_fetch_ranges
+ .other_ranges
+ .into_iter()
+ .map(create_update_task),
+ )
+ .await;
+
+ for result in other_update_results {
+ if let Err(e) = result {
+ error!("inlay hint update task failed: {e:#}");
+ }
+ }
+ }
+ } else {
+ let task_update_results = futures::future::join_all(
+ std::iter::once(hints_fetch_ranges.visible_range)
+ .chain(hints_fetch_ranges.other_ranges.into_iter())
+ .map(create_update_task),
+ )
+ .await;
+
+ for result in task_update_results {
+ if let Err(e) = result {
+ error!("inlay hint update task failed: {e:#}");
+ }
+ }
+ }
+
+ editor
+ .update(&mut cx, |editor, cx| {
+ let pending_refresh_query = editor
+ .inlay_hint_cache
+ .update_tasks
+ .get_mut(&query.excerpt_id)
+ .and_then(|task| task.pending_refresh.take());
+
+ if let Some(pending_refresh_query) = pending_refresh_query {
+ let refresh_multi_buffer = editor.buffer().read(cx);
+ let refresh_multi_buffer_snapshot = refresh_multi_buffer.snapshot(cx);
+ let refresh_visible_hints = Arc::new(editor.visible_inlay_hints(cx));
+ let refresh_cached_excerpt_hints = editor
+ .inlay_hint_cache
+ .hints
+ .get(&pending_refresh_query.excerpt_id)
+ .map(Arc::clone);
+ if let Some(buffer) =
+ refresh_multi_buffer.buffer(pending_refresh_query.buffer_id)
+ {
+ drop(refresh_multi_buffer);
+ editor.inlay_hint_cache.update_tasks.insert(
+ pending_refresh_query.excerpt_id,
+ UpdateTask {
+ invalidate: InvalidationStrategy::RefreshRequested,
+ cache_version: editor.inlay_hint_cache.version,
+ task: new_update_task(
+ pending_refresh_query,
+ refresh_multi_buffer_snapshot,
+ buffer.read(cx).snapshot(),
+ refresh_visible_hints,
+ refresh_cached_excerpt_hints,
+ true,
+ cx,
+ ),
+ pending_refresh: None,
+ },
+ );
+ }
+ }
+ })
+ .ok();
+ });
+
+ RunningTask {
+ _task,
+ is_running_rx,
+ }
+}
+
+async fn fetch_and_update_hints(
+ editor: gpui::WeakViewHandle<Editor>,
+ multi_buffer_snapshot: MultiBufferSnapshot,
+ buffer_snapshot: BufferSnapshot,
+ visible_hints: Arc<Vec<Inlay>>,
+ cached_excerpt_hints: Option<Arc<RwLock<CachedExcerptHints>>>,
+ query: ExcerptQuery,
+ fetch_range: Range<language::Anchor>,
+ mut cx: gpui::AsyncAppContext,
+) -> anyhow::Result<bool> {
+ let inlay_hints_fetch_task = editor
+ .update(&mut cx, |editor, cx| {
+ editor
+ .buffer()
+ .read(cx)
+ .buffer(query.buffer_id)
+ .and_then(|buffer| {
+ let project = editor.project.as_ref()?;
+ Some(project.update(cx, |project, cx| {
+ project.inlay_hints(buffer, fetch_range.clone(), cx)
+ }))
+ })
+ })
+ .ok()
+ .flatten();
+ let mut update_happened = false;
+ let Some(inlay_hints_fetch_task) = inlay_hints_fetch_task else { return Ok(update_happened) };
+ let new_hints = inlay_hints_fetch_task
+ .await
+ .context("inlay hint fetch task")?;
+ let background_task_buffer_snapshot = buffer_snapshot.clone();
+ let backround_fetch_range = fetch_range.clone();
+ let new_update = cx
+ .background()
+ .spawn(async move {
+ calculate_hint_updates(
+ query,
+ backround_fetch_range,
+ new_hints,
+ &background_task_buffer_snapshot,
+ cached_excerpt_hints,
+ &visible_hints,
+ )
+ })
+ .await;
+
+ editor
+ .update(&mut cx, |editor, cx| {
+ if let Some(new_update) = new_update {
+ update_happened = !new_update.add_to_cache.is_empty()
+ || !new_update.remove_from_cache.is_empty()
+ || !new_update.remove_from_visible.is_empty();
+
+ let cached_excerpt_hints = editor
+ .inlay_hint_cache
+ .hints
+ .entry(new_update.excerpt_id)
+ .or_insert_with(|| {
+ Arc::new(RwLock::new(CachedExcerptHints {
+ version: query.cache_version,
+ buffer_version: buffer_snapshot.version().clone(),
+ buffer_id: query.buffer_id,
+ hints: Vec::new(),
+ }))
+ });
+ let mut cached_excerpt_hints = cached_excerpt_hints.write();
+ match query.cache_version.cmp(&cached_excerpt_hints.version) {
+ cmp::Ordering::Less => return,
+ cmp::Ordering::Greater | cmp::Ordering::Equal => {
+ cached_excerpt_hints.version = query.cache_version;
+ }
+ }
+ cached_excerpt_hints
+ .hints
+ .retain(|(hint_id, _)| !new_update.remove_from_cache.contains(hint_id));
+ cached_excerpt_hints.buffer_version = buffer_snapshot.version().clone();
+ editor.inlay_hint_cache.version += 1;
+
+ let mut splice = InlaySplice {
+ to_remove: new_update.remove_from_visible,
+ to_insert: Vec::new(),
+ };
+
+ for new_hint in new_update.add_to_cache {
+ let new_hint_position = multi_buffer_snapshot
+ .anchor_in_excerpt(query.excerpt_id, new_hint.position);
+ let new_inlay_id = InlayId::Hint(post_inc(&mut editor.next_inlay_id));
+ if editor
+ .inlay_hint_cache
+ .allowed_hint_kinds
+ .contains(&new_hint.kind)
+ {
+ splice
+ .to_insert
+ .push((new_hint_position, new_inlay_id, new_hint.clone()));
+ }
+
+ cached_excerpt_hints.hints.push((new_inlay_id, new_hint));
+ }
+
+ cached_excerpt_hints
+ .hints
+ .sort_by(|(_, hint_a), (_, hint_b)| {
+ hint_a.position.cmp(&hint_b.position, &buffer_snapshot)
+ });
+ drop(cached_excerpt_hints);
+
+ if query.invalidate.should_invalidate() {
+ let mut outdated_excerpt_caches = HashSet::default();
+ for (excerpt_id, excerpt_hints) in editor.inlay_hint_cache().hints.iter() {
+ let excerpt_hints = excerpt_hints.read();
+ if excerpt_hints.buffer_id == query.buffer_id
+ && excerpt_id != &query.excerpt_id
+ && buffer_snapshot
+ .version()
+ .changed_since(&excerpt_hints.buffer_version)
+ {
+ outdated_excerpt_caches.insert(*excerpt_id);
+ splice
+ .to_remove
+ .extend(excerpt_hints.hints.iter().map(|(id, _)| id));
+ }
+ }
+ editor
+ .inlay_hint_cache
+ .hints
+ .retain(|excerpt_id, _| !outdated_excerpt_caches.contains(excerpt_id));
+ }
+
+ let InlaySplice {
+ to_remove,
+ to_insert,
+ } = splice;
+ if !to_remove.is_empty() || !to_insert.is_empty() {
+ editor.splice_inlay_hints(to_remove, to_insert, cx)
+ }
+ }
+ })
+ .ok();
+
+ Ok(update_happened)
+}
+
+fn calculate_hint_updates(
+ query: ExcerptQuery,
+ fetch_range: Range<language::Anchor>,
+ new_excerpt_hints: Vec<InlayHint>,
+ buffer_snapshot: &BufferSnapshot,
+ cached_excerpt_hints: Option<Arc<RwLock<CachedExcerptHints>>>,
+ visible_hints: &[Inlay],
+) -> Option<ExcerptHintsUpdate> {
+ let mut add_to_cache: HashSet<InlayHint> = HashSet::default();
+ let mut excerpt_hints_to_persist = HashMap::default();
+ for new_hint in new_excerpt_hints {
+ if !contains_position(&fetch_range, new_hint.position, buffer_snapshot) {
+ continue;
+ }
+ let missing_from_cache = match &cached_excerpt_hints {
+ Some(cached_excerpt_hints) => {
+ let cached_excerpt_hints = cached_excerpt_hints.read();
+ match cached_excerpt_hints.hints.binary_search_by(|probe| {
+ probe.1.position.cmp(&new_hint.position, buffer_snapshot)
+ }) {
+ Ok(ix) => {
+ let (cached_inlay_id, cached_hint) = &cached_excerpt_hints.hints[ix];
+ if cached_hint == &new_hint {
+ excerpt_hints_to_persist.insert(*cached_inlay_id, cached_hint.kind);
+ false
+ } else {
+ true
+ }
+ }
+ Err(_) => true,
+ }
+ }
+ None => true,
+ };
+ if missing_from_cache {
+ add_to_cache.insert(new_hint);
+ }
+ }
+
+ let mut remove_from_visible = Vec::new();
+ let mut remove_from_cache = HashSet::default();
+ if query.invalidate.should_invalidate() {
+ remove_from_visible.extend(
+ visible_hints
+ .iter()
+ .filter(|hint| hint.position.excerpt_id == query.excerpt_id)
+ .filter(|hint| {
+ contains_position(&fetch_range, hint.position.text_anchor, buffer_snapshot)
+ })
+ .filter(|hint| {
+ fetch_range
+ .start
+ .cmp(&hint.position.text_anchor, buffer_snapshot)
+ .is_le()
+ && fetch_range
+ .end
+ .cmp(&hint.position.text_anchor, buffer_snapshot)
+ .is_ge()
+ })
+ .map(|inlay_hint| inlay_hint.id)
+ .filter(|hint_id| !excerpt_hints_to_persist.contains_key(hint_id)),
+ );
+
+ if let Some(cached_excerpt_hints) = &cached_excerpt_hints {
+ let cached_excerpt_hints = cached_excerpt_hints.read();
+ remove_from_cache.extend(
+ cached_excerpt_hints
+ .hints
+ .iter()
+ .filter(|(cached_inlay_id, _)| {
+ !excerpt_hints_to_persist.contains_key(cached_inlay_id)
+ })
+ .filter(|(_, cached_hint)| {
+ fetch_range
+ .start
+ .cmp(&cached_hint.position, buffer_snapshot)
+ .is_le()
+ && fetch_range
+ .end
+ .cmp(&cached_hint.position, buffer_snapshot)
+ .is_ge()
+ })
+ .map(|(cached_inlay_id, _)| *cached_inlay_id),
+ );
+ }
+ }
+
+ if remove_from_visible.is_empty() && remove_from_cache.is_empty() && add_to_cache.is_empty() {
+ None
+ } else {
+ Some(ExcerptHintsUpdate {
+ excerpt_id: query.excerpt_id,
+ remove_from_visible,
+ remove_from_cache,
+ add_to_cache,
+ })
+ }
+}
+
+fn contains_position(
+ range: &Range<language::Anchor>,
+ position: language::Anchor,
+ buffer_snapshot: &BufferSnapshot,
+) -> bool {
+ range.start.cmp(&position, buffer_snapshot).is_le()
+ && range.end.cmp(&position, buffer_snapshot).is_ge()
+}
+
+#[cfg(test)]
+mod tests {
+ use std::sync::atomic::{AtomicBool, AtomicU32, Ordering};
+
+ use crate::{
+ scroll::{autoscroll::Autoscroll, scroll_amount::ScrollAmount},
+ serde_json::json,
+ ExcerptRange, InlayHintSettings,
+ };
+ use futures::StreamExt;
+ use gpui::{executor::Deterministic, TestAppContext, ViewHandle};
+ use language::{
+ language_settings::AllLanguageSettingsContent, FakeLspAdapter, Language, LanguageConfig,
+ };
+ use lsp::FakeLanguageServer;
+ use parking_lot::Mutex;
+ use project::{FakeFs, Project};
+ use settings::SettingsStore;
+ use text::Point;
+ use workspace::Workspace;
+
+ use crate::editor_tests::update_test_settings;
+
+ use super::*;
+
+ #[gpui::test]
+ async fn test_basic_cache_update_with_duplicate_hints(cx: &mut gpui::TestAppContext) {
+ let allowed_hint_kinds = HashSet::from_iter([None, Some(InlayHintKind::Type)]);
+ init_test(cx, |settings| {
+ settings.defaults.inlay_hints = Some(InlayHintSettings {
+ enabled: true,
+ show_type_hints: allowed_hint_kinds.contains(&Some(InlayHintKind::Type)),
+ show_parameter_hints: allowed_hint_kinds.contains(&Some(InlayHintKind::Parameter)),
+ show_other_hints: allowed_hint_kinds.contains(&None),
+ })
+ });
+
+ let (file_with_hints, editor, fake_server) = prepare_test_objects(cx).await;
+ let lsp_request_count = Arc::new(AtomicU32::new(0));
+ fake_server
+ .handle_request::<lsp::request::InlayHintRequest, _, _>(move |params, _| {
+ let task_lsp_request_count = Arc::clone(&lsp_request_count);
+ async move {
+ assert_eq!(
+ params.text_document.uri,
+ lsp::Url::from_file_path(file_with_hints).unwrap(),
+ );
+ let current_call_id =
+ Arc::clone(&task_lsp_request_count).fetch_add(1, Ordering::SeqCst);
+ let mut new_hints = Vec::with_capacity(2 * current_call_id as usize);
+ for _ in 0..2 {
+ let mut i = current_call_id;
+ loop {
+ new_hints.push(lsp::InlayHint {
+ position: lsp::Position::new(0, i),
+ label: lsp::InlayHintLabel::String(i.to_string()),
+ kind: None,
+ text_edits: None,
+ tooltip: None,
+ padding_left: None,
+ padding_right: None,
+ data: None,
+ });
+ if i == 0 {
+ break;
+ }
+ i -= 1;
+ }
+ }
+
+ Ok(Some(new_hints))
+ }
+ })
+ .next()
+ .await;
+ cx.foreground().run_until_parked();
+
+ let mut edits_made = 1;
+ editor.update(cx, |editor, cx| {
+ let expected_layers = vec!["0".to_string()];
+ assert_eq!(
+ expected_layers,
+ cached_hint_labels(editor),
+ "Should get its first hints when opening the editor"
+ );
+ assert_eq!(expected_layers, visible_hint_labels(editor, cx));
+ let inlay_cache = editor.inlay_hint_cache();
+ assert_eq!(
+ inlay_cache.allowed_hint_kinds, allowed_hint_kinds,
+ "Cache should use editor settings to get the allowed hint kinds"
+ );
+ assert_eq!(
+ inlay_cache.version, edits_made,
+ "The editor update the cache version after every cache/view change"
+ );
+ });
+
+ editor.update(cx, |editor, cx| {
+ editor.change_selections(None, cx, |s| s.select_ranges([13..13]));
+ editor.handle_input("some change", cx);
+ edits_made += 1;
+ });
+ cx.foreground().run_until_parked();
+ editor.update(cx, |editor, cx| {
+ let expected_layers = vec!["0".to_string(), "1".to_string()];
+ assert_eq!(
+ expected_layers,
+ cached_hint_labels(editor),
+ "Should get new hints after an edit"
+ );
+ assert_eq!(expected_layers, visible_hint_labels(editor, cx));
+ let inlay_cache = editor.inlay_hint_cache();
+ assert_eq!(
+ inlay_cache.allowed_hint_kinds, allowed_hint_kinds,
+ "Cache should use editor settings to get the allowed hint kinds"
+ );
+ assert_eq!(
+ inlay_cache.version, edits_made,
+ "The editor update the cache version after every cache/view change"
+ );
+ });
+
+ fake_server
+ .request::<lsp::request::InlayHintRefreshRequest>(())
+ .await
+ .expect("inlay refresh request failed");
+ edits_made += 1;
+ cx.foreground().run_until_parked();
+ editor.update(cx, |editor, cx| {
+ let expected_layers = vec!["0".to_string(), "1".to_string(), "2".to_string()];
+ assert_eq!(
+ expected_layers,
+ cached_hint_labels(editor),
+ "Should get new hints after hint refresh/ request"
+ );
+ assert_eq!(expected_layers, visible_hint_labels(editor, cx));
+ let inlay_cache = editor.inlay_hint_cache();
+ assert_eq!(
+ inlay_cache.allowed_hint_kinds, allowed_hint_kinds,
+ "Cache should use editor settings to get the allowed hint kinds"
+ );
+ assert_eq!(
+ inlay_cache.version, edits_made,
+ "The editor update the cache version after every cache/view change"
+ );
+ });
+ }
+
+ #[gpui::test]
+ async fn test_hint_setting_changes(cx: &mut gpui::TestAppContext) {
+ let allowed_hint_kinds = HashSet::from_iter([None, Some(InlayHintKind::Type)]);
+ init_test(cx, |settings| {
+ settings.defaults.inlay_hints = Some(InlayHintSettings {
+ enabled: true,
+ show_type_hints: allowed_hint_kinds.contains(&Some(InlayHintKind::Type)),
+ show_parameter_hints: allowed_hint_kinds.contains(&Some(InlayHintKind::Parameter)),
+ show_other_hints: allowed_hint_kinds.contains(&None),
+ })
+ });
+
+ let (file_with_hints, editor, fake_server) = prepare_test_objects(cx).await;
+ let lsp_request_count = Arc::new(AtomicU32::new(0));
+ let another_lsp_request_count = Arc::clone(&lsp_request_count);
+ fake_server
+ .handle_request::<lsp::request::InlayHintRequest, _, _>(move |params, _| {
+ let task_lsp_request_count = Arc::clone(&another_lsp_request_count);
+ async move {
+ Arc::clone(&task_lsp_request_count).fetch_add(1, Ordering::SeqCst);
+ assert_eq!(
+ params.text_document.uri,
+ lsp::Url::from_file_path(file_with_hints).unwrap(),
+ );
+ Ok(Some(vec![
+ lsp::InlayHint {
+ position: lsp::Position::new(0, 1),
+ label: lsp::InlayHintLabel::String("type hint".to_string()),
+ kind: Some(lsp::InlayHintKind::TYPE),
+ text_edits: None,
+ tooltip: None,
+ padding_left: None,
+ padding_right: None,
+ data: None,
+ },
+ lsp::InlayHint {
+ position: lsp::Position::new(0, 2),
+ label: lsp::InlayHintLabel::String("parameter hint".to_string()),
+ kind: Some(lsp::InlayHintKind::PARAMETER),
+ text_edits: None,
+ tooltip: None,
+ padding_left: None,
+ padding_right: None,
+ data: None,
+ },
+ lsp::InlayHint {
+ position: lsp::Position::new(0, 3),
+ label: lsp::InlayHintLabel::String("other hint".to_string()),
+ kind: None,
+ text_edits: None,
+ tooltip: None,
+ padding_left: None,
+ padding_right: None,
+ data: None,
+ },
+ ]))
+ }
+ })
+ .next()
+ .await;
+ cx.foreground().run_until_parked();
+
+ let mut edits_made = 1;
+ editor.update(cx, |editor, cx| {
+ assert_eq!(
+ lsp_request_count.load(Ordering::Relaxed),
+ 1,
+ "Should query new hints once"
+ );
+ assert_eq!(
+ vec![
+ "other hint".to_string(),
+ "parameter hint".to_string(),
+ "type hint".to_string(),
+ ],
+ cached_hint_labels(editor),
+ "Should get its first hints when opening the editor"
+ );
+ assert_eq!(
+ vec!["other hint".to_string(), "type hint".to_string()],
+ visible_hint_labels(editor, cx)
+ );
+ let inlay_cache = editor.inlay_hint_cache();
+ assert_eq!(
+ inlay_cache.allowed_hint_kinds, allowed_hint_kinds,
+ "Cache should use editor settings to get the allowed hint kinds"
+ );
+ assert_eq!(
+ inlay_cache.version, edits_made,
+ "The editor update the cache version after every cache/view change"
+ );
+ });
+
+ fake_server
+ .request::<lsp::request::InlayHintRefreshRequest>(())
+ .await
+ .expect("inlay refresh request failed");
+ cx.foreground().run_until_parked();
+ editor.update(cx, |editor, cx| {
+ assert_eq!(
+ lsp_request_count.load(Ordering::Relaxed),
+ 2,
+ "Should load new hints twice"
+ );
+ assert_eq!(
+ vec![
+ "other hint".to_string(),
+ "parameter hint".to_string(),
+ "type hint".to_string(),
+ ],
+ cached_hint_labels(editor),
+ "Cached hints should not change due to allowed hint kinds settings update"
+ );
+ assert_eq!(
+ vec!["other hint".to_string(), "type hint".to_string()],
+ visible_hint_labels(editor, cx)
+ );
+ let inlay_cache = editor.inlay_hint_cache();
+ assert_eq!(inlay_cache.allowed_hint_kinds, allowed_hint_kinds);
+ assert_eq!(
+ inlay_cache.version, edits_made,
+ "Should not update cache version due to new loaded hints being the same"
+ );
+ });
+
+ for (new_allowed_hint_kinds, expected_visible_hints) in [
+ (HashSet::from_iter([None]), vec!["other hint".to_string()]),
+ (
+ HashSet::from_iter([Some(InlayHintKind::Type)]),
+ vec!["type hint".to_string()],
+ ),
+ (
+ HashSet::from_iter([Some(InlayHintKind::Parameter)]),
+ vec!["parameter hint".to_string()],
+ ),
+ (
+ HashSet::from_iter([None, Some(InlayHintKind::Type)]),
+ vec!["other hint".to_string(), "type hint".to_string()],
+ ),
+ (
+ HashSet::from_iter([None, Some(InlayHintKind::Parameter)]),
+ vec!["other hint".to_string(), "parameter hint".to_string()],
+ ),
+ (
+ HashSet::from_iter([Some(InlayHintKind::Type), Some(InlayHintKind::Parameter)]),
+ vec!["parameter hint".to_string(), "type hint".to_string()],
+ ),
+ (
+ HashSet::from_iter([
+ None,
+ Some(InlayHintKind::Type),
+ Some(InlayHintKind::Parameter),
+ ]),
+ vec![
+ "other hint".to_string(),
+ "parameter hint".to_string(),
+ "type hint".to_string(),
+ ],
+ ),
+ ] {
+ edits_made += 1;
+ update_test_settings(cx, |settings| {
+ settings.defaults.inlay_hints = Some(InlayHintSettings {
+ enabled: true,
+ show_type_hints: new_allowed_hint_kinds.contains(&Some(InlayHintKind::Type)),
+ show_parameter_hints: new_allowed_hint_kinds
+ .contains(&Some(InlayHintKind::Parameter)),
+ show_other_hints: new_allowed_hint_kinds.contains(&None),
+ })
+ });
+ cx.foreground().run_until_parked();
+ editor.update(cx, |editor, cx| {
+ assert_eq!(
+ lsp_request_count.load(Ordering::Relaxed),
+ 2,
+ "Should not load new hints on allowed hint kinds change for hint kinds {new_allowed_hint_kinds:?}"
+ );
+ assert_eq!(
+ vec![
+ "other hint".to_string(),
+ "parameter hint".to_string(),
+ "type hint".to_string(),
+ ],
+ cached_hint_labels(editor),
+ "Should get its cached hints unchanged after the settings change for hint kinds {new_allowed_hint_kinds:?}"
+ );
+ assert_eq!(
+ expected_visible_hints,
+ visible_hint_labels(editor, cx),
+ "Should get its visible hints filtered after the settings change for hint kinds {new_allowed_hint_kinds:?}"
+ );
+ let inlay_cache = editor.inlay_hint_cache();
+ assert_eq!(
+ inlay_cache.allowed_hint_kinds, new_allowed_hint_kinds,
+ "Cache should use editor settings to get the allowed hint kinds for hint kinds {new_allowed_hint_kinds:?}"
+ );
+ assert_eq!(
+ inlay_cache.version, edits_made,
+ "The editor should update the cache version after every cache/view change for hint kinds {new_allowed_hint_kinds:?} due to visible hints change"
+ );
+ });
+ }
+
+ edits_made += 1;
+ let another_allowed_hint_kinds = HashSet::from_iter([Some(InlayHintKind::Type)]);
+ update_test_settings(cx, |settings| {
+ settings.defaults.inlay_hints = Some(InlayHintSettings {
+ enabled: false,
+ show_type_hints: another_allowed_hint_kinds.contains(&Some(InlayHintKind::Type)),
+ show_parameter_hints: another_allowed_hint_kinds
+ .contains(&Some(InlayHintKind::Parameter)),
+ show_other_hints: another_allowed_hint_kinds.contains(&None),
+ })
+ });
+ cx.foreground().run_until_parked();
+ editor.update(cx, |editor, cx| {
+ assert_eq!(
+ lsp_request_count.load(Ordering::Relaxed),
+ 2,
+ "Should not load new hints when hints got disabled"
+ );
+ assert!(
+ cached_hint_labels(editor).is_empty(),
+ "Should clear the cache when hints got disabled"
+ );
+ assert!(
+ visible_hint_labels(editor, cx).is_empty(),
+ "Should clear visible hints when hints got disabled"
+ );
+ let inlay_cache = editor.inlay_hint_cache();
+ assert_eq!(
+ inlay_cache.allowed_hint_kinds, another_allowed_hint_kinds,
+ "Should update its allowed hint kinds even when hints got disabled"
+ );
+ assert_eq!(
+ inlay_cache.version, edits_made,
+ "The editor should update the cache version after hints got disabled"
+ );
+ });
+
+ fake_server
+ .request::<lsp::request::InlayHintRefreshRequest>(())
+ .await
+ .expect("inlay refresh request failed");
+ cx.foreground().run_until_parked();
+ editor.update(cx, |editor, cx| {
+ assert_eq!(
+ lsp_request_count.load(Ordering::Relaxed),
+ 2,
+ "Should not load new hints when they got disabled"
+ );
+ assert!(cached_hint_labels(editor).is_empty());
+ assert!(visible_hint_labels(editor, cx).is_empty());
+ let inlay_cache = editor.inlay_hint_cache();
+ assert_eq!(inlay_cache.allowed_hint_kinds, another_allowed_hint_kinds);
+ assert_eq!(
+ inlay_cache.version, edits_made,
+ "The editor should not update the cache version after /refresh query without updates"
+ );
+ });
+
+ let final_allowed_hint_kinds = HashSet::from_iter([Some(InlayHintKind::Parameter)]);
+ edits_made += 1;
+ update_test_settings(cx, |settings| {
+ settings.defaults.inlay_hints = Some(InlayHintSettings {
+ enabled: true,
+ show_type_hints: final_allowed_hint_kinds.contains(&Some(InlayHintKind::Type)),
+ show_parameter_hints: final_allowed_hint_kinds
+ .contains(&Some(InlayHintKind::Parameter)),
+ show_other_hints: final_allowed_hint_kinds.contains(&None),
+ })
+ });
+ cx.foreground().run_until_parked();
+ editor.update(cx, |editor, cx| {
+ assert_eq!(
+ lsp_request_count.load(Ordering::Relaxed),
+ 3,
+ "Should query for new hints when they got reenabled"
+ );
+ assert_eq!(
+ vec![
+ "other hint".to_string(),
+ "parameter hint".to_string(),
+ "type hint".to_string(),
+ ],
+ cached_hint_labels(editor),
+ "Should get its cached hints fully repopulated after the hints got reenabled"
+ );
+ assert_eq!(
+ vec!["parameter hint".to_string()],
+ visible_hint_labels(editor, cx),
+ "Should get its visible hints repopulated and filtered after the h"
+ );
+ let inlay_cache = editor.inlay_hint_cache();
+ assert_eq!(
+ inlay_cache.allowed_hint_kinds, final_allowed_hint_kinds,
+ "Cache should update editor settings when hints got reenabled"
+ );
+ assert_eq!(
+ inlay_cache.version, edits_made,
+ "Cache should update its version after hints got reenabled"
+ );
+ });
+
+ fake_server
+ .request::<lsp::request::InlayHintRefreshRequest>(())
+ .await
+ .expect("inlay refresh request failed");
+ cx.foreground().run_until_parked();
+ editor.update(cx, |editor, cx| {
+ assert_eq!(
+ lsp_request_count.load(Ordering::Relaxed),
+ 4,
+ "Should query for new hints again"
+ );
+ assert_eq!(
+ vec![
+ "other hint".to_string(),
+ "parameter hint".to_string(),
+ "type hint".to_string(),
+ ],
+ cached_hint_labels(editor),
+ );
+ assert_eq!(
+ vec!["parameter hint".to_string()],
+ visible_hint_labels(editor, cx),
+ );
+ let inlay_cache = editor.inlay_hint_cache();
+ assert_eq!(inlay_cache.allowed_hint_kinds, final_allowed_hint_kinds);
+ assert_eq!(inlay_cache.version, edits_made);
+ });
+ }
+
+ #[gpui::test]
+ async fn test_hint_request_cancellation(cx: &mut gpui::TestAppContext) {
+ let allowed_hint_kinds = HashSet::from_iter([None]);
+ init_test(cx, |settings| {
+ settings.defaults.inlay_hints = Some(InlayHintSettings {
+ enabled: true,
+ show_type_hints: allowed_hint_kinds.contains(&Some(InlayHintKind::Type)),
+ show_parameter_hints: allowed_hint_kinds.contains(&Some(InlayHintKind::Parameter)),
+ show_other_hints: allowed_hint_kinds.contains(&None),
+ })
+ });
+
+ let (file_with_hints, editor, fake_server) = prepare_test_objects(cx).await;
+ let fake_server = Arc::new(fake_server);
+ let lsp_request_count = Arc::new(AtomicU32::new(0));
+ let another_lsp_request_count = Arc::clone(&lsp_request_count);
+ fake_server
+ .handle_request::<lsp::request::InlayHintRequest, _, _>(move |params, _| {
+ let task_lsp_request_count = Arc::clone(&another_lsp_request_count);
+ async move {
+ let i = Arc::clone(&task_lsp_request_count).fetch_add(1, Ordering::SeqCst) + 1;
+ assert_eq!(
+ params.text_document.uri,
+ lsp::Url::from_file_path(file_with_hints).unwrap(),
+ );
+ Ok(Some(vec![lsp::InlayHint {
+ position: lsp::Position::new(0, i),
+ label: lsp::InlayHintLabel::String(i.to_string()),
+ kind: None,
+ text_edits: None,
+ tooltip: None,
+ padding_left: None,
+ padding_right: None,
+ data: None,
+ }]))
+ }
+ })
+ .next()
+ .await;
+
+ let mut expected_changes = Vec::new();
+ for change_after_opening in [
+ "initial change #1",
+ "initial change #2",
+ "initial change #3",
+ ] {
+ editor.update(cx, |editor, cx| {
+ editor.change_selections(None, cx, |s| s.select_ranges([13..13]));
+ editor.handle_input(change_after_opening, cx);
+ });
+ expected_changes.push(change_after_opening);
+ }
+
+ cx.foreground().run_until_parked();
+
+ editor.update(cx, |editor, cx| {
+ let current_text = editor.text(cx);
+ for change in &expected_changes {
+ assert!(
+ current_text.contains(change),
+ "Should apply all changes made"
+ );
+ }
+ assert_eq!(
+ lsp_request_count.load(Ordering::Relaxed),
+ 2,
+ "Should query new hints twice: for editor init and for the last edit that interrupted all others"
+ );
+ let expected_hints = vec!["2".to_string()];
+ assert_eq!(
+ expected_hints,
+ cached_hint_labels(editor),
+ "Should get hints from the last edit landed only"
+ );
+ assert_eq!(expected_hints, visible_hint_labels(editor, cx));
+ let inlay_cache = editor.inlay_hint_cache();
+ assert_eq!(inlay_cache.allowed_hint_kinds, allowed_hint_kinds);
+ assert_eq!(
+ inlay_cache.version, 1,
+ "Only one update should be registered in the cache after all cancellations"
+ );
+ });
+
+ let mut edits = Vec::new();
+ for async_later_change in [
+ "another change #1",
+ "another change #2",
+ "another change #3",
+ ] {
+ expected_changes.push(async_later_change);
+ let task_editor = editor.clone();
+ let mut task_cx = cx.clone();
+ edits.push(cx.foreground().spawn(async move {
+ task_editor.update(&mut task_cx, |editor, cx| {
+ editor.change_selections(None, cx, |s| s.select_ranges([13..13]));
+ editor.handle_input(async_later_change, cx);
+ });
+ }));
+ }
+ let _ = futures::future::join_all(edits).await;
+ cx.foreground().run_until_parked();
+
+ editor.update(cx, |editor, cx| {
+ let current_text = editor.text(cx);
+ for change in &expected_changes {
+ assert!(
+ current_text.contains(change),
+ "Should apply all changes made"
+ );
+ }
+ assert_eq!(
+ lsp_request_count.load(Ordering::SeqCst),
+ 3,
+ "Should query new hints one more time, for the last edit only"
+ );
+ let expected_hints = vec!["3".to_string()];
+ assert_eq!(
+ expected_hints,
+ cached_hint_labels(editor),
+ "Should get hints from the last edit landed only"
+ );
+ assert_eq!(expected_hints, visible_hint_labels(editor, cx));
+ let inlay_cache = editor.inlay_hint_cache();
+ assert_eq!(inlay_cache.allowed_hint_kinds, allowed_hint_kinds);
+ assert_eq!(
+ inlay_cache.version, 2,
+ "Should update the cache version once more, for the new change"
+ );
+ });
+ }
+
+ #[gpui::test]
+ async fn test_large_buffer_inlay_requests_split(cx: &mut gpui::TestAppContext) {
+ let allowed_hint_kinds = HashSet::from_iter([None, Some(InlayHintKind::Type)]);
+ init_test(cx, |settings| {
+ settings.defaults.inlay_hints = Some(InlayHintSettings {
+ enabled: true,
+ show_type_hints: allowed_hint_kinds.contains(&Some(InlayHintKind::Type)),
+ show_parameter_hints: allowed_hint_kinds.contains(&Some(InlayHintKind::Parameter)),
+ show_other_hints: allowed_hint_kinds.contains(&None),
+ })
+ });
+
+ let mut language = Language::new(
+ LanguageConfig {
+ name: "Rust".into(),
+ path_suffixes: vec!["rs".to_string()],
+ ..Default::default()
+ },
+ Some(tree_sitter_rust::language()),
+ );
+ let mut fake_servers = language
+ .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
+ capabilities: lsp::ServerCapabilities {
+ inlay_hint_provider: Some(lsp::OneOf::Left(true)),
+ ..Default::default()
+ },
+ ..Default::default()
+ }))
+ .await;
+ let fs = FakeFs::new(cx.background());
+ fs.insert_tree(
+ "/a",
+ json!({
+ "main.rs": format!("fn main() {{\n{}\n}}", "let i = 5;\n".repeat(500)),
+ "other.rs": "// Test file",
+ }),
+ )
+ .await;
+ let project = Project::test(fs, ["/a".as_ref()], cx).await;
+ project.update(cx, |project, _| project.languages().add(Arc::new(language)));
+ let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
+ let worktree_id = workspace.update(cx, |workspace, cx| {
+ workspace.project().read_with(cx, |project, cx| {
+ project.worktrees(cx).next().unwrap().read(cx).id()
+ })
+ });
+
+ let _buffer = project
+ .update(cx, |project, cx| {
+ project.open_local_buffer("/a/main.rs", cx)
+ })
+ .await
+ .unwrap();
+ cx.foreground().run_until_parked();
+ cx.foreground().start_waiting();
+ let fake_server = fake_servers.next().await.unwrap();
+ let editor = workspace
+ .update(cx, |workspace, cx| {
+ workspace.open_path((worktree_id, "main.rs"), None, true, cx)
+ })
+ .await
+ .unwrap()
+ .downcast::<Editor>()
+ .unwrap();
+ let lsp_request_ranges = Arc::new(Mutex::new(Vec::new()));
+ let lsp_request_count = Arc::new(AtomicU32::new(0));
+ let closure_lsp_request_ranges = Arc::clone(&lsp_request_ranges);
+ let closure_lsp_request_count = Arc::clone(&lsp_request_count);
+ fake_server
+ .handle_request::<lsp::request::InlayHintRequest, _, _>(move |params, _| {
+ let task_lsp_request_ranges = Arc::clone(&closure_lsp_request_ranges);
+ let task_lsp_request_count = Arc::clone(&closure_lsp_request_count);
+ async move {
+ assert_eq!(
+ params.text_document.uri,
+ lsp::Url::from_file_path("/a/main.rs").unwrap(),
+ );
+
+ task_lsp_request_ranges.lock().push(params.range);
+ let query_start = params.range.start;
+ let i = Arc::clone(&task_lsp_request_count).fetch_add(1, Ordering::SeqCst) + 1;
+ Ok(Some(vec![lsp::InlayHint {
+ position: query_start,
+ label: lsp::InlayHintLabel::String(i.to_string()),
+ kind: None,
+ text_edits: None,
+ tooltip: None,
+ padding_left: None,
+ padding_right: None,
+ data: None,
+ }]))
+ }
+ })
+ .next()
+ .await;
+ cx.foreground().run_until_parked();
+ editor.update(cx, |editor, cx| {
+ let mut ranges = lsp_request_ranges.lock().drain(..).collect::<Vec<_>>();
+ ranges.sort_by_key(|range| range.start);
+ assert_eq!(ranges.len(), 2, "When scroll is at the edge of a big document, its visible part + the rest should be queried for hints");
+ assert_eq!(ranges[0].start, lsp::Position::new(0, 0), "Should query from the beginning of the document");
+ assert_eq!(ranges[0].end.line, ranges[1].start.line, "Both requests should be on the same line");
+ assert_eq!(ranges[0].end.character + 1, ranges[1].start.character, "Both request should be concequent");
+
+ assert_eq!(lsp_request_count.load(Ordering::SeqCst), 2,
+ "When scroll is at the edge of a big document, its visible part + the rest should be queried for hints");
+ let expected_layers = vec!["1".to_string(), "2".to_string()];
+ assert_eq!(
+ expected_layers,
+ cached_hint_labels(editor),
+ "Should have hints from both LSP requests made for a big file"
+ );
+ assert_eq!(expected_layers, visible_hint_labels(editor, cx));
+ let inlay_cache = editor.inlay_hint_cache();
+ assert_eq!(inlay_cache.allowed_hint_kinds, allowed_hint_kinds);
+ assert_eq!(
+ inlay_cache.version, 2,
+ "Both LSP queries should've bumped the cache version"
+ );
+ });
+
+ editor.update(cx, |editor, cx| {
+ editor.scroll_screen(&ScrollAmount::Page(1.0), cx);
+ editor.scroll_screen(&ScrollAmount::Page(1.0), cx);
+ editor.change_selections(None, cx, |s| s.select_ranges([600..600]));
+ editor.handle_input("++++more text++++", cx);
+ });
+
+ cx.foreground().run_until_parked();
+ editor.update(cx, |editor, cx| {
+ let mut ranges = lsp_request_ranges.lock().drain(..).collect::<Vec<_>>();
+ ranges.sort_by_key(|range| range.start);
+ assert_eq!(ranges.len(), 3, "When scroll is at the middle of a big document, its visible part + 2 other inbisible parts should be queried for hints");
+ assert_eq!(ranges[0].start, lsp::Position::new(0, 0), "Should query from the beginning of the document");
+ assert_eq!(ranges[0].end.line + 1, ranges[1].start.line, "Neighbour requests got on different lines due to the line end");
+ assert_ne!(ranges[0].end.character, 0, "First query was in the end of the line, not in the beginning");
+ assert_eq!(ranges[1].start.character, 0, "Second query got pushed into a new line and starts from the beginning");
+ assert_eq!(ranges[1].end.line, ranges[2].start.line, "Neighbour requests should be on the same line");
+ assert_eq!(ranges[1].end.character + 1, ranges[2].start.character, "Neighbour request should be concequent");
+
+ assert_eq!(lsp_request_count.load(Ordering::SeqCst), 5,
+ "When scroll not at the edge of a big document, visible part + 2 other parts should be queried for hints");
+ let expected_layers = vec!["3".to_string(), "4".to_string(), "5".to_string()];
+ assert_eq!(expected_layers, cached_hint_labels(editor),
+ "Should have hints from the new LSP response after edit");
+ assert_eq!(expected_layers, visible_hint_labels(editor, cx));
+ let inlay_cache = editor.inlay_hint_cache();
+ assert_eq!(inlay_cache.allowed_hint_kinds, allowed_hint_kinds);
+ assert_eq!(inlay_cache.version, 5, "Should update the cache for every LSP response with hints added");
+ });
+ }
+
+ #[gpui::test]
+ async fn test_multiple_excerpts_large_multibuffer(
+ deterministic: Arc<Deterministic>,
+ cx: &mut gpui::TestAppContext,
+ ) {
+ let allowed_hint_kinds = HashSet::from_iter([None, Some(InlayHintKind::Type)]);
+ init_test(cx, |settings| {
+ settings.defaults.inlay_hints = Some(InlayHintSettings {
+ enabled: true,
+ show_type_hints: allowed_hint_kinds.contains(&Some(InlayHintKind::Type)),
+ show_parameter_hints: allowed_hint_kinds.contains(&Some(InlayHintKind::Parameter)),
+ show_other_hints: allowed_hint_kinds.contains(&None),
+ })
+ });
+
+ let mut language = Language::new(
+ LanguageConfig {
+ name: "Rust".into(),
+ path_suffixes: vec!["rs".to_string()],
+ ..Default::default()
+ },
+ Some(tree_sitter_rust::language()),
+ );
+ let mut fake_servers = language
+ .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
+ capabilities: lsp::ServerCapabilities {
+ inlay_hint_provider: Some(lsp::OneOf::Left(true)),
+ ..Default::default()
+ },
+ ..Default::default()
+ }))
+ .await;
+ let language = Arc::new(language);
+ let fs = FakeFs::new(cx.background());
+ fs.insert_tree(
+ "/a",
+ json!({
+ "main.rs": format!("fn main() {{\n{}\n}}", (0..501).map(|i| format!("let i = {i};\n")).collect::<Vec<_>>().join("")),
+ "other.rs": format!("fn main() {{\n{}\n}}", (0..501).map(|j| format!("let j = {j};\n")).collect::<Vec<_>>().join("")),
+ }),
+ )
+ .await;
+ let project = Project::test(fs, ["/a".as_ref()], cx).await;
+ project.update(cx, |project, _| {
+ project.languages().add(Arc::clone(&language))
+ });
+ let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
+ let worktree_id = workspace.update(cx, |workspace, cx| {
+ workspace.project().read_with(cx, |project, cx| {
+ project.worktrees(cx).next().unwrap().read(cx).id()
+ })
+ });
+
+ let buffer_1 = project
+ .update(cx, |project, cx| {
+ project.open_buffer((worktree_id, "main.rs"), cx)
+ })
+ .await
+ .unwrap();
+ let buffer_2 = project
+ .update(cx, |project, cx| {
+ project.open_buffer((worktree_id, "other.rs"), cx)
+ })
+ .await
+ .unwrap();
+ let multibuffer = cx.add_model(|cx| {
+ let mut multibuffer = MultiBuffer::new(0);
+ multibuffer.push_excerpts(
+ buffer_1.clone(),
+ [
+ ExcerptRange {
+ context: Point::new(0, 0)..Point::new(2, 0),
+ primary: None,
+ },
+ ExcerptRange {
+ context: Point::new(4, 0)..Point::new(11, 0),
+ primary: None,
+ },
+ ExcerptRange {
+ context: Point::new(22, 0)..Point::new(33, 0),
+ primary: None,
+ },
+ ExcerptRange {
+ context: Point::new(44, 0)..Point::new(55, 0),
+ primary: None,
+ },
+ ExcerptRange {
+ context: Point::new(56, 0)..Point::new(66, 0),
+ primary: None,
+ },
+ ExcerptRange {
+ context: Point::new(67, 0)..Point::new(77, 0),
+ primary: None,
+ },
+ ],
+ cx,
+ );
+ multibuffer.push_excerpts(
+ buffer_2.clone(),
+ [
+ ExcerptRange {
+ context: Point::new(0, 1)..Point::new(2, 1),
+ primary: None,
+ },
+ ExcerptRange {
+ context: Point::new(4, 1)..Point::new(11, 1),
+ primary: None,
+ },
+ ExcerptRange {
+ context: Point::new(22, 1)..Point::new(33, 1),
+ primary: None,
+ },
+ ExcerptRange {
+ context: Point::new(44, 1)..Point::new(55, 1),
+ primary: None,
+ },
+ ExcerptRange {
+ context: Point::new(56, 1)..Point::new(66, 1),
+ primary: None,
+ },
+ ExcerptRange {
+ context: Point::new(67, 1)..Point::new(77, 1),
+ primary: None,
+ },
+ ],
+ cx,
+ );
+ multibuffer
+ });
+
+ deterministic.run_until_parked();
+ cx.foreground().run_until_parked();
+ let (_, editor) =
+ cx.add_window(|cx| Editor::for_multibuffer(multibuffer, Some(project.clone()), cx));
+ let editor_edited = Arc::new(AtomicBool::new(false));
+ let fake_server = fake_servers.next().await.unwrap();
+ let closure_editor_edited = Arc::clone(&editor_edited);
+ fake_server
+ .handle_request::<lsp::request::InlayHintRequest, _, _>(move |params, _| {
+ let task_editor_edited = Arc::clone(&closure_editor_edited);
+ async move {
+ let hint_text = if params.text_document.uri
+ == lsp::Url::from_file_path("/a/main.rs").unwrap()
+ {
+ "main hint"
+ } else if params.text_document.uri
+ == lsp::Url::from_file_path("/a/other.rs").unwrap()
+ {
+ "other hint"
+ } else {
+ panic!("unexpected uri: {:?}", params.text_document.uri);
+ };
+
+ let positions = [
+ lsp::Position::new(0, 2),
+ lsp::Position::new(4, 2),
+ lsp::Position::new(22, 2),
+ lsp::Position::new(44, 2),
+ lsp::Position::new(56, 2),
+ lsp::Position::new(67, 2),
+ ];
+ let out_of_range_hint = lsp::InlayHint {
+ position: lsp::Position::new(
+ params.range.start.line + 99,
+ params.range.start.character + 99,
+ ),
+ label: lsp::InlayHintLabel::String(
+ "out of excerpt range, should be ignored".to_string(),
+ ),
+ kind: None,
+ text_edits: None,
+ tooltip: None,
+ padding_left: None,
+ padding_right: None,
+ data: None,
+ };
+
+ let edited = task_editor_edited.load(Ordering::Acquire);
+ Ok(Some(
+ std::iter::once(out_of_range_hint)
+ .chain(positions.into_iter().enumerate().map(|(i, position)| {
+ lsp::InlayHint {
+ position,
+ label: lsp::InlayHintLabel::String(format!(
+ "{hint_text}{} #{i}",
+ if edited { "(edited)" } else { "" },
+ )),
+ kind: None,
+ text_edits: None,
+ tooltip: None,
+ padding_left: None,
+ padding_right: None,
+ data: None,
+ }
+ }))
+ .collect(),
+ ))
+ }
+ })
+ .next()
+ .await;
+ cx.foreground().run_until_parked();
+
+ editor.update(cx, |editor, cx| {
+ let expected_layers = vec![
+ "main hint #0".to_string(),
+ "main hint #1".to_string(),
+ "main hint #2".to_string(),
+ "main hint #3".to_string(),
+ ];
+ assert_eq!(
+ expected_layers,
+ cached_hint_labels(editor),
+ "When scroll is at the edge of a multibuffer, its visible excerpts only should be queried for inlay hints"
+ );
+ assert_eq!(expected_layers, visible_hint_labels(editor, cx));
+ let inlay_cache = editor.inlay_hint_cache();
+ assert_eq!(inlay_cache.allowed_hint_kinds, allowed_hint_kinds);
+ assert_eq!(inlay_cache.version, 4, "Every visible excerpt hints should bump the verison");
+ });
+
+ editor.update(cx, |editor, cx| {
+ editor.change_selections(Some(Autoscroll::Next), cx, |s| {
+ s.select_ranges([Point::new(4, 0)..Point::new(4, 0)])
+ });
+ editor.change_selections(Some(Autoscroll::Next), cx, |s| {
+ s.select_ranges([Point::new(22, 0)..Point::new(22, 0)])
+ });
+ editor.change_selections(Some(Autoscroll::Next), cx, |s| {
+ s.select_ranges([Point::new(56, 0)..Point::new(56, 0)])
+ });
+ });
+ cx.foreground().run_until_parked();
+ editor.update(cx, |editor, cx| {
+ let expected_layers = vec![
+ "main hint #0".to_string(),
+ "main hint #1".to_string(),
+ "main hint #2".to_string(),
+ "main hint #3".to_string(),
+ "main hint #4".to_string(),
+ "main hint #5".to_string(),
+ "other hint #0".to_string(),
+ "other hint #1".to_string(),
+ "other hint #2".to_string(),
+ ];
+ assert_eq!(expected_layers, cached_hint_labels(editor),
+ "With more scrolls of the multibuffer, more hints should be added into the cache and nothing invalidated without edits");
+ assert_eq!(expected_layers, visible_hint_labels(editor, cx));
+ let inlay_cache = editor.inlay_hint_cache();
+ assert_eq!(inlay_cache.allowed_hint_kinds, allowed_hint_kinds);
+ assert_eq!(inlay_cache.version, 9);
+ });
+
+ editor.update(cx, |editor, cx| {
+ editor.change_selections(Some(Autoscroll::Next), cx, |s| {
+ s.select_ranges([Point::new(100, 0)..Point::new(100, 0)])
+ });
+ });
+ cx.foreground().run_until_parked();
+ editor.update(cx, |editor, cx| {
+ let expected_layers = vec![
+ "main hint #0".to_string(),
+ "main hint #1".to_string(),
+ "main hint #2".to_string(),
+ "main hint #3".to_string(),
+ "main hint #4".to_string(),
+ "main hint #5".to_string(),
+ "other hint #0".to_string(),
+ "other hint #1".to_string(),
+ "other hint #2".to_string(),
+ "other hint #3".to_string(),
+ "other hint #4".to_string(),
+ "other hint #5".to_string(),
+ ];
+ assert_eq!(expected_layers, cached_hint_labels(editor),
+ "After multibuffer was scrolled to the end, all hints for all excerpts should be fetched");
+ assert_eq!(expected_layers, visible_hint_labels(editor, cx));
+ let inlay_cache = editor.inlay_hint_cache();
+ assert_eq!(inlay_cache.allowed_hint_kinds, allowed_hint_kinds);
+ assert_eq!(inlay_cache.version, 12);
+ });
+
+ editor.update(cx, |editor, cx| {
+ editor.change_selections(Some(Autoscroll::Next), cx, |s| {
+ s.select_ranges([Point::new(4, 0)..Point::new(4, 0)])
+ });
+ });
+ cx.foreground().run_until_parked();
+ editor.update(cx, |editor, cx| {
+ let expected_layers = vec![
+ "main hint #0".to_string(),
+ "main hint #1".to_string(),
+ "main hint #2".to_string(),
+ "main hint #3".to_string(),
+ "main hint #4".to_string(),
+ "main hint #5".to_string(),
+ "other hint #0".to_string(),
+ "other hint #1".to_string(),
+ "other hint #2".to_string(),
+ "other hint #3".to_string(),
+ "other hint #4".to_string(),
+ "other hint #5".to_string(),
+ ];
+ assert_eq!(expected_layers, cached_hint_labels(editor),
+ "After multibuffer was scrolled to the end, further scrolls up should not bring more hints");
+ assert_eq!(expected_layers, visible_hint_labels(editor, cx));
+ let inlay_cache = editor.inlay_hint_cache();
+ assert_eq!(inlay_cache.allowed_hint_kinds, allowed_hint_kinds);
+ assert_eq!(inlay_cache.version, 12, "No updates should happen during scrolling already scolled buffer");
+ });
+
+ editor_edited.store(true, Ordering::Release);
+ editor.update(cx, |editor, cx| {
+ editor.handle_input("++++more text++++", cx);
+ });
+ cx.foreground().run_until_parked();
+ editor.update(cx, |editor, cx| {
+ let expected_layers = vec![
+ "main hint(edited) #0".to_string(),
+ "main hint(edited) #1".to_string(),
+ "main hint(edited) #2".to_string(),
+ "main hint(edited) #3".to_string(),
+ "other hint #0".to_string(),
+ "other hint #1".to_string(),
+ "other hint #2".to_string(),
+ "other hint #3".to_string(),
+ "other hint #4".to_string(),
+ "other hint #5".to_string(),
+ ];
+ assert_eq!(expected_layers, cached_hint_labels(editor),
+ "After multibuffer was edited, hints for the edited buffer (1st) should be invalidated and requeried for all of its visible excerpts, \
+unedited (2nd) buffer should have the same hint");
+ assert_eq!(expected_layers, visible_hint_labels(editor, cx));
+ let inlay_cache = editor.inlay_hint_cache();
+ assert_eq!(inlay_cache.allowed_hint_kinds, allowed_hint_kinds);
+ assert_eq!(inlay_cache.version, 16);
+ });
+ }
+
+ pub(crate) fn init_test(cx: &mut TestAppContext, f: impl Fn(&mut AllLanguageSettingsContent)) {
+ cx.foreground().forbid_parking();
+
+ cx.update(|cx| {
+ cx.set_global(SettingsStore::test(cx));
+ theme::init((), cx);
+ client::init_settings(cx);
+ language::init(cx);
+ Project::init_settings(cx);
+ workspace::init_settings(cx);
+ crate::init(cx);
+ });
+
+ update_test_settings(cx, f);
+ }
+
+ async fn prepare_test_objects(
+ cx: &mut TestAppContext,
+ ) -> (&'static str, ViewHandle<Editor>, FakeLanguageServer) {
+ let mut language = Language::new(
+ LanguageConfig {
+ name: "Rust".into(),
+ path_suffixes: vec!["rs".to_string()],
+ ..Default::default()
+ },
+ Some(tree_sitter_rust::language()),
+ );
+ let mut fake_servers = language
+ .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
+ capabilities: lsp::ServerCapabilities {
+ inlay_hint_provider: Some(lsp::OneOf::Left(true)),
+ ..Default::default()
+ },
+ ..Default::default()
+ }))
+ .await;
+
+ let fs = FakeFs::new(cx.background());
+ fs.insert_tree(
+ "/a",
+ json!({
+ "main.rs": "fn main() { a } // and some long comment to ensure inlays are not trimmed out",
+ "other.rs": "// Test file",
+ }),
+ )
+ .await;
+
+ let project = Project::test(fs, ["/a".as_ref()], cx).await;
+ project.update(cx, |project, _| project.languages().add(Arc::new(language)));
+ let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
+ let worktree_id = workspace.update(cx, |workspace, cx| {
+ workspace.project().read_with(cx, |project, cx| {
+ project.worktrees(cx).next().unwrap().read(cx).id()
+ })
+ });
+
+ let _buffer = project
+ .update(cx, |project, cx| {
+ project.open_local_buffer("/a/main.rs", cx)
+ })
+ .await
+ .unwrap();
+ cx.foreground().run_until_parked();
+ cx.foreground().start_waiting();
+ let fake_server = fake_servers.next().await.unwrap();
+ let editor = workspace
+ .update(cx, |workspace, cx| {
+ workspace.open_path((worktree_id, "main.rs"), None, true, cx)
+ })
+ .await
+ .unwrap()
+ .downcast::<Editor>()
+ .unwrap();
+
+ ("/a/main.rs", editor, fake_server)
+ }
+
+ fn cached_hint_labels(editor: &Editor) -> Vec<String> {
+ let mut labels = Vec::new();
+ for (_, excerpt_hints) in &editor.inlay_hint_cache().hints {
+ let excerpt_hints = excerpt_hints.read();
+ for (_, inlay) in excerpt_hints.hints.iter() {
+ match &inlay.label {
+ project::InlayHintLabel::String(s) => labels.push(s.to_string()),
+ _ => unreachable!(),
+ }
+ }
+ }
+
+ labels.sort();
+ labels
+ }
+
+ fn visible_hint_labels(editor: &Editor, cx: &ViewContext<'_, '_, Editor>) -> Vec<String> {
+ let mut hints = editor
+ .visible_inlay_hints(cx)
+ .into_iter()
+ .map(|hint| hint.text.to_string())
+ .collect::<Vec<_>>();
+ hints.sort();
+ hints
+ }
+}
@@ -49,6 +49,10 @@ impl Anchor {
}
}
+ pub fn bias(&self) -> Bias {
+ self.text_anchor.bias
+ }
+
pub fn bias_left(&self, snapshot: &MultiBufferSnapshot) -> Anchor {
if self.text_anchor.bias != Bias::Left {
if let Some(excerpt) = snapshot.excerpt(self.excerpt_id) {
@@ -81,6 +85,19 @@ impl Anchor {
{
snapshot.summary_for_anchor(self)
}
+
+ pub fn is_valid(&self, snapshot: &MultiBufferSnapshot) -> bool {
+ if *self == Anchor::min() || *self == Anchor::max() {
+ true
+ } else if let Some(excerpt) = snapshot.excerpt(self.excerpt_id) {
+ excerpt.contains(self)
+ && (self.text_anchor == excerpt.range.context.start
+ || self.text_anchor == excerpt.range.context.end
+ || self.text_anchor.is_valid(&excerpt.buffer))
+ } else {
+ false
+ }
+ }
}
impl ToOffset for Anchor {
@@ -13,13 +13,14 @@ use gpui::{
};
use language::{Bias, Point};
use util::ResultExt;
-use workspace::WorkspaceId;
+use workspace::{item::Item, WorkspaceId};
use crate::{
display_map::{DisplaySnapshot, ToDisplayPoint},
hover_popover::hide_hover,
persistence::DB,
- Anchor, DisplayPoint, Editor, EditorMode, Event, MultiBufferSnapshot, ToPoint,
+ Anchor, DisplayPoint, Editor, EditorMode, Event, InlayRefreshReason, MultiBufferSnapshot,
+ ToPoint,
};
use self::{
@@ -293,8 +294,19 @@ impl Editor {
self.scroll_manager.visible_line_count
}
- pub(crate) fn set_visible_line_count(&mut self, lines: f32) {
- self.scroll_manager.visible_line_count = Some(lines)
+ pub(crate) fn set_visible_line_count(&mut self, lines: f32, cx: &mut ViewContext<Self>) {
+ let opened_first_time = self.scroll_manager.visible_line_count.is_none();
+ self.scroll_manager.visible_line_count = Some(lines);
+ if opened_first_time {
+ cx.spawn(|editor, mut cx| async move {
+ editor
+ .update(&mut cx, |editor, cx| {
+ editor.refresh_inlays(InlayRefreshReason::NewLinesShown, cx)
+ })
+ .ok()
+ })
+ .detach()
+ }
}
pub fn set_scroll_position(&mut self, scroll_position: Vector2F, cx: &mut ViewContext<Self>) {
@@ -320,6 +332,10 @@ impl Editor {
workspace_id,
cx,
);
+
+ if !self.is_singleton(cx) {
+ self.refresh_inlays(InlayRefreshReason::NewLinesShown, cx);
+ }
}
pub fn scroll_position(&self, cx: &mut ViewContext<Self>) -> Vector2F {
@@ -388,6 +388,7 @@ struct FakeFsState {
event_txs: Vec<smol::channel::Sender<Vec<fsevent::Event>>>,
events_paused: bool,
buffered_events: Vec<fsevent::Event>,
+ metadata_call_count: usize,
read_dir_call_count: usize,
}
@@ -538,6 +539,7 @@ impl FakeFs {
buffered_events: Vec::new(),
events_paused: false,
read_dir_call_count: 0,
+ metadata_call_count: 0,
}),
})
}
@@ -774,10 +776,16 @@ impl FakeFs {
result
}
+ /// How many `read_dir` calls have been issued.
pub fn read_dir_call_count(&self) -> usize {
self.state.lock().read_dir_call_count
}
+ /// How many `metadata` calls have been issued.
+ pub fn metadata_call_count(&self) -> usize {
+ self.state.lock().metadata_call_count
+ }
+
async fn simulate_random_delay(&self) {
self.executor
.upgrade()
@@ -1098,7 +1106,8 @@ impl Fs for FakeFs {
async fn metadata(&self, path: &Path) -> Result<Option<Metadata>> {
self.simulate_random_delay().await;
let path = normalize_path(path);
- let state = self.state.lock();
+ let mut state = self.state.lock();
+ state.metadata_call_count += 1;
if let Some((mut entry, _)) = state.try_read_path(&path, false) {
let is_symlink = entry.lock().is_symlink();
if is_symlink {
@@ -8,8 +8,8 @@ use crate::{
MouseButton, MouseMovedEvent, PromptLevel, WindowBounds,
},
scene::{
- CursorRegion, MouseClick, MouseDown, MouseDownOut, MouseDrag, MouseEvent, MouseHover,
- MouseMove, MouseMoveOut, MouseScrollWheel, MouseUp, MouseUpOut, Scene,
+ CursorRegion, MouseClick, MouseClickOut, MouseDown, MouseDownOut, MouseDrag, MouseEvent,
+ MouseHover, MouseMove, MouseMoveOut, MouseScrollWheel, MouseUp, MouseUpOut, Scene,
},
text_layout::TextLayoutCache,
util::post_inc,
@@ -524,6 +524,10 @@ impl<'a> WindowContext<'a> {
region: Default::default(),
platform_event: e.clone(),
}));
+ mouse_events.push(MouseEvent::ClickOut(MouseClickOut {
+ region: Default::default(),
+ platform_event: e.clone(),
+ }));
}
Event::MouseMoved(
@@ -712,7 +716,10 @@ impl<'a> WindowContext<'a> {
}
}
- MouseEvent::MoveOut(_) | MouseEvent::UpOut(_) | MouseEvent::DownOut(_) => {
+ MouseEvent::MoveOut(_)
+ | MouseEvent::UpOut(_)
+ | MouseEvent::DownOut(_)
+ | MouseEvent::ClickOut(_) => {
for (mouse_region, _) in self.window.mouse_regions.iter().rev() {
// NOT contains
if !mouse_region
@@ -7,8 +7,8 @@ use crate::{
platform::CursorStyle,
platform::MouseButton,
scene::{
- CursorRegion, HandlerSet, MouseClick, MouseDown, MouseDownOut, MouseDrag, MouseHover,
- MouseMove, MouseMoveOut, MouseScrollWheel, MouseUp, MouseUpOut,
+ CursorRegion, HandlerSet, MouseClick, MouseClickOut, MouseDown, MouseDownOut, MouseDrag,
+ MouseHover, MouseMove, MouseMoveOut, MouseScrollWheel, MouseUp, MouseUpOut,
},
AnyElement, Element, EventContext, LayoutContext, MouseRegion, MouseState, SceneBuilder,
SizeConstraint, View, ViewContext,
@@ -136,6 +136,15 @@ impl<Tag, V: View> MouseEventHandler<Tag, V> {
self
}
+ pub fn on_click_out(
+ mut self,
+ button: MouseButton,
+ handler: impl Fn(MouseClickOut, &mut V, &mut EventContext<V>) + 'static,
+ ) -> Self {
+ self.handlers = self.handlers.on_click_out(button, handler);
+ self
+ }
+
pub fn on_down_out(
mut self,
button: MouseButton,
@@ -99,6 +99,20 @@ impl Deref for MouseClick {
}
}
+#[derive(Debug, Default, Clone)]
+pub struct MouseClickOut {
+ pub region: RectF,
+ pub platform_event: MouseButtonEvent,
+}
+
+impl Deref for MouseClickOut {
+ type Target = MouseButtonEvent;
+
+ fn deref(&self) -> &Self::Target {
+ &self.platform_event
+ }
+}
+
#[derive(Debug, Default, Clone)]
pub struct MouseDownOut {
pub region: RectF,
@@ -150,6 +164,7 @@ pub enum MouseEvent {
Down(MouseDown),
Up(MouseUp),
Click(MouseClick),
+ ClickOut(MouseClickOut),
DownOut(MouseDownOut),
UpOut(MouseUpOut),
ScrollWheel(MouseScrollWheel),
@@ -165,6 +180,7 @@ impl MouseEvent {
MouseEvent::Down(r) => r.region = region,
MouseEvent::Up(r) => r.region = region,
MouseEvent::Click(r) => r.region = region,
+ MouseEvent::ClickOut(r) => r.region = region,
MouseEvent::DownOut(r) => r.region = region,
MouseEvent::UpOut(r) => r.region = region,
MouseEvent::ScrollWheel(r) => r.region = region,
@@ -182,6 +198,7 @@ impl MouseEvent {
MouseEvent::Down(_) => true,
MouseEvent::Up(_) => true,
MouseEvent::Click(_) => true,
+ MouseEvent::ClickOut(_) => true,
MouseEvent::DownOut(_) => false,
MouseEvent::UpOut(_) => false,
MouseEvent::ScrollWheel(_) => true,
@@ -222,6 +239,10 @@ impl MouseEvent {
discriminant(&MouseEvent::Click(Default::default()))
}
+ pub fn click_out_disc() -> Discriminant<MouseEvent> {
+ discriminant(&MouseEvent::ClickOut(Default::default()))
+ }
+
pub fn down_out_disc() -> Discriminant<MouseEvent> {
discriminant(&MouseEvent::DownOut(Default::default()))
}
@@ -239,6 +260,7 @@ impl MouseEvent {
MouseEvent::Down(e) => HandlerKey::new(Self::down_disc(), Some(e.button)),
MouseEvent::Up(e) => HandlerKey::new(Self::up_disc(), Some(e.button)),
MouseEvent::Click(e) => HandlerKey::new(Self::click_disc(), Some(e.button)),
+ MouseEvent::ClickOut(e) => HandlerKey::new(Self::click_out_disc(), Some(e.button)),
MouseEvent::UpOut(e) => HandlerKey::new(Self::up_out_disc(), Some(e.button)),
MouseEvent::DownOut(e) => HandlerKey::new(Self::down_out_disc(), Some(e.button)),
MouseEvent::ScrollWheel(_) => HandlerKey::new(Self::scroll_wheel_disc(), None),
@@ -14,7 +14,7 @@ use super::{
MouseClick, MouseDown, MouseDownOut, MouseDrag, MouseEvent, MouseHover, MouseMove, MouseUp,
MouseUpOut,
},
- MouseMoveOut, MouseScrollWheel,
+ MouseClickOut, MouseMoveOut, MouseScrollWheel,
};
#[derive(Clone)]
@@ -89,6 +89,15 @@ impl MouseRegion {
self
}
+ pub fn on_click_out<V, F>(mut self, button: MouseButton, handler: F) -> Self
+ where
+ V: View,
+ F: Fn(MouseClickOut, &mut V, &mut EventContext<V>) + 'static,
+ {
+ self.handlers = self.handlers.on_click_out(button, handler);
+ self
+ }
+
pub fn on_down_out<V, F>(mut self, button: MouseButton, handler: F) -> Self
where
V: View,
@@ -246,6 +255,10 @@ impl HandlerSet {
HandlerKey::new(MouseEvent::click_disc(), Some(button)),
SmallVec::from_buf([Rc::new(|_, _, _, _| true)]),
);
+ set.insert(
+ HandlerKey::new(MouseEvent::click_out_disc(), Some(button)),
+ SmallVec::from_buf([Rc::new(|_, _, _, _| true)]),
+ );
set.insert(
HandlerKey::new(MouseEvent::down_out_disc(), Some(button)),
SmallVec::from_buf([Rc::new(|_, _, _, _| true)]),
@@ -405,6 +418,28 @@ impl HandlerSet {
self
}
+ pub fn on_click_out<V, F>(mut self, button: MouseButton, handler: F) -> Self
+ where
+ V: View,
+ F: Fn(MouseClickOut, &mut V, &mut EventContext<V>) + 'static,
+ {
+ self.insert(MouseEvent::click_out_disc(), Some(button),
+ Rc::new(move |region_event, view, cx, view_id| {
+ if let MouseEvent::ClickOut(e) = region_event {
+ let view = view.downcast_mut().unwrap();
+ let mut cx = ViewContext::mutable(cx, view_id);
+ let mut cx = EventContext::new(&mut cx);
+ handler(e, view, &mut cx);
+ cx.handled
+ } else {
+ panic!(
+ "Mouse Region Event incorrectly called with mismatched event type. Expected MouseRegionEvent::ClickOut, found {:?}",
+ region_event);
+ }
+ }));
+ self
+ }
+
pub fn on_down_out<V, F>(mut self, button: MouseButton, handler: F) -> Self
where
V: View,
@@ -20,7 +20,7 @@ use futures::{
use gpui::{executor::Background, AppContext, AsyncAppContext, Task};
use highlight_map::HighlightMap;
use lazy_static::lazy_static;
-use lsp::CodeActionKind;
+use lsp::{CodeActionKind, LanguageServerBinary};
use parking_lot::{Mutex, RwLock};
use postage::watch;
use regex::Regex;
@@ -30,7 +30,6 @@ use std::{
any::Any,
borrow::Cow,
cell::RefCell,
- ffi::OsString,
fmt::Debug,
hash::Hash,
mem,
@@ -86,12 +85,6 @@ pub trait ToLspPosition {
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub struct LanguageServerName(pub Arc<str>);
-#[derive(Debug, Clone, Deserialize)]
-pub struct LanguageServerBinary {
- pub path: PathBuf,
- pub arguments: Vec<OsString>,
-}
-
/// Represents a Language Server, with certain cached sync properties.
/// Uses [`LspAdapter`] under the hood, but calls all 'static' methods
/// once at startup, and caches the results.
@@ -167,6 +160,17 @@ impl CachedLspAdapter {
.await
}
+ pub fn can_be_reinstalled(&self) -> bool {
+ self.adapter.can_be_reinstalled()
+ }
+
+ pub async fn installation_test_binary(
+ &self,
+ container_dir: PathBuf,
+ ) -> Option<LanguageServerBinary> {
+ self.adapter.installation_test_binary(container_dir).await
+ }
+
pub fn code_action_kinds(&self) -> Option<Vec<CodeActionKind>> {
self.adapter.code_action_kinds()
}
@@ -249,6 +253,15 @@ pub trait LspAdapter: 'static + Send + Sync {
delegate: &dyn LspAdapterDelegate,
) -> Option<LanguageServerBinary>;
+ fn can_be_reinstalled(&self) -> bool {
+ true
+ }
+
+ async fn installation_test_binary(
+ &self,
+ container_dir: PathBuf,
+ ) -> Option<LanguageServerBinary>;
+
async fn process_diagnostics(&self, _: &mut lsp::PublishDiagnosticsParams) {}
async fn process_completion(&self, _: &mut lsp::CompletionItem) {}
@@ -576,7 +589,8 @@ struct LanguageRegistryState {
pub struct PendingLanguageServer {
pub server_id: LanguageServerId,
- pub task: Task<Result<lsp::LanguageServer>>,
+ pub task: Task<Result<Option<lsp::LanguageServer>>>,
+ pub container_dir: Option<Arc<Path>>,
}
impl LanguageRegistry {
@@ -848,7 +862,7 @@ impl LanguageRegistry {
self.state.read().languages.iter().cloned().collect()
}
- pub fn start_language_server(
+ pub fn create_pending_language_server(
self: &Arc<Self>,
language: Arc<Language>,
adapter: Arc<CachedLspAdapter>,
@@ -858,7 +872,7 @@ impl LanguageRegistry {
) -> Option<PendingLanguageServer> {
let server_id = self.state.write().next_language_server_id();
log::info!(
- "starting language server name:{}, path:{root_path:?}, id:{server_id}",
+ "starting language server {:?}, path: {root_path:?}, id: {server_id}",
adapter.name.0
);
@@ -888,66 +902,81 @@ impl LanguageRegistry {
}
})
.detach();
- Ok(server)
+
+ Ok(Some(server))
});
- return Some(PendingLanguageServer { server_id, task });
+ return Some(PendingLanguageServer {
+ server_id,
+ task,
+ container_dir: None,
+ });
}
let download_dir = self
.language_server_download_dir
.clone()
- .ok_or_else(|| anyhow!("language server download directory has not been assigned"))
+ .ok_or_else(|| anyhow!("language server download directory has not been assigned before starting server"))
.log_err()?;
let this = self.clone();
let language = language.clone();
- let download_dir = download_dir.clone();
+ let container_dir: Arc<Path> = Arc::from(download_dir.join(adapter.name.0.as_ref()));
let root_path = root_path.clone();
let adapter = adapter.clone();
let lsp_binary_statuses = self.lsp_binary_statuses_tx.clone();
let login_shell_env_loaded = self.login_shell_env_loaded.clone();
- let task = cx.spawn(|mut cx| async move {
- login_shell_env_loaded.await;
-
- let entry = this
- .lsp_binary_paths
- .lock()
- .entry(adapter.name.clone())
- .or_insert_with(|| {
- cx.spawn(|cx| {
- get_binary(
- adapter.clone(),
- language.clone(),
- delegate.clone(),
- download_dir,
- lsp_binary_statuses,
- cx,
- )
- .map_err(Arc::new)
+ let task = {
+ let container_dir = container_dir.clone();
+ cx.spawn(|mut cx| async move {
+ login_shell_env_loaded.await;
+
+ let mut lock = this.lsp_binary_paths.lock();
+ let entry = lock
+ .entry(adapter.name.clone())
+ .or_insert_with(|| {
+ cx.spawn(|cx| {
+ get_binary(
+ adapter.clone(),
+ language.clone(),
+ delegate.clone(),
+ container_dir,
+ lsp_binary_statuses,
+ cx,
+ )
+ .map_err(Arc::new)
+ })
+ .shared()
})
- .shared()
- })
- .clone();
- let binary = entry.clone().map_err(|e| anyhow!(e)).await?;
+ .clone();
+ drop(lock);
- if let Some(task) = adapter.will_start_server(&delegate, &mut cx) {
- task.await?;
- }
+ let binary = match entry.clone().await.log_err() {
+ Some(binary) => binary,
+ None => return Ok(None),
+ };
- let server = lsp::LanguageServer::new(
- server_id,
- &binary.path,
- &binary.arguments,
- &root_path,
- adapter.code_action_kinds(),
- cx,
- )?;
-
- Ok(server)
- });
+ if let Some(task) = adapter.will_start_server(&delegate, &mut cx) {
+ if task.await.log_err().is_none() {
+ return Ok(None);
+ }
+ }
+
+ Ok(Some(lsp::LanguageServer::new(
+ server_id,
+ binary,
+ &root_path,
+ adapter.code_action_kinds(),
+ cx,
+ )?))
+ })
+ };
- Some(PendingLanguageServer { server_id, task })
+ Some(PendingLanguageServer {
+ server_id,
+ task,
+ container_dir: Some(container_dir),
+ })
}
pub fn language_server_binary_statuses(
@@ -955,6 +984,30 @@ impl LanguageRegistry {
) -> async_broadcast::Receiver<(Arc<Language>, LanguageServerBinaryStatus)> {
self.lsp_binary_statuses_rx.clone()
}
+
+ pub fn delete_server_container(
+ &self,
+ adapter: Arc<CachedLspAdapter>,
+ cx: &mut AppContext,
+ ) -> Task<()> {
+ log::info!("deleting server container");
+
+ let mut lock = self.lsp_binary_paths.lock();
+ lock.remove(&adapter.name);
+
+ let download_dir = self
+ .language_server_download_dir
+ .clone()
+ .expect("language server download directory has not been assigned before deleting server container");
+
+ cx.spawn(|_| async move {
+ let container_dir = download_dir.join(adapter.name.0.as_ref());
+ smol::fs::remove_dir_all(container_dir)
+ .await
+ .context("server container removal")
+ .log_err();
+ })
+ }
}
impl LanguageRegistryState {
@@ -1005,11 +1058,10 @@ async fn get_binary(
adapter: Arc<CachedLspAdapter>,
language: Arc<Language>,
delegate: Arc<dyn LspAdapterDelegate>,
- download_dir: Arc<Path>,
+ container_dir: Arc<Path>,
statuses: async_broadcast::Sender<(Arc<Language>, LanguageServerBinaryStatus)>,
mut cx: AsyncAppContext,
) -> Result<LanguageServerBinary> {
- let container_dir = download_dir.join(adapter.name.0.as_ref());
if !container_dir.exists() {
smol::fs::create_dir_all(&container_dir)
.await
@@ -1030,14 +1082,14 @@ async fn get_binary(
.await;
if let Err(error) = binary.as_ref() {
- if let Some(cached) = adapter
- .cached_server_binary(container_dir, delegate.as_ref())
+ if let Some(binary) = adapter
+ .cached_server_binary(container_dir.to_path_buf(), delegate.as_ref())
.await
{
statuses
.broadcast((language.clone(), LanguageServerBinaryStatus::Cached))
.await?;
- return Ok(cached);
+ return Ok(binary);
} else {
statuses
.broadcast((
@@ -1049,6 +1101,7 @@ async fn get_binary(
.await?;
}
}
+
binary
}
@@ -1066,16 +1119,19 @@ async fn fetch_latest_binary(
LanguageServerBinaryStatus::CheckingForUpdate,
))
.await?;
+
let version_info = adapter.fetch_latest_server_version(delegate).await?;
lsp_binary_statuses_tx
.broadcast((language.clone(), LanguageServerBinaryStatus::Downloading))
.await?;
+
let binary = adapter
.fetch_server_binary(version_info, container_dir.to_path_buf(), delegate)
.await?;
lsp_binary_statuses_tx
.broadcast((language.clone(), LanguageServerBinaryStatus::Downloaded))
.await?;
+
Ok(binary)
}
@@ -1617,6 +1673,10 @@ impl LspAdapter for Arc<FakeLspAdapter> {
unreachable!();
}
+ async fn installation_test_binary(&self, _: PathBuf) -> Option<LanguageServerBinary> {
+ unreachable!();
+ }
+
async fn process_diagnostics(&self, _: &mut lsp::PublishDiagnosticsParams) {}
async fn disk_based_diagnostic_sources(&self) -> Vec<String> {
@@ -1,6 +1,6 @@
use crate::{File, Language};
use anyhow::Result;
-use collections::HashMap;
+use collections::{HashMap, HashSet};
use globset::GlobMatcher;
use gpui::AppContext;
use schemars::{
@@ -52,6 +52,7 @@ pub struct LanguageSettings {
pub show_copilot_suggestions: bool,
pub show_whitespaces: ShowWhitespaceSetting,
pub extend_comment_on_newline: bool,
+ pub inlay_hints: InlayHintSettings,
}
#[derive(Clone, Debug, Default)]
@@ -98,6 +99,8 @@ pub struct LanguageSettingsContent {
pub show_whitespaces: Option<ShowWhitespaceSetting>,
#[serde(default)]
pub extend_comment_on_newline: Option<bool>,
+ #[serde(default)]
+ pub inlay_hints: Option<InlayHintSettings>,
}
#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema)]
@@ -150,6 +153,38 @@ pub enum Formatter {
},
}
+#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
+pub struct InlayHintSettings {
+ #[serde(default)]
+ pub enabled: bool,
+ #[serde(default = "default_true")]
+ pub show_type_hints: bool,
+ #[serde(default = "default_true")]
+ pub show_parameter_hints: bool,
+ #[serde(default = "default_true")]
+ pub show_other_hints: bool,
+}
+
+fn default_true() -> bool {
+ true
+}
+
+impl InlayHintSettings {
+ pub fn enabled_inlay_hint_kinds(&self) -> HashSet<Option<InlayHintKind>> {
+ let mut kinds = HashSet::default();
+ if self.show_type_hints {
+ kinds.insert(Some(InlayHintKind::Type));
+ }
+ if self.show_parameter_hints {
+ kinds.insert(Some(InlayHintKind::Parameter));
+ }
+ if self.show_other_hints {
+ kinds.insert(None);
+ }
+ kinds
+ }
+}
+
impl AllLanguageSettings {
pub fn language<'a>(&'a self, language_name: Option<&str>) -> &'a LanguageSettings {
if let Some(name) = language_name {
@@ -184,6 +219,29 @@ impl AllLanguageSettings {
}
}
+#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
+pub enum InlayHintKind {
+ Type,
+ Parameter,
+}
+
+impl InlayHintKind {
+ pub fn from_name(name: &str) -> Option<Self> {
+ match name {
+ "type" => Some(InlayHintKind::Type),
+ "parameter" => Some(InlayHintKind::Parameter),
+ _ => None,
+ }
+ }
+
+ pub fn name(&self) -> &'static str {
+ match self {
+ InlayHintKind::Type => "type",
+ InlayHintKind::Parameter => "parameter",
+ }
+ }
+}
+
impl settings::Setting for AllLanguageSettings {
const KEY: Option<&'static str> = None;
@@ -347,6 +405,7 @@ fn merge_settings(settings: &mut LanguageSettings, src: &LanguageSettingsContent
&mut settings.extend_comment_on_newline,
src.extend_comment_on_newline,
);
+ merge(&mut settings.inlay_hints, src.inlay_hints);
fn merge<T>(target: &mut T, value: Option<T>) {
if let Some(value) = value {
*target = value;
@@ -16,6 +16,7 @@ use smol::{
process::{self, Child},
};
use std::{
+ ffi::OsString,
fmt,
future::Future,
io::Write,
@@ -36,6 +37,12 @@ type NotificationHandler = Box<dyn Send + FnMut(Option<usize>, &str, AsyncAppCon
type ResponseHandler = Box<dyn Send + FnOnce(Result<String, Error>)>;
type IoHandler = Box<dyn Send + FnMut(bool, &str)>;
+#[derive(Debug, Clone, Deserialize)]
+pub struct LanguageServerBinary {
+ pub path: PathBuf,
+ pub arguments: Vec<OsString>,
+}
+
pub struct LanguageServer {
server_id: LanguageServerId,
next_id: AtomicUsize,
@@ -51,7 +58,7 @@ pub struct LanguageServer {
io_tasks: Mutex<Option<(Task<Option<()>>, Task<Option<()>>)>>,
output_done_rx: Mutex<Option<barrier::Receiver>>,
root_path: PathBuf,
- _server: Option<Child>,
+ _server: Option<Mutex<Child>>,
}
#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
@@ -119,10 +126,9 @@ struct Error {
}
impl LanguageServer {
- pub fn new<T: AsRef<std::ffi::OsStr>>(
+ pub fn new(
server_id: LanguageServerId,
- binary_path: &Path,
- arguments: &[T],
+ binary: LanguageServerBinary,
root_path: &Path,
code_action_kinds: Option<Vec<CodeActionKind>>,
cx: AsyncAppContext,
@@ -133,9 +139,9 @@ impl LanguageServer {
root_path.parent().unwrap_or_else(|| Path::new("/"))
};
- let mut server = process::Command::new(binary_path)
+ let mut server = process::Command::new(&binary.path)
.current_dir(working_dir)
- .args(arguments)
+ .args(binary.arguments)
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::inherit())
@@ -167,9 +173,10 @@ impl LanguageServer {
},
);
- if let Some(name) = binary_path.file_name() {
+ if let Some(name) = binary.path.file_name() {
server.name = name.to_string_lossy().to_string();
}
+
Ok(server)
}
@@ -231,7 +238,7 @@ impl LanguageServer {
io_tasks: Mutex::new(Some((input_task, output_task))),
output_done_rx: Mutex::new(Some(output_done_rx)),
root_path: root_path.to_path_buf(),
- _server: server,
+ _server: server.map(|server| Mutex::new(server)),
}
}
@@ -381,6 +388,9 @@ impl LanguageServer {
resolve_support: None,
..WorkspaceSymbolClientCapabilities::default()
}),
+ inlay_hint: Some(InlayHintWorkspaceClientCapabilities {
+ refresh_support: Some(true),
+ }),
..Default::default()
}),
text_document: Some(TextDocumentClientCapabilities {
@@ -422,6 +432,10 @@ impl LanguageServer {
content_format: Some(vec![MarkupKind::Markdown]),
..Default::default()
}),
+ inlay_hint: Some(InlayHintClientCapabilities {
+ resolve_support: None,
+ dynamic_registration: Some(false),
+ }),
..Default::default()
}),
experimental: Some(json!({
@@ -600,6 +614,7 @@ impl LanguageServer {
})
.detach();
}
+
Err(error) => {
log::error!(
"error deserializing {} request: {:?}, message: {:?}",
@@ -701,7 +716,7 @@ impl LanguageServer {
.context("failed to deserialize response"),
Err(error) => Err(anyhow!("{}", error.message)),
};
- let _ = tx.send(response);
+ _ = tx.send(response);
})
.detach();
}),
@@ -20,3 +20,4 @@ serde.workspace = true
serde_derive.workspace = true
serde_json.workspace = true
smol.workspace = true
+log.workspace = true
@@ -1,21 +1,24 @@
use anyhow::{anyhow, bail, Context, Result};
use async_compression::futures::bufread::GzipDecoder;
use async_tar::Archive;
+use futures::lock::Mutex;
use futures::{future::Shared, FutureExt};
use gpui::{executor::Background, Task};
-use parking_lot::Mutex;
use serde::Deserialize;
use smol::{fs, io::BufReader, process::Command};
+use std::process::Output;
use std::{
env::consts,
path::{Path, PathBuf},
- sync::Arc,
+ sync::{Arc, OnceLock},
};
-use util::http::HttpClient;
+use util::{http::HttpClient, ResultExt};
const VERSION: &str = "v18.15.0";
-#[derive(Deserialize)]
+static RUNTIME_INSTANCE: OnceLock<Arc<NodeRuntime>> = OnceLock::new();
+
+#[derive(Debug, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub struct NpmInfo {
#[serde(default)]
@@ -23,7 +26,7 @@ pub struct NpmInfo {
versions: Vec<String>,
}
-#[derive(Deserialize, Default)]
+#[derive(Debug, Deserialize, Default)]
pub struct NpmInfoDistTags {
latest: Option<String>,
}
@@ -35,12 +38,16 @@ pub struct NodeRuntime {
}
impl NodeRuntime {
- pub fn new(http: Arc<dyn HttpClient>, background: Arc<Background>) -> Arc<NodeRuntime> {
- Arc::new(NodeRuntime {
- http,
- background,
- installation_path: Mutex::new(None),
- })
+ pub fn instance(http: Arc<dyn HttpClient>, background: Arc<Background>) -> Arc<NodeRuntime> {
+ RUNTIME_INSTANCE
+ .get_or_init(|| {
+ Arc::new(NodeRuntime {
+ http,
+ background,
+ installation_path: Mutex::new(None),
+ })
+ })
+ .clone()
}
pub async fn binary_path(&self) -> Result<PathBuf> {
@@ -50,55 +57,74 @@ impl NodeRuntime {
pub async fn run_npm_subcommand(
&self,
- directory: &Path,
+ directory: Option<&Path>,
subcommand: &str,
args: &[&str],
- ) -> Result<()> {
+ ) -> Result<Output> {
+ let attempt = |installation_path: PathBuf| async move {
+ let node_binary = installation_path.join("bin/node");
+ let npm_file = installation_path.join("bin/npm");
+
+ if smol::fs::metadata(&node_binary).await.is_err() {
+ return Err(anyhow!("missing node binary file"));
+ }
+
+ if smol::fs::metadata(&npm_file).await.is_err() {
+ return Err(anyhow!("missing npm file"));
+ }
+
+ let mut command = Command::new(node_binary);
+ command.arg(npm_file).arg(subcommand).args(args);
+
+ if let Some(directory) = directory {
+ command.current_dir(directory);
+ }
+
+ command.output().await.map_err(|e| anyhow!("{e}"))
+ };
+
let installation_path = self.install_if_needed().await?;
- let node_binary = installation_path.join("bin/node");
- let npm_file = installation_path.join("bin/npm");
-
- let output = Command::new(node_binary)
- .arg(npm_file)
- .arg(subcommand)
- .args(args)
- .current_dir(directory)
- .output()
- .await?;
+ let mut output = attempt(installation_path).await;
+ if output.is_err() {
+ let installation_path = self.reinstall().await?;
+ output = attempt(installation_path).await;
+ if output.is_err() {
+ return Err(anyhow!(
+ "failed to launch npm subcommand {subcommand} subcommand"
+ ));
+ }
+ }
- if !output.status.success() {
- return Err(anyhow!(
- "failed to execute npm {subcommand} subcommand:\nstdout: {:?}\nstderr: {:?}",
- String::from_utf8_lossy(&output.stdout),
- String::from_utf8_lossy(&output.stderr)
- ));
+ if let Ok(output) = &output {
+ if !output.status.success() {
+ return Err(anyhow!(
+ "failed to execute npm {subcommand} subcommand:\nstdout: {:?}\nstderr: {:?}",
+ String::from_utf8_lossy(&output.stdout),
+ String::from_utf8_lossy(&output.stderr)
+ ));
+ }
}
- Ok(())
+ output.map_err(|e| anyhow!("{e}"))
}
pub async fn npm_package_latest_version(&self, name: &str) -> Result<String> {
- let installation_path = self.install_if_needed().await?;
- let node_binary = installation_path.join("bin/node");
- let npm_file = installation_path.join("bin/npm");
-
- let output = Command::new(node_binary)
- .arg(npm_file)
- .args(["-fetch-retry-mintimeout", "2000"])
- .args(["-fetch-retry-maxtimeout", "5000"])
- .args(["-fetch-timeout", "5000"])
- .args(["info", name, "--json"])
- .output()
- .await
- .context("failed to run npm info")?;
-
- if !output.status.success() {
- return Err(anyhow!(
- "failed to execute npm info:\nstdout: {:?}\nstderr: {:?}",
- String::from_utf8_lossy(&output.stdout),
- String::from_utf8_lossy(&output.stderr)
- ));
- }
+ let output = self
+ .run_npm_subcommand(
+ None,
+ "info",
+ &[
+ name,
+ "--json",
+ "-fetch-retry-mintimeout",
+ "2000",
+ "-fetch-retry-maxtimeout",
+ "5000",
+ "-fetch-timeout",
+ "5000",
+ ],
+ )
+ .await?;
let mut info: NpmInfo = serde_json::from_slice(&output.stdout)?;
info.dist_tags
@@ -112,41 +138,54 @@ impl NodeRuntime {
directory: &Path,
packages: impl IntoIterator<Item = (&str, &str)>,
) -> Result<()> {
- let installation_path = self.install_if_needed().await?;
- let node_binary = installation_path.join("bin/node");
- let npm_file = installation_path.join("bin/npm");
-
- let output = Command::new(node_binary)
- .arg(npm_file)
- .args(["-fetch-retry-mintimeout", "2000"])
- .args(["-fetch-retry-maxtimeout", "5000"])
- .args(["-fetch-timeout", "5000"])
- .arg("install")
- .arg("--prefix")
- .arg(directory)
- .args(
- packages
- .into_iter()
- .map(|(name, version)| format!("{name}@{version}")),
- )
- .output()
- .await
- .context("failed to run npm install")?;
-
- if !output.status.success() {
- return Err(anyhow!(
- "failed to execute npm install:\nstdout: {:?}\nstderr: {:?}",
- String::from_utf8_lossy(&output.stdout),
- String::from_utf8_lossy(&output.stderr)
- ));
- }
+ let packages: Vec<_> = packages
+ .into_iter()
+ .map(|(name, version)| format!("{name}@{version}"))
+ .collect();
+
+ let mut arguments: Vec<_> = packages.iter().map(|p| p.as_str()).collect();
+ arguments.extend_from_slice(&[
+ "-fetch-retry-mintimeout",
+ "2000",
+ "-fetch-retry-maxtimeout",
+ "5000",
+ "-fetch-timeout",
+ "5000",
+ ]);
+
+ self.run_npm_subcommand(Some(directory), "install", &arguments)
+ .await?;
Ok(())
}
+ async fn reinstall(&self) -> Result<PathBuf> {
+ log::info!("beginnning to reinstall Node runtime");
+ let mut installation_path = self.installation_path.lock().await;
+
+ if let Some(task) = installation_path.as_ref().cloned() {
+ if let Ok(installation_path) = task.await {
+ smol::fs::remove_dir_all(&installation_path)
+ .await
+ .context("node dir removal")
+ .log_err();
+ }
+ }
+
+ let http = self.http.clone();
+ let task = self
+ .background
+ .spawn(async move { Self::install(http).await.map_err(Arc::new) })
+ .shared();
+
+ *installation_path = Some(task.clone());
+ task.await.map_err(|e| anyhow!("{}", e))
+ }
+
async fn install_if_needed(&self) -> Result<PathBuf> {
let task = self
.installation_path
.lock()
+ .await
.get_or_insert_with(|| {
let http = self.http.clone();
self.background
@@ -155,13 +194,11 @@ impl NodeRuntime {
})
.clone();
- match task.await {
- Ok(path) => Ok(path),
- Err(error) => Err(anyhow!("{}", error)),
- }
+ task.await.map_err(|e| anyhow!("{}", e))
}
async fn install(http: Arc<dyn HttpClient>) -> Result<PathBuf> {
+ log::info!("installing Node runtime");
let arch = match consts::ARCH {
"x86_64" => "x64",
"aarch64" => "arm64",
@@ -1,14 +1,15 @@
use crate::{
- DocumentHighlight, Hover, HoverBlock, HoverBlockKind, Location, LocationLink, Project,
- ProjectTransaction,
+ DocumentHighlight, Hover, HoverBlock, HoverBlockKind, InlayHint, InlayHintLabel,
+ InlayHintLabelPart, InlayHintLabelPartTooltip, InlayHintTooltip, Location, LocationLink,
+ MarkupContent, Project, ProjectTransaction,
};
-use anyhow::{anyhow, Result};
+use anyhow::{anyhow, Context, Result};
use async_trait::async_trait;
use client::proto::{self, PeerId};
use fs::LineEnding;
use gpui::{AppContext, AsyncAppContext, ModelHandle};
use language::{
- language_settings::language_settings,
+ language_settings::{language_settings, InlayHintKind},
point_from_lsp, point_to_lsp,
proto::{deserialize_anchor, deserialize_version, serialize_anchor, serialize_version},
range_from_lsp, range_to_lsp, Anchor, Bias, Buffer, CachedLspAdapter, CharKind, CodeAction,
@@ -126,6 +127,10 @@ pub(crate) struct OnTypeFormatting {
pub push_to_history: bool,
}
+pub(crate) struct InlayHints {
+ pub range: Range<Anchor>,
+}
+
pub(crate) struct FormattingOptions {
tab_size: u32,
}
@@ -1780,3 +1785,327 @@ impl LspCommand for OnTypeFormatting {
message.buffer_id
}
}
+
+#[async_trait(?Send)]
+impl LspCommand for InlayHints {
+ type Response = Vec<InlayHint>;
+ type LspRequest = lsp::InlayHintRequest;
+ type ProtoRequest = proto::InlayHints;
+
+ fn check_capabilities(&self, server_capabilities: &lsp::ServerCapabilities) -> bool {
+ let Some(inlay_hint_provider) = &server_capabilities.inlay_hint_provider else { return false };
+ match inlay_hint_provider {
+ lsp::OneOf::Left(enabled) => *enabled,
+ lsp::OneOf::Right(inlay_hint_capabilities) => match inlay_hint_capabilities {
+ lsp::InlayHintServerCapabilities::Options(_) => true,
+ lsp::InlayHintServerCapabilities::RegistrationOptions(_) => false,
+ },
+ }
+ }
+
+ fn to_lsp(
+ &self,
+ path: &Path,
+ buffer: &Buffer,
+ _: &Arc<LanguageServer>,
+ _: &AppContext,
+ ) -> lsp::InlayHintParams {
+ lsp::InlayHintParams {
+ text_document: lsp::TextDocumentIdentifier {
+ uri: lsp::Url::from_file_path(path).unwrap(),
+ },
+ range: range_to_lsp(self.range.to_point_utf16(buffer)),
+ work_done_progress_params: Default::default(),
+ }
+ }
+
+ async fn response_from_lsp(
+ self,
+ message: Option<Vec<lsp::InlayHint>>,
+ _: ModelHandle<Project>,
+ buffer: ModelHandle<Buffer>,
+ _: LanguageServerId,
+ cx: AsyncAppContext,
+ ) -> Result<Vec<InlayHint>> {
+ cx.read(|cx| {
+ let origin_buffer = buffer.read(cx);
+ Ok(message
+ .unwrap_or_default()
+ .into_iter()
+ .map(|lsp_hint| {
+ let kind = lsp_hint.kind.and_then(|kind| match kind {
+ lsp::InlayHintKind::TYPE => Some(InlayHintKind::Type),
+ lsp::InlayHintKind::PARAMETER => Some(InlayHintKind::Parameter),
+ _ => None,
+ });
+ let position = origin_buffer
+ .clip_point_utf16(point_from_lsp(lsp_hint.position), Bias::Left);
+ InlayHint {
+ buffer_id: origin_buffer.remote_id(),
+ position: if kind == Some(InlayHintKind::Parameter) {
+ origin_buffer.anchor_before(position)
+ } else {
+ origin_buffer.anchor_after(position)
+ },
+ padding_left: lsp_hint.padding_left.unwrap_or(false),
+ padding_right: lsp_hint.padding_right.unwrap_or(false),
+ label: match lsp_hint.label {
+ lsp::InlayHintLabel::String(s) => InlayHintLabel::String(s),
+ lsp::InlayHintLabel::LabelParts(lsp_parts) => {
+ InlayHintLabel::LabelParts(
+ lsp_parts
+ .into_iter()
+ .map(|label_part| InlayHintLabelPart {
+ value: label_part.value,
+ tooltip: label_part.tooltip.map(
+ |tooltip| {
+ match tooltip {
+ lsp::InlayHintLabelPartTooltip::String(s) => {
+ InlayHintLabelPartTooltip::String(s)
+ }
+ lsp::InlayHintLabelPartTooltip::MarkupContent(
+ markup_content,
+ ) => InlayHintLabelPartTooltip::MarkupContent(
+ MarkupContent {
+ kind: format!("{:?}", markup_content.kind),
+ value: markup_content.value,
+ },
+ ),
+ }
+ },
+ ),
+ location: label_part.location.map(|lsp_location| {
+ let target_start = origin_buffer.clip_point_utf16(
+ point_from_lsp(lsp_location.range.start),
+ Bias::Left,
+ );
+ let target_end = origin_buffer.clip_point_utf16(
+ point_from_lsp(lsp_location.range.end),
+ Bias::Left,
+ );
+ Location {
+ buffer: buffer.clone(),
+ range: origin_buffer.anchor_after(target_start)
+ ..origin_buffer.anchor_before(target_end),
+ }
+ }),
+ })
+ .collect(),
+ )
+ }
+ },
+ kind,
+ tooltip: lsp_hint.tooltip.map(|tooltip| match tooltip {
+ lsp::InlayHintTooltip::String(s) => InlayHintTooltip::String(s),
+ lsp::InlayHintTooltip::MarkupContent(markup_content) => {
+ InlayHintTooltip::MarkupContent(MarkupContent {
+ kind: format!("{:?}", markup_content.kind),
+ value: markup_content.value,
+ })
+ }
+ }),
+ }
+ })
+ .collect())
+ })
+ }
+
+ fn to_proto(&self, project_id: u64, buffer: &Buffer) -> proto::InlayHints {
+ proto::InlayHints {
+ project_id,
+ buffer_id: buffer.remote_id(),
+ start: Some(language::proto::serialize_anchor(&self.range.start)),
+ end: Some(language::proto::serialize_anchor(&self.range.end)),
+ version: serialize_version(&buffer.version()),
+ }
+ }
+
+ async fn from_proto(
+ message: proto::InlayHints,
+ _: ModelHandle<Project>,
+ buffer: ModelHandle<Buffer>,
+ mut cx: AsyncAppContext,
+ ) -> Result<Self> {
+ let start = message
+ .start
+ .and_then(language::proto::deserialize_anchor)
+ .context("invalid start")?;
+ let end = message
+ .end
+ .and_then(language::proto::deserialize_anchor)
+ .context("invalid end")?;
+ buffer
+ .update(&mut cx, |buffer, _| {
+ buffer.wait_for_version(deserialize_version(&message.version))
+ })
+ .await?;
+
+ Ok(Self { range: start..end })
+ }
+
+ fn response_to_proto(
+ response: Vec<InlayHint>,
+ _: &mut Project,
+ _: PeerId,
+ buffer_version: &clock::Global,
+ cx: &mut AppContext,
+ ) -> proto::InlayHintsResponse {
+ proto::InlayHintsResponse {
+ hints: response
+ .into_iter()
+ .map(|response_hint| proto::InlayHint {
+ position: Some(language::proto::serialize_anchor(&response_hint.position)),
+ padding_left: response_hint.padding_left,
+ padding_right: response_hint.padding_right,
+ label: Some(proto::InlayHintLabel {
+ label: Some(match response_hint.label {
+ InlayHintLabel::String(s) => proto::inlay_hint_label::Label::Value(s),
+ InlayHintLabel::LabelParts(label_parts) => {
+ proto::inlay_hint_label::Label::LabelParts(proto::InlayHintLabelParts {
+ parts: label_parts.into_iter().map(|label_part| proto::InlayHintLabelPart {
+ value: label_part.value,
+ tooltip: label_part.tooltip.map(|tooltip| {
+ let proto_tooltip = match tooltip {
+ InlayHintLabelPartTooltip::String(s) => proto::inlay_hint_label_part_tooltip::Content::Value(s),
+ InlayHintLabelPartTooltip::MarkupContent(markup_content) => proto::inlay_hint_label_part_tooltip::Content::MarkupContent(proto::MarkupContent {
+ kind: markup_content.kind,
+ value: markup_content.value,
+ }),
+ };
+ proto::InlayHintLabelPartTooltip {content: Some(proto_tooltip)}
+ }),
+ location: label_part.location.map(|location| proto::Location {
+ start: Some(serialize_anchor(&location.range.start)),
+ end: Some(serialize_anchor(&location.range.end)),
+ buffer_id: location.buffer.read(cx).remote_id(),
+ }),
+ }).collect()
+ })
+ }
+ }),
+ }),
+ kind: response_hint.kind.map(|kind| kind.name().to_string()),
+ tooltip: response_hint.tooltip.map(|response_tooltip| {
+ let proto_tooltip = match response_tooltip {
+ InlayHintTooltip::String(s) => {
+ proto::inlay_hint_tooltip::Content::Value(s)
+ }
+ InlayHintTooltip::MarkupContent(markup_content) => {
+ proto::inlay_hint_tooltip::Content::MarkupContent(
+ proto::MarkupContent {
+ kind: markup_content.kind,
+ value: markup_content.value,
+ },
+ )
+ }
+ };
+ proto::InlayHintTooltip {
+ content: Some(proto_tooltip),
+ }
+ }),
+ })
+ .collect(),
+ version: serialize_version(buffer_version),
+ }
+ }
+
+ async fn response_from_proto(
+ self,
+ message: proto::InlayHintsResponse,
+ project: ModelHandle<Project>,
+ buffer: ModelHandle<Buffer>,
+ mut cx: AsyncAppContext,
+ ) -> Result<Vec<InlayHint>> {
+ buffer
+ .update(&mut cx, |buffer, _| {
+ buffer.wait_for_version(deserialize_version(&message.version))
+ })
+ .await?;
+
+ let mut hints = Vec::new();
+ for message_hint in message.hints {
+ let buffer_id = message_hint
+ .position
+ .as_ref()
+ .and_then(|location| location.buffer_id)
+ .context("missing buffer id")?;
+ let hint = InlayHint {
+ buffer_id,
+ position: message_hint
+ .position
+ .and_then(language::proto::deserialize_anchor)
+ .context("invalid position")?,
+ label: match message_hint
+ .label
+ .and_then(|label| label.label)
+ .context("missing label")?
+ {
+ proto::inlay_hint_label::Label::Value(s) => InlayHintLabel::String(s),
+ proto::inlay_hint_label::Label::LabelParts(parts) => {
+ let mut label_parts = Vec::new();
+ for part in parts.parts {
+ label_parts.push(InlayHintLabelPart {
+ value: part.value,
+ tooltip: part.tooltip.map(|tooltip| match tooltip.content {
+ Some(proto::inlay_hint_label_part_tooltip::Content::Value(s)) => InlayHintLabelPartTooltip::String(s),
+ Some(proto::inlay_hint_label_part_tooltip::Content::MarkupContent(markup_content)) => InlayHintLabelPartTooltip::MarkupContent(MarkupContent {
+ kind: markup_content.kind,
+ value: markup_content.value,
+ }),
+ None => InlayHintLabelPartTooltip::String(String::new()),
+ }),
+ location: match part.location {
+ Some(location) => {
+ let target_buffer = project
+ .update(&mut cx, |this, cx| {
+ this.wait_for_remote_buffer(location.buffer_id, cx)
+ })
+ .await?;
+ Some(Location {
+ range: location
+ .start
+ .and_then(language::proto::deserialize_anchor)
+ .context("invalid start")?
+ ..location
+ .end
+ .and_then(language::proto::deserialize_anchor)
+ .context("invalid end")?,
+ buffer: target_buffer,
+ })},
+ None => None,
+ },
+ });
+ }
+
+ InlayHintLabel::LabelParts(label_parts)
+ }
+ },
+ padding_left: message_hint.padding_left,
+ padding_right: message_hint.padding_right,
+ kind: message_hint
+ .kind
+ .as_deref()
+ .and_then(InlayHintKind::from_name),
+ tooltip: message_hint.tooltip.and_then(|tooltip| {
+ Some(match tooltip.content? {
+ proto::inlay_hint_tooltip::Content::Value(s) => InlayHintTooltip::String(s),
+ proto::inlay_hint_tooltip::Content::MarkupContent(markup_content) => {
+ InlayHintTooltip::MarkupContent(MarkupContent {
+ kind: markup_content.kind,
+ value: markup_content.value,
+ })
+ }
+ })
+ }),
+ };
+
+ hints.push(hint);
+ }
+
+ Ok(hints)
+ }
+
+ fn buffer_id_from_proto(message: &proto::InlayHints) -> u64 {
+ message.buffer_id
+ }
+}
@@ -29,14 +29,15 @@ use gpui::{
AnyModelHandle, AppContext, AsyncAppContext, BorrowAppContext, Entity, ModelContext,
ModelHandle, Task, WeakModelHandle,
};
+use itertools::Itertools;
use language::{
- language_settings::{language_settings, FormatOnSave, Formatter},
+ language_settings::{language_settings, FormatOnSave, Formatter, InlayHintKind},
point_to_lsp,
proto::{
deserialize_anchor, deserialize_fingerprint, deserialize_line_ending, deserialize_version,
serialize_anchor, serialize_version,
},
- range_from_lsp, range_to_lsp, Anchor, Bias, Buffer, CachedLspAdapter, CodeAction, CodeLabel,
+ range_from_lsp, range_to_lsp, Bias, Buffer, CachedLspAdapter, CodeAction, CodeLabel,
Completion, Diagnostic, DiagnosticEntry, DiagnosticSet, Diff, Event as BufferEvent, File as _,
Language, LanguageRegistry, LanguageServerName, LocalFile, LspAdapterDelegate, OffsetRangeExt,
Operation, Patch, PendingLanguageServer, PointUtf16, TextBufferSnapshot, ToOffset,
@@ -45,7 +46,7 @@ use language::{
use log::error;
use lsp::{
DiagnosticSeverity, DiagnosticTag, DidChangeWatchedFilesRegistrationOptions,
- DocumentHighlightKind, LanguageServer, LanguageServerId,
+ DocumentHighlightKind, LanguageServer, LanguageServerBinary, LanguageServerId, OneOf,
};
use lsp_command::*;
use postage::watch;
@@ -65,6 +66,7 @@ use std::{
num::NonZeroU32,
ops::Range,
path::{self, Component, Path, PathBuf},
+ process::Stdio,
rc::Rc,
str,
sync::{
@@ -74,6 +76,7 @@ use std::{
time::{Duration, Instant},
};
use terminals::Terminals;
+use text::Anchor;
use util::{
debug_panic, defer, http::HttpClient, merge_json_value_into,
paths::LOCAL_SETTINGS_RELATIVE_PATH, post_inc, ResultExt, TryFutureExt as _,
@@ -223,6 +226,7 @@ enum OpenBuffer {
Operations(Vec<Operation>),
}
+#[derive(Clone)]
enum WorktreeHandle {
Strong(ModelHandle<Worktree>),
Weak(WeakModelHandle<Worktree>),
@@ -275,10 +279,12 @@ pub enum Event {
new_peer_id: proto::PeerId,
},
CollaboratorLeft(proto::PeerId),
+ RefreshInlays,
}
pub enum LanguageServerState {
Starting(Task<Option<Arc<LanguageServer>>>),
+
Running {
language: Arc<Language>,
adapter: Arc<CachedLspAdapter>,
@@ -316,12 +322,63 @@ pub struct DiagnosticSummary {
pub warning_count: usize,
}
-#[derive(Debug, Clone)]
+#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct Location {
pub buffer: ModelHandle<Buffer>,
pub range: Range<language::Anchor>,
}
+#[derive(Debug, Clone, PartialEq, Eq, Hash)]
+pub struct InlayHint {
+ pub buffer_id: u64,
+ pub position: language::Anchor,
+ pub label: InlayHintLabel,
+ pub kind: Option<InlayHintKind>,
+ pub padding_left: bool,
+ pub padding_right: bool,
+ pub tooltip: Option<InlayHintTooltip>,
+}
+
+impl InlayHint {
+ pub fn text(&self) -> String {
+ match &self.label {
+ InlayHintLabel::String(s) => s.to_owned(),
+ InlayHintLabel::LabelParts(parts) => parts.iter().map(|part| &part.value).join(""),
+ }
+ }
+}
+
+#[derive(Debug, Clone, PartialEq, Eq, Hash)]
+pub enum InlayHintLabel {
+ String(String),
+ LabelParts(Vec<InlayHintLabelPart>),
+}
+
+#[derive(Debug, Clone, PartialEq, Eq, Hash)]
+pub struct InlayHintLabelPart {
+ pub value: String,
+ pub tooltip: Option<InlayHintLabelPartTooltip>,
+ pub location: Option<Location>,
+}
+
+#[derive(Debug, Clone, PartialEq, Eq, Hash)]
+pub enum InlayHintTooltip {
+ String(String),
+ MarkupContent(MarkupContent),
+}
+
+#[derive(Debug, Clone, PartialEq, Eq, Hash)]
+pub enum InlayHintLabelPartTooltip {
+ String(String),
+ MarkupContent(MarkupContent),
+}
+
+#[derive(Debug, Clone, PartialEq, Eq, Hash)]
+pub struct MarkupContent {
+ pub kind: String,
+ pub value: String,
+}
+
#[derive(Debug, Clone)]
pub struct LocationLink {
pub origin: Option<Location>,
@@ -482,6 +539,8 @@ impl Project {
client.add_model_request_handler(Self::handle_apply_additional_edits_for_completion);
client.add_model_request_handler(Self::handle_apply_code_action);
client.add_model_request_handler(Self::handle_on_type_formatting);
+ client.add_model_request_handler(Self::handle_inlay_hints);
+ client.add_model_request_handler(Self::handle_refresh_inlay_hints);
client.add_model_request_handler(Self::handle_reload_buffers);
client.add_model_request_handler(Self::handle_synchronize_buffers);
client.add_model_request_handler(Self::handle_format_buffers);
@@ -2424,348 +2483,524 @@ impl Project {
language: Arc<Language>,
cx: &mut ModelContext<Self>,
) {
- if !language_settings(
- Some(&language),
- worktree
- .update(cx, |tree, cx| tree.root_file(cx))
- .map(|f| f as _)
- .as_ref(),
- cx,
- )
- .enable_language_server
- {
+ let root_file = worktree.update(cx, |tree, cx| tree.root_file(cx));
+ let settings = language_settings(Some(&language), root_file.map(|f| f as _).as_ref(), cx);
+ if !settings.enable_language_server {
return;
}
let worktree_id = worktree.read(cx).id();
for adapter in language.lsp_adapters() {
- let key = (worktree_id, adapter.name.clone());
- if self.language_server_ids.contains_key(&key) {
- continue;
- }
-
- let pending_server = match self.languages.start_language_server(
- language.clone(),
- adapter.clone(),
+ self.start_language_server(
+ worktree_id,
worktree_path.clone(),
- ProjectLspAdapterDelegate::new(self, cx),
- cx,
- ) {
- Some(pending_server) => pending_server,
- None => continue,
- };
-
- let lsp = settings::get::<ProjectSettings>(cx)
- .lsp
- .get(&adapter.name.0);
- let override_options = lsp.map(|s| s.initialization_options.clone()).flatten();
-
- let mut initialization_options = adapter.initialization_options.clone();
- match (&mut initialization_options, override_options) {
- (Some(initialization_options), Some(override_options)) => {
- merge_json_value_into(override_options, initialization_options);
- }
- (None, override_options) => initialization_options = override_options,
- _ => {}
- }
-
- let server_id = pending_server.server_id;
- let state = self.setup_pending_language_server(
- initialization_options,
- pending_server,
adapter.clone(),
language.clone(),
- key.clone(),
cx,
);
- self.language_servers.insert(server_id, state);
- self.language_server_ids.insert(key.clone(), server_id);
}
}
- fn setup_pending_language_server(
+ fn start_language_server(
&mut self,
- initialization_options: Option<serde_json::Value>,
- pending_server: PendingLanguageServer,
+ worktree_id: WorktreeId,
+ worktree_path: Arc<Path>,
adapter: Arc<CachedLspAdapter>,
language: Arc<Language>,
- key: (WorktreeId, LanguageServerName),
- cx: &mut ModelContext<Project>,
- ) -> LanguageServerState {
+ cx: &mut ModelContext<Self>,
+ ) {
+ let key = (worktree_id, adapter.name.clone());
+ if self.language_server_ids.contains_key(&key) {
+ return;
+ }
+
+ let pending_server = match self.languages.create_pending_language_server(
+ language.clone(),
+ adapter.clone(),
+ worktree_path,
+ ProjectLspAdapterDelegate::new(self, cx),
+ cx,
+ ) {
+ Some(pending_server) => pending_server,
+ None => return,
+ };
+
+ let project_settings = settings::get::<ProjectSettings>(cx);
+ let lsp = project_settings.lsp.get(&adapter.name.0);
+ let override_options = lsp.map(|s| s.initialization_options.clone()).flatten();
+
+ let mut initialization_options = adapter.initialization_options.clone();
+ match (&mut initialization_options, override_options) {
+ (Some(initialization_options), Some(override_options)) => {
+ merge_json_value_into(override_options, initialization_options);
+ }
+ (None, override_options) => initialization_options = override_options,
+ _ => {}
+ }
+
let server_id = pending_server.server_id;
- let languages = self.languages.clone();
+ let container_dir = pending_server.container_dir.clone();
+ let state = LanguageServerState::Starting({
+ let adapter = adapter.clone();
+ let server_name = adapter.name.0.clone();
+ let languages = self.languages.clone();
+ let language = language.clone();
+ let key = key.clone();
+
+ cx.spawn_weak(|this, mut cx| async move {
+ let result = Self::setup_and_insert_language_server(
+ this,
+ initialization_options,
+ pending_server,
+ adapter.clone(),
+ languages,
+ language.clone(),
+ server_id,
+ key,
+ &mut cx,
+ )
+ .await;
- LanguageServerState::Starting(cx.spawn_weak(|this, mut cx| async move {
- let workspace_config = cx.update(|cx| languages.workspace_configuration(cx)).await;
- let language_server = pending_server.task.await.log_err()?;
+ match result {
+ Ok(server) => server,
+
+ Err(err) => {
+ log::error!("failed to start language server {:?}: {}", server_name, err);
- language_server
- .on_notification::<lsp::notification::LogMessage, _>({
- move |params, mut cx| {
if let Some(this) = this.upgrade(&cx) {
- this.update(&mut cx, |_, cx| {
- cx.emit(Event::LanguageServerLog(server_id, params.message))
- });
- }
- }
- })
- .detach();
+ if let Some(container_dir) = container_dir {
+ let installation_test_binary = adapter
+ .installation_test_binary(container_dir.to_path_buf())
+ .await;
- language_server
- .on_notification::<lsp::notification::PublishDiagnostics, _>({
- let adapter = adapter.clone();
- move |mut params, cx| {
- let adapter = adapter.clone();
- cx.spawn(|mut cx| async move {
- adapter.process_diagnostics(&mut params).await;
- if let Some(this) = this.upgrade(&cx) {
- this.update(&mut cx, |this, cx| {
- this.update_diagnostics(
+ this.update(&mut cx, |_, cx| {
+ Self::check_errored_server(
+ language,
+ adapter,
server_id,
- params,
- &adapter.disk_based_diagnostic_sources,
+ installation_test_binary,
cx,
)
- .log_err();
});
}
- })
- .detach();
- }
- })
- .detach();
-
- language_server
- .on_request::<lsp::request::WorkspaceConfiguration, _, _>({
- let languages = languages.clone();
- move |params, mut cx| {
- let languages = languages.clone();
- async move {
- let workspace_config =
- cx.update(|cx| languages.workspace_configuration(cx)).await;
- Ok(params
- .items
- .into_iter()
- .map(|item| {
- if let Some(section) = &item.section {
- workspace_config
- .get(section)
- .cloned()
- .unwrap_or(serde_json::Value::Null)
- } else {
- workspace_config.clone()
- }
- })
- .collect())
}
+
+ None
}
- })
- .detach();
+ }
+ })
+ });
- // Even though we don't have handling for these requests, respond to them to
- // avoid stalling any language server like `gopls` which waits for a response
- // to these requests when initializing.
- language_server
- .on_request::<lsp::request::WorkDoneProgressCreate, _, _>(
- move |params, mut cx| async move {
- if let Some(this) = this.upgrade(&cx) {
- this.update(&mut cx, |this, _| {
- if let Some(status) =
- this.language_server_statuses.get_mut(&server_id)
- {
- if let lsp::NumberOrString::String(token) = params.token {
- status.progress_tokens.insert(token);
- }
- }
- });
- }
- Ok(())
- },
- )
- .detach();
- language_server
- .on_request::<lsp::request::RegisterCapability, _, _>(
- move |params, mut cx| async move {
- let this = this
- .upgrade(&cx)
- .ok_or_else(|| anyhow!("project dropped"))?;
- for reg in params.registrations {
- if reg.method == "workspace/didChangeWatchedFiles" {
- if let Some(options) = reg.register_options {
- let options = serde_json::from_value(options)?;
- this.update(&mut cx, |this, cx| {
- this.on_lsp_did_change_watched_files(
- server_id, options, cx,
- );
- });
- }
- }
- }
- Ok(())
- },
- )
- .detach();
+ self.language_servers.insert(server_id, state);
+ self.language_server_ids.insert(key, server_id);
+ }
- language_server
- .on_request::<lsp::request::ApplyWorkspaceEdit, _, _>({
- let adapter = adapter.clone();
- move |params, cx| {
- Self::on_lsp_workspace_edit(this, params, server_id, adapter.clone(), cx)
- }
- })
- .detach();
+ fn reinstall_language_server(
+ &mut self,
+ language: Arc<Language>,
+ adapter: Arc<CachedLspAdapter>,
+ server_id: LanguageServerId,
+ cx: &mut ModelContext<Self>,
+ ) -> Option<Task<()>> {
+ log::info!("beginning to reinstall server");
- let disk_based_diagnostics_progress_token =
- adapter.disk_based_diagnostics_progress_token.clone();
+ let existing_server = match self.language_servers.remove(&server_id) {
+ Some(LanguageServerState::Running { server, .. }) => Some(server),
+ _ => None,
+ };
- language_server
- .on_notification::<lsp::notification::Progress, _>({
- move |params, mut cx| {
+ for worktree in &self.worktrees {
+ if let Some(worktree) = worktree.upgrade(cx) {
+ let key = (worktree.read(cx).id(), adapter.name.clone());
+ self.language_server_ids.remove(&key);
+ }
+ }
+
+ Some(cx.spawn(move |this, mut cx| async move {
+ if let Some(task) = existing_server.and_then(|server| server.shutdown()) {
+ log::info!("shutting down existing server");
+ task.await;
+ }
+
+ // TODO: This is race-safe with regards to preventing new instances from
+ // starting while deleting, but existing instances in other projects are going
+ // to be very confused and messed up
+ this.update(&mut cx, |this, cx| {
+ this.languages.delete_server_container(adapter.clone(), cx)
+ })
+ .await;
+
+ this.update(&mut cx, |this, mut cx| {
+ let worktrees = this.worktrees.clone();
+ for worktree in worktrees {
+ let worktree = match worktree.upgrade(cx) {
+ Some(worktree) => worktree.read(cx),
+ None => continue,
+ };
+ let worktree_id = worktree.id();
+ let root_path = worktree.abs_path();
+
+ this.start_language_server(
+ worktree_id,
+ root_path,
+ adapter.clone(),
+ language.clone(),
+ &mut cx,
+ );
+ }
+ })
+ }))
+ }
+
+ async fn setup_and_insert_language_server(
+ this: WeakModelHandle<Self>,
+ initialization_options: Option<serde_json::Value>,
+ pending_server: PendingLanguageServer,
+ adapter: Arc<CachedLspAdapter>,
+ languages: Arc<LanguageRegistry>,
+ language: Arc<Language>,
+ server_id: LanguageServerId,
+ key: (WorktreeId, LanguageServerName),
+ cx: &mut AsyncAppContext,
+ ) -> Result<Option<Arc<LanguageServer>>> {
+ let setup = Self::setup_pending_language_server(
+ this,
+ initialization_options,
+ pending_server,
+ adapter.clone(),
+ languages,
+ server_id,
+ cx,
+ );
+
+ let language_server = match setup.await? {
+ Some(language_server) => language_server,
+ None => return Ok(None),
+ };
+
+ let this = match this.upgrade(cx) {
+ Some(this) => this,
+ None => return Err(anyhow!("failed to upgrade project handle")),
+ };
+
+ this.update(cx, |this, cx| {
+ this.insert_newly_running_language_server(
+ language,
+ adapter,
+ language_server.clone(),
+ server_id,
+ key,
+ cx,
+ )
+ })?;
+
+ Ok(Some(language_server))
+ }
+
+ async fn setup_pending_language_server(
+ this: WeakModelHandle<Self>,
+ initialization_options: Option<serde_json::Value>,
+ pending_server: PendingLanguageServer,
+ adapter: Arc<CachedLspAdapter>,
+ languages: Arc<LanguageRegistry>,
+ server_id: LanguageServerId,
+ cx: &mut AsyncAppContext,
+ ) -> Result<Option<Arc<LanguageServer>>> {
+ let workspace_config = cx.update(|cx| languages.workspace_configuration(cx)).await;
+ let language_server = match pending_server.task.await? {
+ Some(server) => server.initialize(initialization_options).await?,
+ None => {
+ return Ok(None);
+ }
+ };
+
+ language_server
+ .on_notification::<lsp::notification::LogMessage, _>({
+ move |params, mut cx| {
+ if let Some(this) = this.upgrade(&cx) {
+ this.update(&mut cx, |_, cx| {
+ cx.emit(Event::LanguageServerLog(server_id, params.message))
+ });
+ }
+ }
+ })
+ .detach();
+
+ language_server
+ .on_notification::<lsp::notification::PublishDiagnostics, _>({
+ let adapter = adapter.clone();
+ move |mut params, cx| {
+ let this = this;
+ let adapter = adapter.clone();
+ cx.spawn(|mut cx| async move {
+ adapter.process_diagnostics(&mut params).await;
if let Some(this) = this.upgrade(&cx) {
this.update(&mut cx, |this, cx| {
- this.on_lsp_progress(
- params,
+ this.update_diagnostics(
server_id,
- disk_based_diagnostics_progress_token.clone(),
+ params,
+ &adapter.disk_based_diagnostic_sources,
cx,
- );
+ )
+ .log_err();
});
}
+ })
+ .detach();
+ }
+ })
+ .detach();
+
+ language_server
+ .on_request::<lsp::request::WorkspaceConfiguration, _, _>({
+ let languages = languages.clone();
+ move |params, mut cx| {
+ let languages = languages.clone();
+ async move {
+ let workspace_config =
+ cx.update(|cx| languages.workspace_configuration(cx)).await;
+ Ok(params
+ .items
+ .into_iter()
+ .map(|item| {
+ if let Some(section) = &item.section {
+ workspace_config
+ .get(section)
+ .cloned()
+ .unwrap_or(serde_json::Value::Null)
+ } else {
+ workspace_config.clone()
+ }
+ })
+ .collect())
}
- })
- .detach();
+ }
+ })
+ .detach();
- let language_server = language_server
- .initialize(initialization_options)
- .await
- .log_err()?;
- language_server
- .notify::<lsp::notification::DidChangeConfiguration>(
- lsp::DidChangeConfigurationParams {
- settings: workspace_config,
- },
- )
- .ok();
+ // Even though we don't have handling for these requests, respond to them to
+ // avoid stalling any language server like `gopls` which waits for a response
+ // to these requests when initializing.
+ language_server
+ .on_request::<lsp::request::WorkDoneProgressCreate, _, _>(
+ move |params, mut cx| async move {
+ if let Some(this) = this.upgrade(&cx) {
+ this.update(&mut cx, |this, _| {
+ if let Some(status) = this.language_server_statuses.get_mut(&server_id)
+ {
+ if let lsp::NumberOrString::String(token) = params.token {
+ status.progress_tokens.insert(token);
+ }
+ }
+ });
+ }
+ Ok(())
+ },
+ )
+ .detach();
+ language_server
+ .on_request::<lsp::request::RegisterCapability, _, _>({
+ move |params, mut cx| async move {
+ let this = this
+ .upgrade(&cx)
+ .ok_or_else(|| anyhow!("project dropped"))?;
+ for reg in params.registrations {
+ if reg.method == "workspace/didChangeWatchedFiles" {
+ if let Some(options) = reg.register_options {
+ let options = serde_json::from_value(options)?;
+ this.update(&mut cx, |this, cx| {
+ this.on_lsp_did_change_watched_files(server_id, options, cx);
+ });
+ }
+ }
+ }
+ Ok(())
+ }
+ })
+ .detach();
- let this = this.upgrade(&cx)?;
- this.update(&mut cx, |this, cx| {
- // If the language server for this key doesn't match the server id, don't store the
- // server. Which will cause it to be dropped, killing the process
- if this
- .language_server_ids
- .get(&key)
- .map(|id| id != &server_id)
- .unwrap_or(false)
- {
- return None;
+ language_server
+ .on_request::<lsp::request::ApplyWorkspaceEdit, _, _>({
+ let adapter = adapter.clone();
+ move |params, cx| {
+ Self::on_lsp_workspace_edit(this, params, server_id, adapter.clone(), cx)
}
+ })
+ .detach();
- // Update language_servers collection with Running variant of LanguageServerState
- // indicating that the server is up and running and ready
- this.language_servers.insert(
- server_id,
- LanguageServerState::Running {
- adapter: adapter.clone(),
- language: language.clone(),
- watched_paths: Default::default(),
- server: language_server.clone(),
- simulate_disk_based_diagnostics_completion: None,
- },
- );
- this.language_server_statuses.insert(
- server_id,
- LanguageServerStatus {
- name: language_server.name().to_string(),
- pending_work: Default::default(),
- has_pending_diagnostic_updates: false,
- progress_tokens: Default::default(),
- },
- );
+ language_server
+ .on_request::<lsp::request::InlayHintRefreshRequest, _, _>({
+ move |(), mut cx| async move {
+ let this = this
+ .upgrade(&cx)
+ .ok_or_else(|| anyhow!("project dropped"))?;
+ this.update(&mut cx, |project, cx| {
+ cx.emit(Event::RefreshInlays);
+ project.remote_id().map(|project_id| {
+ project.client.send(proto::RefreshInlayHints { project_id })
+ })
+ })
+ .transpose()?;
+ Ok(())
+ }
+ })
+ .detach();
- cx.emit(Event::LanguageServerAdded(server_id));
+ let disk_based_diagnostics_progress_token =
+ adapter.disk_based_diagnostics_progress_token.clone();
- if let Some(project_id) = this.remote_id() {
- this.client
- .send(proto::StartLanguageServer {
- project_id,
- server: Some(proto::LanguageServer {
- id: server_id.0 as u64,
- name: language_server.name().to_string(),
- }),
- })
- .log_err();
+ language_server
+ .on_notification::<lsp::notification::Progress, _>(move |params, mut cx| {
+ if let Some(this) = this.upgrade(&cx) {
+ this.update(&mut cx, |this, cx| {
+ this.on_lsp_progress(
+ params,
+ server_id,
+ disk_based_diagnostics_progress_token.clone(),
+ cx,
+ );
+ });
}
+ })
+ .detach();
- // Tell the language server about every open buffer in the worktree that matches the language.
- for buffer in this.opened_buffers.values() {
- if let Some(buffer_handle) = buffer.upgrade(cx) {
- let buffer = buffer_handle.read(cx);
- let file = match File::from_dyn(buffer.file()) {
- Some(file) => file,
- None => continue,
- };
- let language = match buffer.language() {
- Some(language) => language,
- None => continue,
- };
+ language_server
+ .notify::<lsp::notification::DidChangeConfiguration>(
+ lsp::DidChangeConfigurationParams {
+ settings: workspace_config,
+ },
+ )
+ .ok();
- if file.worktree.read(cx).id() != key.0
- || !language.lsp_adapters().iter().any(|a| a.name == key.1)
- {
- continue;
- }
+ Ok(Some(language_server))
+ }
- let file = file.as_local()?;
- let versions = this
- .buffer_snapshots
- .entry(buffer.remote_id())
- .or_default()
- .entry(server_id)
- .or_insert_with(|| {
- vec![LspBufferSnapshot {
- version: 0,
- snapshot: buffer.text_snapshot(),
- }]
- });
+ fn insert_newly_running_language_server(
+ &mut self,
+ language: Arc<Language>,
+ adapter: Arc<CachedLspAdapter>,
+ language_server: Arc<LanguageServer>,
+ server_id: LanguageServerId,
+ key: (WorktreeId, LanguageServerName),
+ cx: &mut ModelContext<Self>,
+ ) -> Result<()> {
+ // If the language server for this key doesn't match the server id, don't store the
+ // server. Which will cause it to be dropped, killing the process
+ if self
+ .language_server_ids
+ .get(&key)
+ .map(|id| id != &server_id)
+ .unwrap_or(false)
+ {
+ return Ok(());
+ }
- let snapshot = versions.last().unwrap();
- let version = snapshot.version;
- let initial_snapshot = &snapshot.snapshot;
- let uri = lsp::Url::from_file_path(file.abs_path(cx)).unwrap();
- language_server
- .notify::<lsp::notification::DidOpenTextDocument>(
- lsp::DidOpenTextDocumentParams {
- text_document: lsp::TextDocumentItem::new(
- uri,
- adapter
- .language_ids
- .get(language.name().as_ref())
- .cloned()
- .unwrap_or_default(),
- version,
- initial_snapshot.text(),
- ),
- },
- )
- .log_err()?;
- buffer_handle.update(cx, |buffer, cx| {
- buffer.set_completion_triggers(
- language_server
- .capabilities()
- .completion_provider
- .as_ref()
- .and_then(|provider| provider.trigger_characters.clone())
- .unwrap_or_default(),
- cx,
- )
- });
- }
+ // Update language_servers collection with Running variant of LanguageServerState
+ // indicating that the server is up and running and ready
+ self.language_servers.insert(
+ server_id,
+ LanguageServerState::Running {
+ adapter: adapter.clone(),
+ language: language.clone(),
+ watched_paths: Default::default(),
+ server: language_server.clone(),
+ simulate_disk_based_diagnostics_completion: None,
+ },
+ );
+
+ self.language_server_statuses.insert(
+ server_id,
+ LanguageServerStatus {
+ name: language_server.name().to_string(),
+ pending_work: Default::default(),
+ has_pending_diagnostic_updates: false,
+ progress_tokens: Default::default(),
+ },
+ );
+
+ cx.emit(Event::LanguageServerAdded(server_id));
+
+ if let Some(project_id) = self.remote_id() {
+ self.client.send(proto::StartLanguageServer {
+ project_id,
+ server: Some(proto::LanguageServer {
+ id: server_id.0 as u64,
+ name: language_server.name().to_string(),
+ }),
+ })?;
+ }
+
+ // Tell the language server about every open buffer in the worktree that matches the language.
+ for buffer in self.opened_buffers.values() {
+ if let Some(buffer_handle) = buffer.upgrade(cx) {
+ let buffer = buffer_handle.read(cx);
+ let file = match File::from_dyn(buffer.file()) {
+ Some(file) => file,
+ None => continue,
+ };
+ let language = match buffer.language() {
+ Some(language) => language,
+ None => continue,
+ };
+
+ if file.worktree.read(cx).id() != key.0
+ || !language.lsp_adapters().iter().any(|a| a.name == key.1)
+ {
+ continue;
}
- cx.notify();
- Some(language_server)
- })
- }))
+ let file = match file.as_local() {
+ Some(file) => file,
+ None => continue,
+ };
+
+ let versions = self
+ .buffer_snapshots
+ .entry(buffer.remote_id())
+ .or_default()
+ .entry(server_id)
+ .or_insert_with(|| {
+ vec![LspBufferSnapshot {
+ version: 0,
+ snapshot: buffer.text_snapshot(),
+ }]
+ });
+
+ let snapshot = versions.last().unwrap();
+ let version = snapshot.version;
+ let initial_snapshot = &snapshot.snapshot;
+ let uri = lsp::Url::from_file_path(file.abs_path(cx)).unwrap();
+ language_server.notify::<lsp::notification::DidOpenTextDocument>(
+ lsp::DidOpenTextDocumentParams {
+ text_document: lsp::TextDocumentItem::new(
+ uri,
+ adapter
+ .language_ids
+ .get(language.name().as_ref())
+ .cloned()
+ .unwrap_or_default(),
+ version,
+ initial_snapshot.text(),
+ ),
+ },
+ )?;
+
+ buffer_handle.update(cx, |buffer, cx| {
+ buffer.set_completion_triggers(
+ language_server
+ .capabilities()
+ .completion_provider
+ .as_ref()
+ .and_then(|provider| provider.trigger_characters.clone())
+ .unwrap_or_default(),
+ cx,
+ )
+ });
+ }
+ }
+
+ cx.notify();
+ Ok(())
}
// Returns a list of all of the worktrees which no longer have a language server and the root path
@@ -2814,9 +3049,7 @@ impl Project {
let mut root_path = None;
let server = match server_state {
- Some(LanguageServerState::Starting(started_language_server)) => {
- started_language_server.await
- }
+ Some(LanguageServerState::Starting(task)) => task.await,
Some(LanguageServerState::Running { server, .. }) => Some(server),
None => None,
};
@@ -2927,6 +3160,72 @@ impl Project {
.detach();
}
+ fn check_errored_server(
+ language: Arc<Language>,
+ adapter: Arc<CachedLspAdapter>,
+ server_id: LanguageServerId,
+ installation_test_binary: Option<LanguageServerBinary>,
+ cx: &mut ModelContext<Self>,
+ ) {
+ if !adapter.can_be_reinstalled() {
+ log::info!(
+ "Validation check requested for {:?} but it cannot be reinstalled",
+ adapter.name.0
+ );
+ return;
+ }
+
+ cx.spawn(|this, mut cx| async move {
+ log::info!("About to spawn test binary");
+
+ // A lack of test binary counts as a failure
+ let process = installation_test_binary.and_then(|binary| {
+ smol::process::Command::new(&binary.path)
+ .current_dir(&binary.path)
+ .args(binary.arguments)
+ .stdin(Stdio::piped())
+ .stdout(Stdio::piped())
+ .stderr(Stdio::inherit())
+ .kill_on_drop(true)
+ .spawn()
+ .ok()
+ });
+
+ const PROCESS_TIMEOUT: Duration = Duration::from_secs(5);
+ let mut timeout = cx.background().timer(PROCESS_TIMEOUT).fuse();
+
+ let mut errored = false;
+ if let Some(mut process) = process {
+ futures::select! {
+ status = process.status().fuse() => match status {
+ Ok(status) => errored = !status.success(),
+ Err(_) => errored = true,
+ },
+
+ _ = timeout => {
+ log::info!("test binary time-ed out, this counts as a success");
+ _ = process.kill();
+ }
+ }
+ } else {
+ log::warn!("test binary failed to launch");
+ errored = true;
+ }
+
+ if errored {
+ log::warn!("test binary check failed");
+ let task = this.update(&mut cx, move |this, mut cx| {
+ this.reinstall_language_server(language, adapter, server_id, &mut cx)
+ });
+
+ if let Some(task) = task {
+ task.await;
+ }
+ }
+ })
+ .detach();
+ }
+
fn on_lsp_progress(
&mut self,
progress: lsp::ProgressParams,
@@ -596,6 +596,8 @@ async fn test_reporting_fs_changes_to_language_servers(cx: &mut gpui::TestAppCon
);
});
+ let prev_read_dir_count = fs.read_dir_call_count();
+
// Keep track of the FS events reported to the language server.
let fake_server = fake_servers.next().await.unwrap();
let file_changes = Arc::new(Mutex::new(Vec::new()));
@@ -607,6 +609,12 @@ async fn test_reporting_fs_changes_to_language_servers(cx: &mut gpui::TestAppCon
register_options: serde_json::to_value(
lsp::DidChangeWatchedFilesRegistrationOptions {
watchers: vec![
+ lsp::FileSystemWatcher {
+ glob_pattern: lsp::GlobPattern::String(
+ "/the-root/Cargo.toml".to_string(),
+ ),
+ kind: None,
+ },
lsp::FileSystemWatcher {
glob_pattern: lsp::GlobPattern::String(
"/the-root/src/*.{rs,c}".to_string(),
@@ -638,6 +646,7 @@ async fn test_reporting_fs_changes_to_language_servers(cx: &mut gpui::TestAppCon
cx.foreground().run_until_parked();
assert_eq!(mem::take(&mut *file_changes.lock()), &[]);
+ assert_eq!(fs.read_dir_call_count() - prev_read_dir_count, 4);
// Now the language server has asked us to watch an ignored directory path,
// so we recursively load it.
@@ -3071,17 +3071,20 @@ impl BackgroundScanner {
path_prefix = self.path_prefixes_to_scan_rx.recv().fuse() => {
let Ok(path_prefix) = path_prefix else { break };
+ log::trace!("adding path prefix {:?}", path_prefix);
- self.forcibly_load_paths(&[path_prefix.clone()]).await;
+ let did_scan = self.forcibly_load_paths(&[path_prefix.clone()]).await;
+ if did_scan {
+ let abs_path =
+ {
+ let mut state = self.state.lock();
+ state.path_prefixes_to_scan.insert(path_prefix.clone());
+ state.snapshot.abs_path.join(&path_prefix)
+ };
- let abs_path =
- {
- let mut state = self.state.lock();
- state.path_prefixes_to_scan.insert(path_prefix.clone());
- state.snapshot.abs_path.join(path_prefix)
- };
- if let Some(abs_path) = self.fs.canonicalize(&abs_path).await.log_err() {
- self.process_events(vec![abs_path]).await;
+ if let Some(abs_path) = self.fs.canonicalize(&abs_path).await.log_err() {
+ self.process_events(vec![abs_path]).await;
+ }
}
}
@@ -3097,10 +3100,13 @@ impl BackgroundScanner {
}
}
- async fn process_scan_request(&self, request: ScanRequest, scanning: bool) -> bool {
+ async fn process_scan_request(&self, mut request: ScanRequest, scanning: bool) -> bool {
log::debug!("rescanning paths {:?}", request.relative_paths);
- let root_path = self.forcibly_load_paths(&request.relative_paths).await;
+ request.relative_paths.sort_unstable();
+ self.forcibly_load_paths(&request.relative_paths).await;
+
+ let root_path = self.state.lock().snapshot.abs_path.clone();
let root_canonical_path = match self.fs.canonicalize(&root_path).await {
Ok(path) => path,
Err(err) => {
@@ -3108,10 +3114,9 @@ impl BackgroundScanner {
return false;
}
};
-
let abs_paths = request
.relative_paths
- .into_iter()
+ .iter()
.map(|path| {
if path.file_name().is_some() {
root_canonical_path.join(path)
@@ -3120,12 +3125,19 @@ impl BackgroundScanner {
}
})
.collect::<Vec<_>>();
- self.reload_entries_for_paths(root_path, root_canonical_path, abs_paths, None)
- .await;
+
+ self.reload_entries_for_paths(
+ root_path,
+ root_canonical_path,
+ &request.relative_paths,
+ abs_paths,
+ None,
+ )
+ .await;
self.send_status_update(scanning, Some(request.done))
}
- async fn process_events(&mut self, abs_paths: Vec<PathBuf>) {
+ async fn process_events(&mut self, mut abs_paths: Vec<PathBuf>) {
log::debug!("received fs events {:?}", abs_paths);
let root_path = self.state.lock().snapshot.abs_path.clone();
@@ -3137,25 +3149,61 @@ impl BackgroundScanner {
}
};
- let (scan_job_tx, scan_job_rx) = channel::unbounded();
- let paths = self
- .reload_entries_for_paths(
+ let mut relative_paths = Vec::with_capacity(abs_paths.len());
+ let mut unloaded_relative_paths = Vec::new();
+ abs_paths.sort_unstable();
+ abs_paths.dedup_by(|a, b| a.starts_with(&b));
+ abs_paths.retain(|abs_path| {
+ let snapshot = &self.state.lock().snapshot;
+ {
+ let relative_path: Arc<Path> =
+ if let Ok(path) = abs_path.strip_prefix(&root_canonical_path) {
+ path.into()
+ } else {
+ log::error!(
+ "ignoring event {abs_path:?} outside of root path {root_canonical_path:?}",
+ );
+ return false;
+ };
+
+ let parent_dir_is_loaded = relative_path.parent().map_or(true, |parent| {
+ snapshot
+ .entry_for_path(parent)
+ .map_or(false, |entry| entry.kind == EntryKind::Dir)
+ });
+ if !parent_dir_is_loaded {
+ log::debug!("ignoring event {relative_path:?} within unloaded directory");
+ unloaded_relative_paths.push(relative_path);
+ return false;
+ }
+
+ relative_paths.push(relative_path);
+ true
+ }
+ });
+
+ if !relative_paths.is_empty() {
+ let (scan_job_tx, scan_job_rx) = channel::unbounded();
+ self.reload_entries_for_paths(
root_path,
root_canonical_path,
+ &relative_paths,
abs_paths,
Some(scan_job_tx.clone()),
)
.await;
- drop(scan_job_tx);
- self.scan_dirs(false, scan_job_rx).await;
+ drop(scan_job_tx);
+ self.scan_dirs(false, scan_job_rx).await;
- let (scan_job_tx, scan_job_rx) = channel::unbounded();
- self.update_ignore_statuses(scan_job_tx).await;
- self.scan_dirs(false, scan_job_rx).await;
+ let (scan_job_tx, scan_job_rx) = channel::unbounded();
+ self.update_ignore_statuses(scan_job_tx).await;
+ self.scan_dirs(false, scan_job_rx).await;
+ }
{
let mut state = self.state.lock();
- state.reload_repositories(&paths, self.fs.as_ref());
+ relative_paths.extend(unloaded_relative_paths);
+ state.reload_repositories(&relative_paths, self.fs.as_ref());
state.snapshot.completed_scan_id = state.snapshot.scan_id;
for (_, entry_id) in mem::take(&mut state.removed_entry_ids) {
state.scanned_dirs.remove(&entry_id);
@@ -3165,12 +3213,11 @@ impl BackgroundScanner {
self.send_status_update(false, None);
}
- async fn forcibly_load_paths(&self, paths: &[Arc<Path>]) -> Arc<Path> {
- let root_path;
+ async fn forcibly_load_paths(&self, paths: &[Arc<Path>]) -> bool {
let (scan_job_tx, mut scan_job_rx) = channel::unbounded();
{
let mut state = self.state.lock();
- root_path = state.snapshot.abs_path.clone();
+ let root_path = state.snapshot.abs_path.clone();
for path in paths {
for ancestor in path.ancestors() {
if let Some(entry) = state.snapshot.entry_for_path(ancestor) {
@@ -3201,8 +3248,8 @@ impl BackgroundScanner {
while let Some(job) = scan_job_rx.next().await {
self.scan_dir(&job).await.log_err();
}
- self.state.lock().paths_to_scan.clear();
- root_path
+
+ mem::take(&mut self.state.lock().paths_to_scan).len() > 0
}
async fn scan_dirs(
@@ -3475,7 +3522,7 @@ impl BackgroundScanner {
.expect("channel is unbounded");
}
} else {
- log::debug!("defer scanning directory {:?} {:?}", entry.path, entry.kind);
+ log::debug!("defer scanning directory {:?}", entry.path);
entry.kind = EntryKind::UnloadedDir;
}
}
@@ -3490,26 +3537,10 @@ impl BackgroundScanner {
&self,
root_abs_path: Arc<Path>,
root_canonical_path: PathBuf,
- mut abs_paths: Vec<PathBuf>,
+ relative_paths: &[Arc<Path>],
+ abs_paths: Vec<PathBuf>,
scan_queue_tx: Option<Sender<ScanJob>>,
- ) -> Vec<Arc<Path>> {
- let mut event_paths = Vec::<Arc<Path>>::with_capacity(abs_paths.len());
- abs_paths.sort_unstable();
- abs_paths.dedup_by(|a, b| a.starts_with(&b));
- abs_paths.retain(|abs_path| {
- if let Ok(path) = abs_path.strip_prefix(&root_canonical_path) {
- event_paths.push(path.into());
- true
- } else {
- log::error!(
- "unexpected event {:?} for root path {:?}",
- abs_path,
- root_canonical_path
- );
- false
- }
- });
-
+ ) {
let metadata = futures::future::join_all(
abs_paths
.iter()
@@ -3538,30 +3569,15 @@ impl BackgroundScanner {
// Remove any entries for paths that no longer exist or are being recursively
// refreshed. Do this before adding any new entries, so that renames can be
// detected regardless of the order of the paths.
- for (path, metadata) in event_paths.iter().zip(metadata.iter()) {
+ for (path, metadata) in relative_paths.iter().zip(metadata.iter()) {
if matches!(metadata, Ok(None)) || doing_recursive_update {
log::trace!("remove path {:?}", path);
state.remove_path(path);
}
}
- for (path, metadata) in event_paths.iter().zip(metadata.iter()) {
- if let (Some(parent), true) = (path.parent(), doing_recursive_update) {
- if state
- .snapshot
- .entry_for_path(parent)
- .map_or(true, |entry| entry.kind != EntryKind::Dir)
- {
- log::debug!(
- "ignoring event {path:?} within unloaded directory {:?}",
- parent
- );
- continue;
- }
- }
-
+ for (path, metadata) in relative_paths.iter().zip(metadata.iter()) {
let abs_path: Arc<Path> = root_abs_path.join(&path).into();
-
match metadata {
Ok(Some((metadata, canonical_path))) => {
let ignore_stack = state
@@ -3624,12 +3640,10 @@ impl BackgroundScanner {
util::extend_sorted(
&mut state.changed_paths,
- event_paths.iter().cloned(),
+ relative_paths.iter().cloned(),
usize::MAX,
Ord::cmp,
);
-
- event_paths
}
fn remove_repo_path(&self, path: &Path, snapshot: &mut LocalSnapshot) -> Option<()> {
@@ -3760,25 +3774,22 @@ impl BackgroundScanner {
// Scan any directories that were previously ignored and weren't
// previously scanned.
- if was_ignored
- && !entry.is_ignored
- && !entry.is_external
- && entry.kind == EntryKind::UnloadedDir
- {
- job.scan_queue
- .try_send(ScanJob {
- abs_path: abs_path.clone(),
- path: entry.path.clone(),
- ignore_stack: child_ignore_stack.clone(),
- scan_queue: job.scan_queue.clone(),
- ancestor_inodes: self
- .state
- .lock()
- .snapshot
- .ancestor_inodes_for_path(&entry.path),
- is_external: false,
- })
- .unwrap();
+ if was_ignored && !entry.is_ignored && entry.kind.is_unloaded() {
+ let state = self.state.lock();
+ if state.should_scan_directory(&entry) {
+ job.scan_queue
+ .try_send(ScanJob {
+ abs_path: abs_path.clone(),
+ path: entry.path.clone(),
+ ignore_stack: child_ignore_stack.clone(),
+ scan_queue: job.scan_queue.clone(),
+ ancestor_inodes: state
+ .snapshot
+ .ancestor_inodes_for_path(&entry.path),
+ is_external: false,
+ })
+ .unwrap();
+ }
}
job.ignore_queue
@@ -454,6 +454,10 @@ async fn test_open_gitignored_files(cx: &mut TestAppContext) {
"b1.js": "b1",
"b2.js": "b2",
},
+ "c": {
+ "c1.js": "c1",
+ "c2.js": "c2",
+ }
},
},
"two": {
@@ -521,6 +525,7 @@ async fn test_open_gitignored_files(cx: &mut TestAppContext) {
(Path::new("one/node_modules/b"), true),
(Path::new("one/node_modules/b/b1.js"), true),
(Path::new("one/node_modules/b/b2.js"), true),
+ (Path::new("one/node_modules/c"), true),
(Path::new("two"), false),
(Path::new("two/x.js"), false),
(Path::new("two/y.js"), false),
@@ -564,6 +569,7 @@ async fn test_open_gitignored_files(cx: &mut TestAppContext) {
(Path::new("one/node_modules/b"), true),
(Path::new("one/node_modules/b/b1.js"), true),
(Path::new("one/node_modules/b/b2.js"), true),
+ (Path::new("one/node_modules/c"), true),
(Path::new("two"), false),
(Path::new("two/x.js"), false),
(Path::new("two/y.js"), false),
@@ -578,6 +584,17 @@ async fn test_open_gitignored_files(cx: &mut TestAppContext) {
// Only the newly-expanded directory is scanned.
assert_eq!(fs.read_dir_call_count() - prev_read_dir_count, 1);
});
+
+ // No work happens when files and directories change within an unloaded directory.
+ let prev_fs_call_count = fs.read_dir_call_count() + fs.metadata_call_count();
+ fs.create_dir("/root/one/node_modules/c/lib".as_ref())
+ .await
+ .unwrap();
+ cx.foreground().run_until_parked();
+ assert_eq!(
+ fs.read_dir_call_count() + fs.metadata_call_count() - prev_fs_call_count,
+ 0
+ );
}
#[gpui::test]
@@ -384,6 +384,12 @@ impl<'a> From<&'a str> for Rope {
}
}
+impl From<String> for Rope {
+ fn from(text: String) -> Self {
+ Rope::from(text.as_str())
+ }
+}
+
impl fmt::Display for Rope {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
for chunk in self.chunks() {
@@ -136,6 +136,10 @@ message Envelope {
OnTypeFormattingResponse on_type_formatting_response = 112;
UpdateWorktreeSettings update_worktree_settings = 113;
+
+ InlayHints inlay_hints = 116;
+ InlayHintsResponse inlay_hints_response = 117;
+ RefreshInlayHints refresh_inlay_hints = 118;
}
}
@@ -705,6 +709,68 @@ message OnTypeFormattingResponse {
Transaction transaction = 1;
}
+message InlayHints {
+ uint64 project_id = 1;
+ uint64 buffer_id = 2;
+ Anchor start = 3;
+ Anchor end = 4;
+ repeated VectorClockEntry version = 5;
+}
+
+message InlayHintsResponse {
+ repeated InlayHint hints = 1;
+ repeated VectorClockEntry version = 2;
+}
+
+message InlayHint {
+ Anchor position = 1;
+ InlayHintLabel label = 2;
+ optional string kind = 3;
+ bool padding_left = 4;
+ bool padding_right = 5;
+ InlayHintTooltip tooltip = 6;
+}
+
+message InlayHintLabel {
+ oneof label {
+ string value = 1;
+ InlayHintLabelParts label_parts = 2;
+ }
+}
+
+message InlayHintLabelParts {
+ repeated InlayHintLabelPart parts = 1;
+}
+
+message InlayHintLabelPart {
+ string value = 1;
+ InlayHintLabelPartTooltip tooltip = 2;
+ Location location = 3;
+}
+
+message InlayHintTooltip {
+ oneof content {
+ string value = 1;
+ MarkupContent markup_content = 2;
+ }
+}
+
+message InlayHintLabelPartTooltip {
+ oneof content {
+ string value = 1;
+ MarkupContent markup_content = 2;
+ }
+}
+
+message RefreshInlayHints {
+ uint64 project_id = 1;
+}
+
+message MarkupContent {
+ string kind = 1;
+ string value = 2;
+}
+
message PerformRenameResponse {
ProjectTransaction transaction = 2;
}
@@ -198,6 +198,9 @@ messages!(
(PerformRenameResponse, Background),
(OnTypeFormatting, Background),
(OnTypeFormattingResponse, Background),
+ (InlayHints, Background),
+ (InlayHintsResponse, Background),
+ (RefreshInlayHints, Foreground),
(Ping, Foreground),
(PrepareRename, Background),
(PrepareRenameResponse, Background),
@@ -286,6 +289,8 @@ request_messages!(
(PerformRename, PerformRenameResponse),
(PrepareRename, PrepareRenameResponse),
(OnTypeFormatting, OnTypeFormattingResponse),
+ (InlayHints, InlayHintsResponse),
+ (RefreshInlayHints, Ack),
(ReloadBuffers, ReloadBuffersResponse),
(RequestContact, Ack),
(RemoveContact, Ack),
@@ -332,6 +337,8 @@ entity_messages!(
OpenBufferForSymbol,
PerformRename,
OnTypeFormatting,
+ InlayHints,
+ RefreshInlayHints,
PrepareRename,
ReloadBuffers,
RemoveProjectCollaborator,
@@ -97,6 +97,42 @@ where
}
}
+ pub fn next_item(&self) -> Option<&'a T> {
+ self.assert_did_seek();
+ if let Some(entry) = self.stack.last() {
+ if entry.index == entry.tree.0.items().len() - 1 {
+ if let Some(next_leaf) = self.next_leaf() {
+ Some(next_leaf.0.items().first().unwrap())
+ } else {
+ None
+ }
+ } else {
+ match *entry.tree.0 {
+ Node::Leaf { ref items, .. } => Some(&items[entry.index + 1]),
+ _ => unreachable!(),
+ }
+ }
+ } else if self.at_end {
+ None
+ } else {
+ self.tree.first()
+ }
+ }
+
+ fn next_leaf(&self) -> Option<&'a SumTree<T>> {
+ for entry in self.stack.iter().rev().skip(1) {
+ if entry.index < entry.tree.0.child_trees().len() - 1 {
+ match *entry.tree.0 {
+ Node::Internal {
+ ref child_trees, ..
+ } => return Some(child_trees[entry.index + 1].leftmost_leaf()),
+ Node::Leaf { .. } => unreachable!(),
+ };
+ }
+ }
+ None
+ }
+
pub fn prev_item(&self) -> Option<&'a T> {
self.assert_did_seek();
if let Some(entry) = self.stack.last() {
@@ -95,31 +95,18 @@ impl<D> fmt::Debug for End<D> {
}
}
-#[derive(Copy, Clone, Eq, PartialEq, Debug, Hash)]
+#[derive(Copy, Clone, Eq, PartialEq, PartialOrd, Ord, Debug, Hash, Default)]
pub enum Bias {
+ #[default]
Left,
Right,
}
-impl Default for Bias {
- fn default() -> Self {
- Bias::Left
- }
-}
-
-impl PartialOrd for Bias {
- fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
- Some(self.cmp(other))
- }
-}
-
-impl Ord for Bias {
- fn cmp(&self, other: &Self) -> Ordering {
- match (self, other) {
- (Self::Left, Self::Left) => Ordering::Equal,
- (Self::Left, Self::Right) => Ordering::Less,
- (Self::Right, Self::Right) => Ordering::Equal,
- (Self::Right, Self::Left) => Ordering::Greater,
+impl Bias {
+ pub fn invert(self) -> Self {
+ match self {
+ Self::Left => Self::Right,
+ Self::Right => Self::Left,
}
}
}
@@ -838,6 +825,14 @@ mod tests {
assert_eq!(cursor.item(), None);
}
+ if before_start {
+ assert_eq!(cursor.next_item(), reference_items.get(0));
+ } else if pos + 1 < reference_items.len() {
+ assert_eq!(cursor.next_item().unwrap(), &reference_items[pos + 1]);
+ } else {
+ assert_eq!(cursor.next_item(), None);
+ }
+
if i < 5 {
cursor.next(&());
if pos < reference_items.len() {
@@ -883,14 +878,17 @@ mod tests {
);
assert_eq!(cursor.item(), None);
assert_eq!(cursor.prev_item(), None);
+ assert_eq!(cursor.next_item(), None);
assert_eq!(cursor.start().sum, 0);
cursor.prev(&());
assert_eq!(cursor.item(), None);
assert_eq!(cursor.prev_item(), None);
+ assert_eq!(cursor.next_item(), None);
assert_eq!(cursor.start().sum, 0);
cursor.next(&());
assert_eq!(cursor.item(), None);
assert_eq!(cursor.prev_item(), None);
+ assert_eq!(cursor.next_item(), None);
assert_eq!(cursor.start().sum, 0);
// Single-element tree
@@ -903,22 +901,26 @@ mod tests {
);
assert_eq!(cursor.item(), Some(&1));
assert_eq!(cursor.prev_item(), None);
+ assert_eq!(cursor.next_item(), None);
assert_eq!(cursor.start().sum, 0);
cursor.next(&());
assert_eq!(cursor.item(), None);
assert_eq!(cursor.prev_item(), Some(&1));
+ assert_eq!(cursor.next_item(), None);
assert_eq!(cursor.start().sum, 1);
cursor.prev(&());
assert_eq!(cursor.item(), Some(&1));
assert_eq!(cursor.prev_item(), None);
+ assert_eq!(cursor.next_item(), None);
assert_eq!(cursor.start().sum, 0);
let mut cursor = tree.cursor::<IntegersSummary>();
assert_eq!(cursor.slice(&Count(1), Bias::Right, &()).items(&()), [1]);
assert_eq!(cursor.item(), None);
assert_eq!(cursor.prev_item(), Some(&1));
+ assert_eq!(cursor.next_item(), None);
assert_eq!(cursor.start().sum, 1);
cursor.seek(&Count(0), Bias::Right, &());
@@ -930,6 +932,7 @@ mod tests {
);
assert_eq!(cursor.item(), None);
assert_eq!(cursor.prev_item(), Some(&1));
+ assert_eq!(cursor.next_item(), None);
assert_eq!(cursor.start().sum, 1);
// Multiple-element tree
@@ -940,67 +943,80 @@ mod tests {
assert_eq!(cursor.slice(&Count(2), Bias::Right, &()).items(&()), [1, 2]);
assert_eq!(cursor.item(), Some(&3));
assert_eq!(cursor.prev_item(), Some(&2));
+ assert_eq!(cursor.next_item(), Some(&4));
assert_eq!(cursor.start().sum, 3);
cursor.next(&());
assert_eq!(cursor.item(), Some(&4));
assert_eq!(cursor.prev_item(), Some(&3));
+ assert_eq!(cursor.next_item(), Some(&5));
assert_eq!(cursor.start().sum, 6);
cursor.next(&());
assert_eq!(cursor.item(), Some(&5));
assert_eq!(cursor.prev_item(), Some(&4));
+ assert_eq!(cursor.next_item(), Some(&6));
assert_eq!(cursor.start().sum, 10);
cursor.next(&());
assert_eq!(cursor.item(), Some(&6));
assert_eq!(cursor.prev_item(), Some(&5));
+ assert_eq!(cursor.next_item(), None);
assert_eq!(cursor.start().sum, 15);
cursor.next(&());
cursor.next(&());
assert_eq!(cursor.item(), None);
assert_eq!(cursor.prev_item(), Some(&6));
+ assert_eq!(cursor.next_item(), None);
assert_eq!(cursor.start().sum, 21);
cursor.prev(&());
assert_eq!(cursor.item(), Some(&6));
assert_eq!(cursor.prev_item(), Some(&5));
+ assert_eq!(cursor.next_item(), None);
assert_eq!(cursor.start().sum, 15);
cursor.prev(&());
assert_eq!(cursor.item(), Some(&5));
assert_eq!(cursor.prev_item(), Some(&4));
+ assert_eq!(cursor.next_item(), Some(&6));
assert_eq!(cursor.start().sum, 10);
cursor.prev(&());
assert_eq!(cursor.item(), Some(&4));
assert_eq!(cursor.prev_item(), Some(&3));
+ assert_eq!(cursor.next_item(), Some(&5));
assert_eq!(cursor.start().sum, 6);
cursor.prev(&());
assert_eq!(cursor.item(), Some(&3));
assert_eq!(cursor.prev_item(), Some(&2));
+ assert_eq!(cursor.next_item(), Some(&4));
assert_eq!(cursor.start().sum, 3);
cursor.prev(&());
assert_eq!(cursor.item(), Some(&2));
assert_eq!(cursor.prev_item(), Some(&1));
+ assert_eq!(cursor.next_item(), Some(&3));
assert_eq!(cursor.start().sum, 1);
cursor.prev(&());
assert_eq!(cursor.item(), Some(&1));
assert_eq!(cursor.prev_item(), None);
+ assert_eq!(cursor.next_item(), Some(&2));
assert_eq!(cursor.start().sum, 0);
cursor.prev(&());
assert_eq!(cursor.item(), None);
assert_eq!(cursor.prev_item(), None);
+ assert_eq!(cursor.next_item(), Some(&1));
assert_eq!(cursor.start().sum, 0);
cursor.next(&());
assert_eq!(cursor.item(), Some(&1));
assert_eq!(cursor.prev_item(), None);
+ assert_eq!(cursor.next_item(), Some(&2));
assert_eq!(cursor.start().sum, 0);
let mut cursor = tree.cursor::<IntegersSummary>();
@@ -1012,6 +1028,7 @@ mod tests {
);
assert_eq!(cursor.item(), None);
assert_eq!(cursor.prev_item(), Some(&6));
+ assert_eq!(cursor.next_item(), None);
assert_eq!(cursor.start().sum, 21);
cursor.seek(&Count(3), Bias::Right, &());
@@ -1023,6 +1040,7 @@ mod tests {
);
assert_eq!(cursor.item(), None);
assert_eq!(cursor.prev_item(), Some(&6));
+ assert_eq!(cursor.next_item(), None);
assert_eq!(cursor.start().sum, 21);
// Seeking can bias left or right
@@ -395,16 +395,17 @@ impl TerminalElement {
// Terminal Emulator controlled behavior:
region = region
// Start selections
- .on_down(
- MouseButton::Left,
- TerminalElement::generic_button_handler(
- connection,
- origin,
- move |terminal, origin, e, _cx| {
- terminal.mouse_down(&e, origin);
- },
- ),
- )
+ .on_down(MouseButton::Left, move |event, v: &mut TerminalView, cx| {
+ cx.focus_parent();
+ v.context_menu.update(cx, |menu, _cx| menu.delay_cancel());
+ if let Some(conn_handle) = connection.upgrade(cx) {
+ conn_handle.update(cx, |terminal, cx| {
+ terminal.mouse_down(&event, origin);
+
+ cx.notify();
+ })
+ }
+ })
// Update drag selections
.on_drag(MouseButton::Left, move |event, _: &mut TerminalView, cx| {
if cx.is_self_focused() {
@@ -87,6 +87,7 @@ impl TerminalPanel {
}
})
},
+ |_, _| {},
None,
))
.with_child(Pane::render_tab_bar_button(
@@ -100,6 +101,7 @@ impl TerminalPanel {
Some(("Toggle Zoom".into(), Some(Box::new(workspace::ToggleZoom)))),
cx,
move |pane, cx| pane.toggle_zoom(&Default::default(), cx),
+ |_, _| {},
None,
))
.into_any()
@@ -689,6 +689,7 @@ pub struct Editor {
pub line_number_active: Color,
pub guest_selections: Vec<SelectionStyle>,
pub syntax: Arc<SyntaxTheme>,
+ pub hint: HighlightStyle,
pub suggestion: HighlightStyle,
pub diagnostic_path_header: DiagnosticPathHeader,
pub diagnostic_header: DiagnosticHeader,
@@ -118,14 +118,15 @@ pub fn merge_non_null_json_value_into(source: serde_json::Value, target: &mut se
}
}
-pub trait ResultExt {
+pub trait ResultExt<E> {
type Ok;
fn log_err(self) -> Option<Self::Ok>;
fn warn_on_err(self) -> Option<Self::Ok>;
+ fn inspect_error(self, func: impl FnOnce(&E)) -> Self;
}
-impl<T, E> ResultExt for Result<T, E>
+impl<T, E> ResultExt<E> for Result<T, E>
where
E: std::fmt::Debug,
{
@@ -152,6 +153,15 @@ where
}
}
}
+
+ /// https://doc.rust-lang.org/std/result/enum.Result.html#method.inspect_err
+ fn inspect_error(self, func: impl FnOnce(&E)) -> Self {
+ if let Err(err) = &self {
+ func(err);
+ }
+
+ self
+ }
}
pub trait TryFutureExt {
@@ -273,6 +273,11 @@ impl Pane {
Some(("New...".into(), None)),
cx,
|pane, cx| pane.deploy_new_menu(cx),
+ |pane, cx| {
+ pane.tab_bar_context_menu
+ .handle
+ .update(cx, |menu, _| menu.delay_cancel())
+ },
pane.tab_bar_context_menu
.handle_if_kind(TabBarContextMenuKind::New),
))
@@ -283,6 +288,11 @@ impl Pane {
Some(("Split Pane".into(), None)),
cx,
|pane, cx| pane.deploy_split_menu(cx),
+ |pane, cx| {
+ pane.tab_bar_context_menu
+ .handle
+ .update(cx, |menu, _| menu.delay_cancel())
+ },
pane.tab_bar_context_menu
.handle_if_kind(TabBarContextMenuKind::Split),
))
@@ -304,6 +314,7 @@ impl Pane {
Some((tooltip_label, Some(Box::new(ToggleZoom)))),
cx,
move |pane, cx| pane.toggle_zoom(&Default::default(), cx),
+ move |_, _| {},
None,
)
})
@@ -988,7 +999,7 @@ impl Pane {
fn deploy_split_menu(&mut self, cx: &mut ViewContext<Self>) {
self.tab_bar_context_menu.handle.update(cx, |menu, cx| {
- menu.show(
+ menu.toggle(
Default::default(),
AnchorCorner::TopRight,
vec![
@@ -1006,7 +1017,7 @@ impl Pane {
fn deploy_new_menu(&mut self, cx: &mut ViewContext<Self>) {
self.tab_bar_context_menu.handle.update(cx, |menu, cx| {
- menu.show(
+ menu.toggle(
Default::default(),
AnchorCorner::TopRight,
vec![
@@ -1416,13 +1427,17 @@ impl Pane {
.into_any()
}
- pub fn render_tab_bar_button<F: 'static + Fn(&mut Pane, &mut EventContext<Pane>)>(
+ pub fn render_tab_bar_button<
+ F1: 'static + Fn(&mut Pane, &mut EventContext<Pane>),
+ F2: 'static + Fn(&mut Pane, &mut EventContext<Pane>),
+ >(
index: usize,
icon: &'static str,
is_active: bool,
tooltip: Option<(String, Option<Box<dyn Action>>)>,
cx: &mut ViewContext<Pane>,
- on_click: F,
+ on_click: F1,
+ on_down: F2,
context_menu: Option<ViewHandle<ContextMenu>>,
) -> AnyElement<Pane> {
enum TabBarButton {}
@@ -1440,6 +1455,7 @@ impl Pane {
.with_height(style.button_width)
})
.with_cursor_style(CursorStyle::PointingHand)
+ .on_down(MouseButton::Left, move |_, pane, cx| on_down(pane, cx))
.on_click(MouseButton::Left, move |_, pane, cx| on_click(pane, cx))
.into_any();
if let Some((tooltip, action)) = tooltip {
@@ -2,6 +2,7 @@ use anyhow::{anyhow, Context, Result};
use async_trait::async_trait;
use futures::StreamExt;
pub use language::*;
+use lsp::LanguageServerBinary;
use smol::fs::{self, File};
use std::{any::Any, path::PathBuf, sync::Arc};
use util::{
@@ -86,31 +87,19 @@ impl super::LspAdapter for CLspAdapter {
container_dir: PathBuf,
_: &dyn LspAdapterDelegate,
) -> Option<LanguageServerBinary> {
- (|| async move {
- let mut last_clangd_dir = None;
- let mut entries = fs::read_dir(&container_dir).await?;
- while let Some(entry) = entries.next().await {
- let entry = entry?;
- if entry.file_type().await?.is_dir() {
- last_clangd_dir = Some(entry.path());
- }
- }
- let clangd_dir = last_clangd_dir.ok_or_else(|| anyhow!("no cached binary"))?;
- let clangd_bin = clangd_dir.join("bin/clangd");
- if clangd_bin.exists() {
- Ok(LanguageServerBinary {
- path: clangd_bin,
- arguments: vec![],
- })
- } else {
- Err(anyhow!(
- "missing clangd binary in directory {:?}",
- clangd_dir
- ))
- }
- })()
- .await
- .log_err()
+ get_cached_server_binary(container_dir).await
+ }
+
+ async fn installation_test_binary(
+ &self,
+ container_dir: PathBuf,
+ ) -> Option<LanguageServerBinary> {
+ get_cached_server_binary(container_dir)
+ .await
+ .map(|mut binary| {
+ binary.arguments = vec!["--help".into()];
+ binary
+ })
}
async fn label_for_completion(
@@ -250,6 +239,34 @@ impl super::LspAdapter for CLspAdapter {
}
}
+async fn get_cached_server_binary(container_dir: PathBuf) -> Option<LanguageServerBinary> {
+ (|| async move {
+ let mut last_clangd_dir = None;
+ let mut entries = fs::read_dir(&container_dir).await?;
+ while let Some(entry) = entries.next().await {
+ let entry = entry?;
+ if entry.file_type().await?.is_dir() {
+ last_clangd_dir = Some(entry.path());
+ }
+ }
+ let clangd_dir = last_clangd_dir.ok_or_else(|| anyhow!("no cached binary"))?;
+ let clangd_bin = clangd_dir.join("bin/clangd");
+ if clangd_bin.exists() {
+ Ok(LanguageServerBinary {
+ path: clangd_bin,
+ arguments: vec![],
+ })
+ } else {
+ Err(anyhow!(
+ "missing clangd binary in directory {:?}",
+ clangd_dir
+ ))
+ }
+ })()
+ .await
+ .log_err()
+}
+
#[cfg(test)]
mod tests {
use gpui::TestAppContext;
@@ -3,7 +3,7 @@ use async_trait::async_trait;
use futures::StreamExt;
use gpui::{AsyncAppContext, Task};
pub use language::*;
-use lsp::{CompletionItemKind, SymbolKind};
+use lsp::{CompletionItemKind, LanguageServerBinary, SymbolKind};
use smol::fs::{self, File};
use std::{
any::Any,
@@ -140,20 +140,14 @@ impl LspAdapter for ElixirLspAdapter {
container_dir: PathBuf,
_: &dyn LspAdapterDelegate,
) -> Option<LanguageServerBinary> {
- (|| async move {
- let mut last = None;
- let mut entries = fs::read_dir(&container_dir).await?;
- while let Some(entry) = entries.next().await {
- last = Some(entry?.path());
- }
- last.map(|path| LanguageServerBinary {
- path,
- arguments: vec![],
- })
- .ok_or_else(|| anyhow!("no cached binary"))
- })()
- .await
- .log_err()
+ get_cached_server_binary(container_dir).await
+ }
+
+ async fn installation_test_binary(
+ &self,
+ container_dir: PathBuf,
+ ) -> Option<LanguageServerBinary> {
+ get_cached_server_binary(container_dir).await
}
async fn label_for_completion(
@@ -239,3 +233,20 @@ impl LspAdapter for ElixirLspAdapter {
})
}
}
+
+async fn get_cached_server_binary(container_dir: PathBuf) -> Option<LanguageServerBinary> {
+ (|| async move {
+ let mut last = None;
+ let mut entries = fs::read_dir(&container_dir).await?;
+ while let Some(entry) = entries.next().await {
+ last = Some(entry?.path());
+ }
+ last.map(|path| LanguageServerBinary {
+ path,
+ arguments: vec![],
+ })
+ .ok_or_else(|| anyhow!("no cached binary"))
+ })()
+ .await
+ .log_err()
+}
@@ -4,6 +4,7 @@ use futures::StreamExt;
use gpui::{AsyncAppContext, Task};
pub use language::*;
use lazy_static::lazy_static;
+use lsp::LanguageServerBinary;
use regex::Regex;
use smol::{fs, process};
use std::{
@@ -148,32 +149,19 @@ impl super::LspAdapter for GoLspAdapter {
container_dir: PathBuf,
_: &dyn LspAdapterDelegate,
) -> Option<LanguageServerBinary> {
- (|| async move {
- let mut last_binary_path = None;
- let mut entries = fs::read_dir(&container_dir).await?;
- while let Some(entry) = entries.next().await {
- let entry = entry?;
- if entry.file_type().await?.is_file()
- && entry
- .file_name()
- .to_str()
- .map_or(false, |name| name.starts_with("gopls_"))
- {
- last_binary_path = Some(entry.path());
- }
- }
+ get_cached_server_binary(container_dir).await
+ }
- if let Some(path) = last_binary_path {
- Ok(LanguageServerBinary {
- path,
- arguments: server_binary_arguments(),
- })
- } else {
- Err(anyhow!("no cached binary"))
- }
- })()
- .await
- .log_err()
+ async fn installation_test_binary(
+ &self,
+ container_dir: PathBuf,
+ ) -> Option<LanguageServerBinary> {
+ get_cached_server_binary(container_dir)
+ .await
+ .map(|mut binary| {
+ binary.arguments = vec!["--help".into()];
+ binary
+ })
}
async fn label_for_completion(
@@ -336,6 +324,35 @@ impl super::LspAdapter for GoLspAdapter {
}
}
+async fn get_cached_server_binary(container_dir: PathBuf) -> Option<LanguageServerBinary> {
+ (|| async move {
+ let mut last_binary_path = None;
+ let mut entries = fs::read_dir(&container_dir).await?;
+ while let Some(entry) = entries.next().await {
+ let entry = entry?;
+ if entry.file_type().await?.is_file()
+ && entry
+ .file_name()
+ .to_str()
+ .map_or(false, |name| name.starts_with("gopls_"))
+ {
+ last_binary_path = Some(entry.path());
+ }
+ }
+
+ if let Some(path) = last_binary_path {
+ Ok(LanguageServerBinary {
+ path,
+ arguments: server_binary_arguments(),
+ })
+ } else {
+ Err(anyhow!("no cached binary"))
+ }
+ })()
+ .await
+ .log_err()
+}
+
fn adjust_runs(
delta: usize,
mut runs: Vec<(Range<usize>, HighlightId)>,
@@ -1,7 +1,8 @@
use anyhow::{anyhow, Result};
use async_trait::async_trait;
use futures::StreamExt;
-use language::{LanguageServerBinary, LanguageServerName, LspAdapter, LspAdapterDelegate};
+use language::{LanguageServerName, LspAdapter, LspAdapterDelegate};
+use lsp::LanguageServerBinary;
use node_runtime::NodeRuntime;
use serde_json::json;
use smol::fs;
@@ -13,6 +14,9 @@ use std::{
};
use util::ResultExt;
+const SERVER_PATH: &'static str =
+ "node_modules/vscode-langservers-extracted/bin/vscode-html-language-server";
+
fn server_binary_arguments(server_path: &Path) -> Vec<OsString> {
vec![server_path.into(), "--stdio".into()]
}
@@ -22,9 +26,6 @@ pub struct HtmlLspAdapter {
}
impl HtmlLspAdapter {
- const SERVER_PATH: &'static str =
- "node_modules/vscode-langservers-extracted/bin/vscode-html-language-server";
-
pub fn new(node: Arc<NodeRuntime>) -> Self {
HtmlLspAdapter { node }
}
@@ -54,7 +55,7 @@ impl LspAdapter for HtmlLspAdapter {
_: &dyn LspAdapterDelegate,
) -> Result<LanguageServerBinary> {
let version = version.downcast::<String>().unwrap();
- let server_path = container_dir.join(Self::SERVER_PATH);
+ let server_path = container_dir.join(SERVER_PATH);
if fs::metadata(&server_path).await.is_err() {
self.node
@@ -76,31 +77,14 @@ impl LspAdapter for HtmlLspAdapter {
container_dir: PathBuf,
_: &dyn LspAdapterDelegate,
) -> Option<LanguageServerBinary> {
- (|| async move {
- let mut last_version_dir = None;
- let mut entries = fs::read_dir(&container_dir).await?;
- while let Some(entry) = entries.next().await {
- let entry = entry?;
- if entry.file_type().await?.is_dir() {
- last_version_dir = Some(entry.path());
- }
- }
- let last_version_dir = last_version_dir.ok_or_else(|| anyhow!("no cached binary"))?;
- let server_path = last_version_dir.join(Self::SERVER_PATH);
- if server_path.exists() {
- Ok(LanguageServerBinary {
- path: self.node.binary_path().await?,
- arguments: server_binary_arguments(&server_path),
- })
- } else {
- Err(anyhow!(
- "missing executable in directory {:?}",
- last_version_dir
- ))
- }
- })()
- .await
- .log_err()
+ get_cached_server_binary(container_dir, &self.node).await
+ }
+
+ async fn installation_test_binary(
+ &self,
+ container_dir: PathBuf,
+ ) -> Option<LanguageServerBinary> {
+ get_cached_server_binary(container_dir, &self.node).await
}
async fn initialization_options(&self) -> Option<serde_json::Value> {
@@ -109,3 +93,34 @@ impl LspAdapter for HtmlLspAdapter {
}))
}
}
+
+async fn get_cached_server_binary(
+ container_dir: PathBuf,
+ node: &NodeRuntime,
+) -> Option<LanguageServerBinary> {
+ (|| async move {
+ let mut last_version_dir = None;
+ let mut entries = fs::read_dir(&container_dir).await?;
+ while let Some(entry) = entries.next().await {
+ let entry = entry?;
+ if entry.file_type().await?.is_dir() {
+ last_version_dir = Some(entry.path());
+ }
+ }
+ let last_version_dir = last_version_dir.ok_or_else(|| anyhow!("no cached binary"))?;
+ let server_path = last_version_dir.join(SERVER_PATH);
+ if server_path.exists() {
+ Ok(LanguageServerBinary {
+ path: node.binary_path().await?,
+ arguments: server_binary_arguments(&server_path),
+ })
+ } else {
+ Err(anyhow!(
+ "missing executable in directory {:?}",
+ last_version_dir
+ ))
+ }
+ })()
+ .await
+ .log_err()
+}
@@ -3,9 +3,8 @@ use async_trait::async_trait;
use collections::HashMap;
use futures::{future::BoxFuture, FutureExt, StreamExt};
use gpui::AppContext;
-use language::{
- LanguageRegistry, LanguageServerBinary, LanguageServerName, LspAdapter, LspAdapterDelegate,
-};
+use language::{LanguageRegistry, LanguageServerName, LspAdapter, LspAdapterDelegate};
+use lsp::LanguageServerBinary;
use node_runtime::NodeRuntime;
use serde_json::json;
use settings::{KeymapFile, SettingsJsonSchemaParams, SettingsStore};
@@ -84,32 +83,14 @@ impl LspAdapter for JsonLspAdapter {
container_dir: PathBuf,
_: &dyn LspAdapterDelegate,
) -> Option<LanguageServerBinary> {
- (|| async move {
- let mut last_version_dir = None;
- let mut entries = fs::read_dir(&container_dir).await?;
- while let Some(entry) = entries.next().await {
- let entry = entry?;
- if entry.file_type().await?.is_dir() {
- last_version_dir = Some(entry.path());
- }
- }
+ get_cached_server_binary(container_dir, &self.node).await
+ }
- let last_version_dir = last_version_dir.ok_or_else(|| anyhow!("no cached binary"))?;
- let server_path = last_version_dir.join(SERVER_PATH);
- if server_path.exists() {
- Ok(LanguageServerBinary {
- path: self.node.binary_path().await?,
- arguments: server_binary_arguments(&server_path),
- })
- } else {
- Err(anyhow!(
- "missing executable in directory {:?}",
- last_version_dir
- ))
- }
- })()
- .await
- .log_err()
+ async fn installation_test_binary(
+ &self,
+ container_dir: PathBuf,
+ ) -> Option<LanguageServerBinary> {
+ get_cached_server_binary(container_dir, &self.node).await
}
async fn initialization_options(&self) -> Option<serde_json::Value> {
@@ -162,6 +143,38 @@ impl LspAdapter for JsonLspAdapter {
}
}
+async fn get_cached_server_binary(
+ container_dir: PathBuf,
+ node: &NodeRuntime,
+) -> Option<LanguageServerBinary> {
+ (|| async move {
+ let mut last_version_dir = None;
+ let mut entries = fs::read_dir(&container_dir).await?;
+ while let Some(entry) = entries.next().await {
+ let entry = entry?;
+ if entry.file_type().await?.is_dir() {
+ last_version_dir = Some(entry.path());
+ }
+ }
+
+ let last_version_dir = last_version_dir.ok_or_else(|| anyhow!("no cached binary"))?;
+ let server_path = last_version_dir.join(SERVER_PATH);
+ if server_path.exists() {
+ Ok(LanguageServerBinary {
+ path: node.binary_path().await?,
+ arguments: server_binary_arguments(&server_path),
+ })
+ } else {
+ Err(anyhow!(
+ "missing executable in directory {:?}",
+ last_version_dir
+ ))
+ }
+ })()
+ .await
+ .log_err()
+}
+
fn schema_file_match(path: &Path) -> &Path {
path.strip_prefix(path.parent().unwrap().parent().unwrap())
.unwrap()
@@ -3,7 +3,8 @@ use async_trait::async_trait;
use collections::HashMap;
use futures::lock::Mutex;
use gpui::executor::Background;
-use language::{LanguageServerBinary, LanguageServerName, LspAdapter, LspAdapterDelegate};
+use language::{LanguageServerName, LspAdapter, LspAdapterDelegate};
+use lsp::LanguageServerBinary;
use plugin_runtime::{Plugin, PluginBinary, PluginBuilder, WasiFn};
use std::{any::Any, path::PathBuf, sync::Arc};
use util::ResultExt;
@@ -129,6 +130,14 @@ impl LspAdapter for PluginLspAdapter {
.await
}
+ fn can_be_reinstalled(&self) -> bool {
+ false
+ }
+
+ async fn installation_test_binary(&self, _: PathBuf) -> Option<LanguageServerBinary> {
+ None
+ }
+
async fn initialization_options(&self) -> Option<serde_json::Value> {
let string: String = self
.runtime
@@ -3,7 +3,8 @@ use async_compression::futures::bufread::GzipDecoder;
use async_tar::Archive;
use async_trait::async_trait;
use futures::{io::BufReader, StreamExt};
-use language::{LanguageServerBinary, LanguageServerName, LspAdapterDelegate};
+use language::{LanguageServerName, LspAdapterDelegate};
+use lsp::LanguageServerBinary;
use smol::fs;
use std::{any::Any, env::consts, ffi::OsString, path::PathBuf};
use util::{
@@ -91,31 +92,47 @@ impl super::LspAdapter for LuaLspAdapter {
container_dir: PathBuf,
_: &dyn LspAdapterDelegate,
) -> Option<LanguageServerBinary> {
- async_iife!({
- let mut last_binary_path = None;
- let mut entries = fs::read_dir(&container_dir).await?;
- while let Some(entry) = entries.next().await {
- let entry = entry?;
- if entry.file_type().await?.is_file()
- && entry
- .file_name()
- .to_str()
- .map_or(false, |name| name == "lua-language-server")
- {
- last_binary_path = Some(entry.path());
- }
- }
+ get_cached_server_binary(container_dir).await
+ }
- if let Some(path) = last_binary_path {
- Ok(LanguageServerBinary {
- path,
- arguments: server_binary_arguments(),
- })
- } else {
- Err(anyhow!("no cached binary"))
- }
- })
- .await
- .log_err()
+ async fn installation_test_binary(
+ &self,
+ container_dir: PathBuf,
+ ) -> Option<LanguageServerBinary> {
+ get_cached_server_binary(container_dir)
+ .await
+ .map(|mut binary| {
+ binary.arguments = vec!["--version".into()];
+ binary
+ })
}
}
+
+async fn get_cached_server_binary(container_dir: PathBuf) -> Option<LanguageServerBinary> {
+ async_iife!({
+ let mut last_binary_path = None;
+ let mut entries = fs::read_dir(&container_dir).await?;
+ while let Some(entry) = entries.next().await {
+ let entry = entry?;
+ if entry.file_type().await?.is_file()
+ && entry
+ .file_name()
+ .to_str()
+ .map_or(false, |name| name == "lua-language-server")
+ {
+ last_binary_path = Some(entry.path());
+ }
+ }
+
+ if let Some(path) = last_binary_path {
+ Ok(LanguageServerBinary {
+ path,
+ arguments: server_binary_arguments(),
+ })
+ } else {
+ Err(anyhow!("no cached binary"))
+ }
+ })
+ .await
+ .log_err()
+}
@@ -1,7 +1,8 @@
use anyhow::{anyhow, Result};
use async_trait::async_trait;
use futures::StreamExt;
-use language::{LanguageServerBinary, LanguageServerName, LspAdapter, LspAdapterDelegate};
+use language::{LanguageServerName, LspAdapter, LspAdapterDelegate};
+use lsp::LanguageServerBinary;
use node_runtime::NodeRuntime;
use smol::fs;
use std::{
@@ -12,6 +13,8 @@ use std::{
};
use util::ResultExt;
+const SERVER_PATH: &'static str = "node_modules/pyright/langserver.index.js";
+
fn server_binary_arguments(server_path: &Path) -> Vec<OsString> {
vec![server_path.into(), "--stdio".into()]
}
@@ -21,8 +24,6 @@ pub struct PythonLspAdapter {
}
impl PythonLspAdapter {
- const SERVER_PATH: &'static str = "node_modules/pyright/langserver.index.js";
-
pub fn new(node: Arc<NodeRuntime>) -> Self {
PythonLspAdapter { node }
}
@@ -48,7 +49,7 @@ impl LspAdapter for PythonLspAdapter {
_: &dyn LspAdapterDelegate,
) -> Result<LanguageServerBinary> {
let version = version.downcast::<String>().unwrap();
- let server_path = container_dir.join(Self::SERVER_PATH);
+ let server_path = container_dir.join(SERVER_PATH);
if fs::metadata(&server_path).await.is_err() {
self.node
@@ -67,31 +68,14 @@ impl LspAdapter for PythonLspAdapter {
container_dir: PathBuf,
_: &dyn LspAdapterDelegate,
) -> Option<LanguageServerBinary> {
- (|| async move {
- let mut last_version_dir = None;
- let mut entries = fs::read_dir(&container_dir).await?;
- while let Some(entry) = entries.next().await {
- let entry = entry?;
- if entry.file_type().await?.is_dir() {
- last_version_dir = Some(entry.path());
- }
- }
- let last_version_dir = last_version_dir.ok_or_else(|| anyhow!("no cached binary"))?;
- let server_path = last_version_dir.join(Self::SERVER_PATH);
- if server_path.exists() {
- Ok(LanguageServerBinary {
- path: self.node.binary_path().await?,
- arguments: server_binary_arguments(&server_path),
- })
- } else {
- Err(anyhow!(
- "missing executable in directory {:?}",
- last_version_dir
- ))
- }
- })()
- .await
- .log_err()
+ get_cached_server_binary(container_dir, &self.node).await
+ }
+
+ async fn installation_test_binary(
+ &self,
+ container_dir: PathBuf,
+ ) -> Option<LanguageServerBinary> {
+ get_cached_server_binary(container_dir, &self.node).await
}
async fn process_completion(&self, item: &mut lsp::CompletionItem) {
@@ -170,6 +154,37 @@ impl LspAdapter for PythonLspAdapter {
}
}
+async fn get_cached_server_binary(
+ container_dir: PathBuf,
+ node: &NodeRuntime,
+) -> Option<LanguageServerBinary> {
+ (|| async move {
+ let mut last_version_dir = None;
+ let mut entries = fs::read_dir(&container_dir).await?;
+ while let Some(entry) = entries.next().await {
+ let entry = entry?;
+ if entry.file_type().await?.is_dir() {
+ last_version_dir = Some(entry.path());
+ }
+ }
+ let last_version_dir = last_version_dir.ok_or_else(|| anyhow!("no cached binary"))?;
+ let server_path = last_version_dir.join(SERVER_PATH);
+ if server_path.exists() {
+ Ok(LanguageServerBinary {
+ path: node.binary_path().await?,
+ arguments: server_binary_arguments(&server_path),
+ })
+ } else {
+ Err(anyhow!(
+ "missing executable in directory {:?}",
+ last_version_dir
+ ))
+ }
+ })()
+ .await
+ .log_err()
+}
+
#[cfg(test)]
mod tests {
use gpui::{ModelContext, TestAppContext};
@@ -1,6 +1,7 @@
use anyhow::{anyhow, Result};
use async_trait::async_trait;
-use language::{LanguageServerBinary, LanguageServerName, LspAdapter, LspAdapterDelegate};
+use language::{LanguageServerName, LspAdapter, LspAdapterDelegate};
+use lsp::LanguageServerBinary;
use std::{any::Any, path::PathBuf, sync::Arc};
pub struct RubyLanguageServer;
@@ -38,6 +39,14 @@ impl LspAdapter for RubyLanguageServer {
})
}
+ fn can_be_reinstalled(&self) -> bool {
+ false
+ }
+
+ async fn installation_test_binary(&self, _: PathBuf) -> Option<LanguageServerBinary> {
+ None
+ }
+
async fn label_for_completion(
&self,
item: &lsp::CompletionItem,
@@ -4,6 +4,7 @@ use async_trait::async_trait;
use futures::{io::BufReader, StreamExt};
pub use language::*;
use lazy_static::lazy_static;
+use lsp::LanguageServerBinary;
use regex::Regex;
use smol::fs::{self, File};
use std::{any::Any, borrow::Cow, env::consts, path::PathBuf, str, sync::Arc};
@@ -78,20 +79,19 @@ impl LspAdapter for RustLspAdapter {
container_dir: PathBuf,
_: &dyn LspAdapterDelegate,
) -> Option<LanguageServerBinary> {
- (|| async move {
- let mut last = None;
- let mut entries = fs::read_dir(&container_dir).await?;
- while let Some(entry) = entries.next().await {
- last = Some(entry?.path());
- }
+ get_cached_server_binary(container_dir).await
+ }
- anyhow::Ok(LanguageServerBinary {
- path: last.ok_or_else(|| anyhow!("no cached binary"))?,
- arguments: Default::default(),
+ async fn installation_test_binary(
+ &self,
+ container_dir: PathBuf,
+ ) -> Option<LanguageServerBinary> {
+ get_cached_server_binary(container_dir)
+ .await
+ .map(|mut binary| {
+ binary.arguments = vec!["--help".into()];
+ binary
})
- })()
- .await
- .log_err()
}
async fn disk_based_diagnostic_sources(&self) -> Vec<String> {
@@ -258,6 +258,22 @@ impl LspAdapter for RustLspAdapter {
})
}
}
+async fn get_cached_server_binary(container_dir: PathBuf) -> Option<LanguageServerBinary> {
+ (|| async move {
+ let mut last = None;
+ let mut entries = fs::read_dir(&container_dir).await?;
+ while let Some(entry) = entries.next().await {
+ last = Some(entry?.path());
+ }
+
+ anyhow::Ok(LanguageServerBinary {
+ path: last.ok_or_else(|| anyhow!("no cached binary"))?,
+ arguments: Default::default(),
+ })
+ })()
+ .await
+ .log_err()
+}
#[cfg(test)]
mod tests {
@@ -4,8 +4,8 @@ use async_tar::Archive;
use async_trait::async_trait;
use futures::{future::BoxFuture, FutureExt};
use gpui::AppContext;
-use language::{LanguageServerBinary, LanguageServerName, LspAdapter, LspAdapterDelegate};
-use lsp::CodeActionKind;
+use language::{LanguageServerName, LspAdapter, LspAdapterDelegate};
+use lsp::{CodeActionKind, LanguageServerBinary};
use node_runtime::NodeRuntime;
use serde_json::{json, Value};
use smol::{fs, io::BufReader, stream::StreamExt};
@@ -104,28 +104,14 @@ impl LspAdapter for TypeScriptLspAdapter {
container_dir: PathBuf,
_: &dyn LspAdapterDelegate,
) -> Option<LanguageServerBinary> {
- (|| async move {
- let old_server_path = container_dir.join(Self::OLD_SERVER_PATH);
- let new_server_path = container_dir.join(Self::NEW_SERVER_PATH);
- if new_server_path.exists() {
- Ok(LanguageServerBinary {
- path: self.node.binary_path().await?,
- arguments: typescript_server_binary_arguments(&new_server_path),
- })
- } else if old_server_path.exists() {
- Ok(LanguageServerBinary {
- path: self.node.binary_path().await?,
- arguments: typescript_server_binary_arguments(&old_server_path),
- })
- } else {
- Err(anyhow!(
- "missing executable in directory {:?}",
- container_dir
- ))
- }
- })()
- .await
- .log_err()
+ get_cached_ts_server_binary(container_dir, &self.node).await
+ }
+
+ async fn installation_test_binary(
+ &self,
+ container_dir: PathBuf,
+ ) -> Option<LanguageServerBinary> {
+ get_cached_ts_server_binary(container_dir, &self.node).await
}
fn code_action_kinds(&self) -> Option<Vec<CodeActionKind>> {
@@ -173,6 +159,34 @@ impl LspAdapter for TypeScriptLspAdapter {
}
}
+async fn get_cached_ts_server_binary(
+ container_dir: PathBuf,
+ node: &NodeRuntime,
+) -> Option<LanguageServerBinary> {
+ (|| async move {
+ let old_server_path = container_dir.join(TypeScriptLspAdapter::OLD_SERVER_PATH);
+ let new_server_path = container_dir.join(TypeScriptLspAdapter::NEW_SERVER_PATH);
+ if new_server_path.exists() {
+ Ok(LanguageServerBinary {
+ path: node.binary_path().await?,
+ arguments: typescript_server_binary_arguments(&new_server_path),
+ })
+ } else if old_server_path.exists() {
+ Ok(LanguageServerBinary {
+ path: node.binary_path().await?,
+ arguments: typescript_server_binary_arguments(&old_server_path),
+ })
+ } else {
+ Err(anyhow!(
+ "missing executable in directory {:?}",
+ container_dir
+ ))
+ }
+ })()
+ .await
+ .log_err()
+}
+
pub struct EsLintLspAdapter {
node: Arc<NodeRuntime>,
}
@@ -249,11 +263,11 @@ impl LspAdapter for EsLintLspAdapter {
fs::rename(first.path(), &repo_root).await?;
self.node
- .run_npm_subcommand(&repo_root, "install", &[])
+ .run_npm_subcommand(Some(&repo_root), "install", &[])
.await?;
self.node
- .run_npm_subcommand(&repo_root, "run-script", &["compile"])
+ .run_npm_subcommand(Some(&repo_root), "run-script", &["compile"])
.await?;
}
@@ -268,21 +282,14 @@ impl LspAdapter for EsLintLspAdapter {
container_dir: PathBuf,
_: &dyn LspAdapterDelegate,
) -> Option<LanguageServerBinary> {
- (|| async move {
- // This is unfortunate but we don't know what the version is to build a path directly
- let mut dir = fs::read_dir(&container_dir).await?;
- let first = dir.next().await.ok_or(anyhow!("missing first file"))??;
- if !first.file_type().await?.is_dir() {
- return Err(anyhow!("First entry is not a directory"));
- }
+ get_cached_eslint_server_binary(container_dir, &self.node).await
+ }
- Ok(LanguageServerBinary {
- path: first.path().join(Self::SERVER_PATH),
- arguments: Default::default(),
- })
- })()
- .await
- .log_err()
+ async fn installation_test_binary(
+ &self,
+ container_dir: PathBuf,
+ ) -> Option<LanguageServerBinary> {
+ get_cached_eslint_server_binary(container_dir, &self.node).await
}
async fn label_for_completion(
@@ -298,6 +305,28 @@ impl LspAdapter for EsLintLspAdapter {
}
}
+async fn get_cached_eslint_server_binary(
+ container_dir: PathBuf,
+ node: &NodeRuntime,
+) -> Option<LanguageServerBinary> {
+ (|| async move {
+ // This is unfortunate but we don't know what the version is to build a path directly
+ let mut dir = fs::read_dir(&container_dir).await?;
+ let first = dir.next().await.ok_or(anyhow!("missing first file"))??;
+ if !first.file_type().await?.is_dir() {
+ return Err(anyhow!("First entry is not a directory"));
+ }
+ let server_path = first.path().join(EsLintLspAdapter::SERVER_PATH);
+
+ Ok(LanguageServerBinary {
+ path: node.binary_path().await?,
+ arguments: eslint_server_binary_arguments(&server_path),
+ })
+ })()
+ .await
+ .log_err()
+}
+
#[cfg(test)]
mod tests {
use gpui::TestAppContext;
@@ -3,9 +3,9 @@ use async_trait::async_trait;
use futures::{future::BoxFuture, FutureExt, StreamExt};
use gpui::AppContext;
use language::{
- language_settings::all_language_settings, LanguageServerBinary, LanguageServerName, LspAdapter,
- LspAdapterDelegate,
+ language_settings::all_language_settings, LanguageServerName, LspAdapter, LspAdapterDelegate,
};
+use lsp::LanguageServerBinary;
use node_runtime::NodeRuntime;
use serde_json::Value;
use smol::fs;
@@ -18,6 +18,8 @@ use std::{
};
use util::ResultExt;
+const SERVER_PATH: &'static str = "node_modules/yaml-language-server/bin/yaml-language-server";
+
fn server_binary_arguments(server_path: &Path) -> Vec<OsString> {
vec![server_path.into(), "--stdio".into()]
}
@@ -27,8 +29,6 @@ pub struct YamlLspAdapter {
}
impl YamlLspAdapter {
- const SERVER_PATH: &'static str = "node_modules/yaml-language-server/bin/yaml-language-server";
-
pub fn new(node: Arc<NodeRuntime>) -> Self {
YamlLspAdapter { node }
}
@@ -58,7 +58,7 @@ impl LspAdapter for YamlLspAdapter {
_: &dyn LspAdapterDelegate,
) -> Result<LanguageServerBinary> {
let version = version.downcast::<String>().unwrap();
- let server_path = container_dir.join(Self::SERVER_PATH);
+ let server_path = container_dir.join(SERVER_PATH);
if fs::metadata(&server_path).await.is_err() {
self.node
@@ -77,33 +77,15 @@ impl LspAdapter for YamlLspAdapter {
container_dir: PathBuf,
_: &dyn LspAdapterDelegate,
) -> Option<LanguageServerBinary> {
- (|| async move {
- let mut last_version_dir = None;
- let mut entries = fs::read_dir(&container_dir).await?;
- while let Some(entry) = entries.next().await {
- let entry = entry?;
- if entry.file_type().await?.is_dir() {
- last_version_dir = Some(entry.path());
- }
- }
- let last_version_dir = last_version_dir.ok_or_else(|| anyhow!("no cached binary"))?;
- let server_path = last_version_dir.join(Self::SERVER_PATH);
- if server_path.exists() {
- Ok(LanguageServerBinary {
- path: self.node.binary_path().await?,
- arguments: server_binary_arguments(&server_path),
- })
- } else {
- Err(anyhow!(
- "missing executable in directory {:?}",
- last_version_dir
- ))
- }
- })()
- .await
- .log_err()
+ get_cached_server_binary(container_dir, &self.node).await
}
+ async fn installation_test_binary(
+ &self,
+ container_dir: PathBuf,
+ ) -> Option<LanguageServerBinary> {
+ get_cached_server_binary(container_dir, &self.node).await
+ }
fn workspace_configuration(&self, cx: &mut AppContext) -> Option<BoxFuture<'static, Value>> {
let tab_size = all_language_settings(None, cx)
.language(Some("YAML"))
@@ -121,3 +103,34 @@ impl LspAdapter for YamlLspAdapter {
)
}
}
+
+async fn get_cached_server_binary(
+ container_dir: PathBuf,
+ node: &NodeRuntime,
+) -> Option<LanguageServerBinary> {
+ (|| async move {
+ let mut last_version_dir = None;
+ let mut entries = fs::read_dir(&container_dir).await?;
+ while let Some(entry) = entries.next().await {
+ let entry = entry?;
+ if entry.file_type().await?.is_dir() {
+ last_version_dir = Some(entry.path());
+ }
+ }
+ let last_version_dir = last_version_dir.ok_or_else(|| anyhow!("no cached binary"))?;
+ let server_path = last_version_dir.join(SERVER_PATH);
+ if server_path.exists() {
+ Ok(LanguageServerBinary {
+ path: node.binary_path().await?,
+ arguments: server_binary_arguments(&server_path),
+ })
+ } else {
+ Err(anyhow!(
+ "missing executable in directory {:?}",
+ last_version_dir
+ ))
+ }
+ })()
+ .await
+ .log_err()
+}
@@ -131,7 +131,7 @@ fn main() {
languages.set_executor(cx.background().clone());
languages.set_language_server_download_dir(paths::LANGUAGES_DIR.clone());
let languages = Arc::new(languages);
- let node_runtime = NodeRuntime::new(http.clone(), cx.background().to_owned());
+ let node_runtime = NodeRuntime::instance(http.clone(), cx.background().to_owned());
languages::init(languages.clone(), node_runtime.clone());
let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http.clone(), cx));
@@ -2144,7 +2144,7 @@ mod tests {
languages.set_executor(cx.background().clone());
let languages = Arc::new(languages);
let http = FakeHttpClient::with_404_response();
- let node_runtime = NodeRuntime::new(http, cx.background().to_owned());
+ let node_runtime = NodeRuntime::instance(http, cx.background().to_owned());
languages::init(languages.clone(), node_runtime);
for name in languages.language_names() {
languages.language_for_name(&name);
@@ -0,0 +1,33 @@
+module.exports = {
+ env: {
+ node: true,
+ },
+ extends: [
+ "eslint:recommended",
+ "plugin:@typescript-eslint/recommended",
+ "plugin:import/typescript",
+ ],
+ parser: "@typescript-eslint/parser",
+ parserOptions: {
+ ecmaVersion: "latest",
+ sourceType: "module",
+ },
+ plugins: ["@typescript-eslint", "import"],
+ globals: {
+ module: true,
+ },
+ settings: {
+ "import/parsers": {
+ "@typescript-eslint/parser": [".ts"],
+ },
+ "import/resolver": {
+ typescript: true,
+ node: true,
+ },
+ "import/extensions": [".ts"],
+ },
+ rules: {
+ "linebreak-style": ["error", "unix"],
+ semi: ["error", "never"],
+ },
+}
@@ -0,0 +1,6 @@
+{
+ "semi": false,
+ "printWidth": 80,
+ "htmlWhitespaceSensitivity": "strict",
+ "tabWidth": 4
+}
@@ -12,27 +12,36 @@
"@tokens-studio/types": "^0.2.3",
"@types/chroma-js": "^2.4.0",
"@types/node": "^18.14.1",
+ "@typescript-eslint/eslint-plugin": "^5.60.1",
+ "@typescript-eslint/parser": "^5.60.1",
+ "@vitest/coverage-v8": "^0.32.0",
"ayu": "^8.0.1",
- "bezier-easing": "^2.1.0",
- "case-anything": "^2.1.10",
"chroma-js": "^2.4.2",
"deepmerge": "^4.3.0",
+ "eslint": "^8.43.0",
+ "eslint-import-resolver-typescript": "^3.5.5",
+ "eslint-plugin-import": "^2.27.5",
"json-schema-to-typescript": "^13.0.2",
"toml": "^3.0.0",
"ts-deepmerge": "^6.0.3",
"ts-node": "^10.9.1",
+ "typescript": "^5.1.5",
"utility-types": "^3.10.0",
"vitest": "^0.32.0"
- },
- "devDependencies": {
- "@vitest/coverage-v8": "^0.32.0"
+ }
+ },
+ "node_modules/@aashutoshrathi/word-wrap": {
+ "version": "1.2.6",
+ "resolved": "https://registry.npmjs.org/@aashutoshrathi/word-wrap/-/word-wrap-1.2.6.tgz",
+ "integrity": "sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA==",
+ "engines": {
+ "node": ">=0.10.0"
}
},
"node_modules/@ampproject/remapping": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.2.1.tgz",
"integrity": "sha512-lFMjJTrFL3j7L9yBxwYfCq2k6qqwHyzuUl/XBnif78PWTJYyL/dfowQHWE3sp6U6ZzqWiiIZnpTMO96zhkjwtg==",
- "dev": true,
"dependencies": {
"@jridgewell/gen-mapping": "^0.3.0",
"@jridgewell/trace-mapping": "^0.3.9"
@@ -61,8 +70,7 @@
"node_modules/@bcoe/v8-coverage": {
"version": "0.2.3",
"resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz",
- "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==",
- "dev": true
+ "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw=="
},
"node_modules/@cspotcode/source-map-support": {
"version": "0.8.1",
@@ -90,11 +98,92 @@
"node": ">=12"
}
},
+ "node_modules/@eslint-community/eslint-utils": {
+ "version": "4.4.0",
+ "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz",
+ "integrity": "sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==",
+ "dependencies": {
+ "eslint-visitor-keys": "^3.3.0"
+ },
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "peerDependencies": {
+ "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0"
+ }
+ },
+ "node_modules/@eslint-community/regexpp": {
+ "version": "4.5.1",
+ "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.5.1.tgz",
+ "integrity": "sha512-Z5ba73P98O1KUYCCJTUeVpja9RcGoMdncZ6T49FCUl2lN38JtCJ+3WgIDBv0AuY4WChU5PmtJmOCTlN6FZTFKQ==",
+ "engines": {
+ "node": "^12.0.0 || ^14.0.0 || >=16.0.0"
+ }
+ },
+ "node_modules/@eslint/eslintrc": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.0.3.tgz",
+ "integrity": "sha512-+5gy6OQfk+xx3q0d6jGZZC3f3KzAkXc/IanVxd1is/VIIziRqqt3ongQz0FiTUXqTk0c7aDB3OaFuKnuSoJicQ==",
+ "dependencies": {
+ "ajv": "^6.12.4",
+ "debug": "^4.3.2",
+ "espree": "^9.5.2",
+ "globals": "^13.19.0",
+ "ignore": "^5.2.0",
+ "import-fresh": "^3.2.1",
+ "js-yaml": "^4.1.0",
+ "minimatch": "^3.1.2",
+ "strip-json-comments": "^3.1.1"
+ },
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/@eslint/js": {
+ "version": "8.43.0",
+ "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.43.0.tgz",
+ "integrity": "sha512-s2UHCoiXfxMvmfzqoN+vrQ84ahUSYde9qNO1MdxmoEhyHWsfmwOpFlwYV+ePJEVc7gFnATGUi376WowX1N7tFg==",
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ }
+ },
+ "node_modules/@humanwhocodes/config-array": {
+ "version": "0.11.10",
+ "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.10.tgz",
+ "integrity": "sha512-KVVjQmNUepDVGXNuoRRdmmEjruj0KfiGSbS8LVc12LMsWDQzRXJ0qdhN8L8uUigKpfEHRhlaQFY0ib1tnUbNeQ==",
+ "dependencies": {
+ "@humanwhocodes/object-schema": "^1.2.1",
+ "debug": "^4.1.1",
+ "minimatch": "^3.0.5"
+ },
+ "engines": {
+ "node": ">=10.10.0"
+ }
+ },
+ "node_modules/@humanwhocodes/module-importer": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz",
+ "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==",
+ "engines": {
+ "node": ">=12.22"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/nzakas"
+ }
+ },
+ "node_modules/@humanwhocodes/object-schema": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz",
+ "integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA=="
+ },
"node_modules/@istanbuljs/schema": {
"version": "0.1.3",
"resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz",
"integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==",
- "dev": true,
"engines": {
"node": ">=8"
}
@@ -103,7 +192,6 @@
"version": "0.3.3",
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz",
"integrity": "sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==",
- "dev": true,
"dependencies": {
"@jridgewell/set-array": "^1.0.1",
"@jridgewell/sourcemap-codec": "^1.4.10",
@@ -125,7 +213,6 @@
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz",
"integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==",
- "dev": true,
"engines": {
"node": ">=6.0.0"
}
@@ -149,6 +236,62 @@
"resolved": "https://registry.npmjs.org/@jsdevtools/ono/-/ono-7.1.3.tgz",
"integrity": "sha512-4JQNk+3mVzK3xh2rqd6RB4J46qUR19azEHBneZyTZM+c456qOrbbM/5xcR8huNCCcbVt7+UmizG6GuUvPvKUYg=="
},
+ "node_modules/@nodelib/fs.scandir": {
+ "version": "2.1.5",
+ "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
+ "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==",
+ "dependencies": {
+ "@nodelib/fs.stat": "2.0.5",
+ "run-parallel": "^1.1.9"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/@nodelib/fs.stat": {
+ "version": "2.0.5",
+ "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz",
+ "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==",
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/@nodelib/fs.walk": {
+ "version": "1.2.8",
+ "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz",
+ "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==",
+ "dependencies": {
+ "@nodelib/fs.scandir": "2.1.5",
+ "fastq": "^1.6.0"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/@pkgr/utils": {
+ "version": "2.4.1",
+ "resolved": "https://registry.npmjs.org/@pkgr/utils/-/utils-2.4.1.tgz",
+ "integrity": "sha512-JOqwkgFEyi+OROIyq7l4Jy28h/WwhDnG/cPkXG2Z1iFbubB6jsHW1NDvmyOzTBxHr3yg68YGirmh1JUgMqa+9w==",
+ "dependencies": {
+ "cross-spawn": "^7.0.3",
+ "fast-glob": "^3.2.12",
+ "is-glob": "^4.0.3",
+ "open": "^9.1.0",
+ "picocolors": "^1.0.0",
+ "tslib": "^2.5.0"
+ },
+ "engines": {
+ "node": "^12.20.0 || ^14.18.0 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/unts"
+ }
+ },
+ "node_modules/@pkgr/utils/node_modules/tslib": {
+ "version": "2.6.0",
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.0.tgz",
+ "integrity": "sha512-7At1WUettjcSRHXCyYtTselblcHl9PJFFVKiCAy/bY97+BPZXSQ2wbq0P9s8tK2G7dFQfNnlJnPAiArVBVBsfA=="
+ },
"node_modules/@tokens-studio/types": {
"version": "0.2.3",
"resolved": "https://registry.npmjs.org/@tokens-studio/types/-/types-0.2.3.tgz",
@@ -204,14 +347,18 @@
"node_modules/@types/istanbul-lib-coverage": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.4.tgz",
- "integrity": "sha512-z/QT1XN4K4KYuslS23k62yDIDLwLFkzxOuMplDtObz0+y7VqJCaO2o+SPwHCvLFZh7xazvvoor2tA/hPz9ee7g==",
- "dev": true
+ "integrity": "sha512-z/QT1XN4K4KYuslS23k62yDIDLwLFkzxOuMplDtObz0+y7VqJCaO2o+SPwHCvLFZh7xazvvoor2tA/hPz9ee7g=="
},
"node_modules/@types/json-schema": {
"version": "7.0.12",
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.12.tgz",
"integrity": "sha512-Hr5Jfhc9eYOQNPYO5WLDq/n4jqijdHNlDXjuAQkkt+mWdQR+XJToOHrsD4cPaMXpn6KO7y2+wM8AZEs8VpBLVA=="
},
+ "node_modules/@types/json5": {
+ "version": "0.0.29",
+ "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz",
+ "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ=="
+ },
"node_modules/@types/lodash": {
"version": "4.14.195",
"resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.195.tgz",
@@ -232,11 +379,215 @@
"resolved": "https://registry.npmjs.org/@types/prettier/-/prettier-2.7.3.tgz",
"integrity": "sha512-+68kP9yzs4LMp7VNh8gdzMSPZFL44MLGqiHWvttYJe+6qnuVr4Ek9wSBQoveqY/r+LwjCcU29kNVkidwim+kYA=="
},
+ "node_modules/@types/semver": {
+ "version": "7.5.0",
+ "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.0.tgz",
+ "integrity": "sha512-G8hZ6XJiHnuhQKR7ZmysCeJWE08o8T0AXtk5darsCaTVsYZhhgUrq53jizaR2FvsoeCwJhlmwTjkXBY5Pn/ZHw=="
+ },
+ "node_modules/@typescript-eslint/eslint-plugin": {
+ "version": "5.60.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.60.1.tgz",
+ "integrity": "sha512-KSWsVvsJsLJv3c4e73y/Bzt7OpqMCADUO846bHcuWYSYM19bldbAeDv7dYyV0jwkbMfJ2XdlzwjhXtuD7OY6bw==",
+ "dependencies": {
+ "@eslint-community/regexpp": "^4.4.0",
+ "@typescript-eslint/scope-manager": "5.60.1",
+ "@typescript-eslint/type-utils": "5.60.1",
+ "@typescript-eslint/utils": "5.60.1",
+ "debug": "^4.3.4",
+ "grapheme-splitter": "^1.0.4",
+ "ignore": "^5.2.0",
+ "natural-compare-lite": "^1.4.0",
+ "semver": "^7.3.7",
+ "tsutils": "^3.21.0"
+ },
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "@typescript-eslint/parser": "^5.0.0",
+ "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@typescript-eslint/parser": {
+ "version": "5.60.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.60.1.tgz",
+ "integrity": "sha512-pHWlc3alg2oSMGwsU/Is8hbm3XFbcrb6P5wIxcQW9NsYBfnrubl/GhVVD/Jm/t8HXhA2WncoIRfBtnCgRGV96Q==",
+ "dependencies": {
+ "@typescript-eslint/scope-manager": "5.60.1",
+ "@typescript-eslint/types": "5.60.1",
+ "@typescript-eslint/typescript-estree": "5.60.1",
+ "debug": "^4.3.4"
+ },
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@typescript-eslint/scope-manager": {
+ "version": "5.60.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.60.1.tgz",
+ "integrity": "sha512-Dn/LnN7fEoRD+KspEOV0xDMynEmR3iSHdgNsarlXNLGGtcUok8L4N71dxUgt3YvlO8si7E+BJ5Fe3wb5yUw7DQ==",
+ "dependencies": {
+ "@typescript-eslint/types": "5.60.1",
+ "@typescript-eslint/visitor-keys": "5.60.1"
+ },
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ }
+ },
+ "node_modules/@typescript-eslint/type-utils": {
+ "version": "5.60.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.60.1.tgz",
+ "integrity": "sha512-vN6UztYqIu05nu7JqwQGzQKUJctzs3/Hg7E2Yx8rz9J+4LgtIDFWjjl1gm3pycH0P3mHAcEUBd23LVgfrsTR8A==",
+ "dependencies": {
+ "@typescript-eslint/typescript-estree": "5.60.1",
+ "@typescript-eslint/utils": "5.60.1",
+ "debug": "^4.3.4",
+ "tsutils": "^3.21.0"
+ },
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "eslint": "*"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@typescript-eslint/types": {
+ "version": "5.60.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.60.1.tgz",
+ "integrity": "sha512-zDcDx5fccU8BA0IDZc71bAtYIcG9PowaOwaD8rjYbqwK7dpe/UMQl3inJ4UtUK42nOCT41jTSCwg76E62JpMcg==",
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ }
+ },
+ "node_modules/@typescript-eslint/typescript-estree": {
+ "version": "5.60.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.60.1.tgz",
+ "integrity": "sha512-hkX70J9+2M2ZT6fhti5Q2FoU9zb+GeZK2SLP1WZlvUDqdMbEKhexZODD1WodNRyO8eS+4nScvT0dts8IdaBzfw==",
+ "dependencies": {
+ "@typescript-eslint/types": "5.60.1",
+ "@typescript-eslint/visitor-keys": "5.60.1",
+ "debug": "^4.3.4",
+ "globby": "^11.1.0",
+ "is-glob": "^4.0.3",
+ "semver": "^7.3.7",
+ "tsutils": "^3.21.0"
+ },
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@typescript-eslint/utils": {
+ "version": "5.60.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.60.1.tgz",
+ "integrity": "sha512-tiJ7FFdFQOWssFa3gqb94Ilexyw0JVxj6vBzaSpfN/8IhoKkDuSAenUKvsSHw2A/TMpJb26izIszTXaqygkvpQ==",
+ "dependencies": {
+ "@eslint-community/eslint-utils": "^4.2.0",
+ "@types/json-schema": "^7.0.9",
+ "@types/semver": "^7.3.12",
+ "@typescript-eslint/scope-manager": "5.60.1",
+ "@typescript-eslint/types": "5.60.1",
+ "@typescript-eslint/typescript-estree": "5.60.1",
+ "eslint-scope": "^5.1.1",
+ "semver": "^7.3.7"
+ },
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0"
+ }
+ },
+ "node_modules/@typescript-eslint/utils/node_modules/eslint-scope": {
+ "version": "5.1.1",
+ "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz",
+ "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==",
+ "dependencies": {
+ "esrecurse": "^4.3.0",
+ "estraverse": "^4.1.1"
+ },
+ "engines": {
+ "node": ">=8.0.0"
+ }
+ },
+ "node_modules/@typescript-eslint/utils/node_modules/estraverse": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz",
+ "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==",
+ "engines": {
+ "node": ">=4.0"
+ }
+ },
+ "node_modules/@typescript-eslint/visitor-keys": {
+ "version": "5.60.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.60.1.tgz",
+ "integrity": "sha512-xEYIxKcultP6E/RMKqube11pGjXH1DCo60mQoWhVYyKfLkwbIVVjYxmOenNMxILx0TjCujPTjjnTIVzm09TXIw==",
+ "dependencies": {
+ "@typescript-eslint/types": "5.60.1",
+ "eslint-visitor-keys": "^3.3.0"
+ },
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ }
+ },
"node_modules/@vitest/coverage-v8": {
"version": "0.32.0",
"resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-0.32.0.tgz",
"integrity": "sha512-VXXlWq9X/NbsoP/l/CHLBjutsFFww1UY1qEhzGjn/DY7Tqe+z0Nu8XKc8im/XUAmjiWsh2XV7sy/F0IKAl4eaw==",
- "dev": true,
"dependencies": {
"@ampproject/remapping": "^2.2.1",
"@bcoe/v8-coverage": "^0.2.3",
@@ -332,6 +683,14 @@
"node": ">=0.4.0"
}
},
+ "node_modules/acorn-jsx": {
+ "version": "5.3.2",
+ "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz",
+ "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==",
+ "peerDependencies": {
+ "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0"
+ }
+ },
"node_modules/acorn-walk": {
"version": "8.2.0",
"resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.2.0.tgz",
@@ -340,6 +699,21 @@
"node": ">=0.4.0"
}
},
+ "node_modules/ajv": {
+ "version": "6.12.6",
+ "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
+ "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
+ "dependencies": {
+ "fast-deep-equal": "^3.1.1",
+ "fast-json-stable-stringify": "^2.0.0",
+ "json-schema-traverse": "^0.4.1",
+ "uri-js": "^4.2.2"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/epoberezkin"
+ }
+ },
"node_modules/ansi-regex": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
@@ -374,6 +748,78 @@
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
"integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="
},
+ "node_modules/array-buffer-byte-length": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.0.tgz",
+ "integrity": "sha512-LPuwb2P+NrQw3XhxGc36+XSvuBPopovXYTR9Ew++Du9Yb/bx5AzBfrIsBoj0EZUifjQU+sHL21sseZ3jerWO/A==",
+ "dependencies": {
+ "call-bind": "^1.0.2",
+ "is-array-buffer": "^3.0.1"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/array-includes": {
+ "version": "3.1.6",
+ "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.6.tgz",
+ "integrity": "sha512-sgTbLvL6cNnw24FnbaDyjmvddQ2ML8arZsgaJhoABMoplz/4QRhtrYS+alr1BUM1Bwp6dhx8vVCBSLG+StwOFw==",
+ "dependencies": {
+ "call-bind": "^1.0.2",
+ "define-properties": "^1.1.4",
+ "es-abstract": "^1.20.4",
+ "get-intrinsic": "^1.1.3",
+ "is-string": "^1.0.7"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/array-union": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz",
+ "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/array.prototype.flat": {
+ "version": "1.3.1",
+ "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.1.tgz",
+ "integrity": "sha512-roTU0KWIOmJ4DRLmwKd19Otg0/mT3qPNt0Qb3GWW8iObuZXxrjB/pzn0R3hqpRSWg4HCwqx+0vwOnWnvlOyeIA==",
+ "dependencies": {
+ "call-bind": "^1.0.2",
+ "define-properties": "^1.1.4",
+ "es-abstract": "^1.20.4",
+ "es-shim-unscopables": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/array.prototype.flatmap": {
+ "version": "1.3.1",
+ "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.3.1.tgz",
+ "integrity": "sha512-8UGn9O1FDVvMNB0UlLv4voxRMze7+FpHyF5mSMRjWHUMlpoDViniy05870VlxhfgTnLbpuwTzvD76MTtWxB/mQ==",
+ "dependencies": {
+ "call-bind": "^1.0.2",
+ "define-properties": "^1.1.4",
+ "es-abstract": "^1.20.4",
+ "es-shim-unscopables": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
"node_modules/assertion-error": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz",
@@ -382,6 +828,17 @@
"node": "*"
}
},
+ "node_modules/available-typed-arrays": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz",
+ "integrity": "sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw==",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
"node_modules/ayu": {
"version": "8.0.1",
"resolved": "https://registry.npmjs.org/ayu/-/ayu-8.0.1.tgz",
@@ -397,16 +854,30 @@
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="
},
- "node_modules/bezier-easing": {
- "version": "2.1.0",
- "resolved": "https://registry.npmjs.org/bezier-easing/-/bezier-easing-2.1.0.tgz",
- "integrity": "sha512-gbIqZ/eslnUFC1tjEvtz0sgx+xTK20wDnYMIA27VA04R7w6xxXQPZDbibjA9DTWZRA2CXtwHykkVzlCaAJAZig=="
+ "node_modules/big-integer": {
+ "version": "1.6.51",
+ "resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.51.tgz",
+ "integrity": "sha512-GPEid2Y9QU1Exl1rpO9B2IPJGHPSupF5GnVIP0blYvNOMer2bTvSWs1jGOUg04hTmu67nmLsQ9TBo1puaotBHg==",
+ "engines": {
+ "node": ">=0.6"
+ }
},
"node_modules/blueimp-md5": {
"version": "2.19.0",
"resolved": "https://registry.npmjs.org/blueimp-md5/-/blueimp-md5-2.19.0.tgz",
"integrity": "sha512-DRQrD6gJyy8FbiE4s+bDoXS9hiW3Vbx5uCdwvcCf3zLHL+Iv7LtGHLpr+GZV8rHG8tK766FGYBwRbu8pELTt+w=="
},
+ "node_modules/bplist-parser": {
+ "version": "0.2.0",
+ "resolved": "https://registry.npmjs.org/bplist-parser/-/bplist-parser-0.2.0.tgz",
+ "integrity": "sha512-z0M+byMThzQmD9NILRniCUXYsYpjwnlO8N5uCFaCqIOpqRsJCrQL9NK3JsD67CN5a08nF5oIL2bD6loTdHOuKw==",
+ "dependencies": {
+ "big-integer": "^1.6.44"
+ },
+ "engines": {
+ "node": ">= 5.10.0"
+ }
+ },
"node_modules/brace-expansion": {
"version": "1.1.11",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
@@ -416,6 +887,31 @@
"concat-map": "0.0.1"
}
},
+ "node_modules/braces": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz",
+ "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==",
+ "dependencies": {
+ "fill-range": "^7.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/bundle-name": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/bundle-name/-/bundle-name-3.0.0.tgz",
+ "integrity": "sha512-PKA4BeSvBpQKQ8iPOGCSiell+N8P+Tf1DlwqmYhpe2gAhKPHn8EYOxVT+ShuGmhg8lN8XiSlS80yiExKXrURlw==",
+ "dependencies": {
+ "run-applescript": "^5.0.0"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
"node_modules/cac": {
"version": "6.7.14",
"resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz",
@@ -424,20 +920,29 @@
"node": ">=8"
}
},
+ "node_modules/call-bind": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz",
+ "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==",
+ "dependencies": {
+ "function-bind": "^1.1.1",
+ "get-intrinsic": "^1.0.2"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
"node_modules/call-me-maybe": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/call-me-maybe/-/call-me-maybe-1.0.2.tgz",
"integrity": "sha512-HpX65o1Hnr9HH25ojC1YGs7HCQLq0GCOibSaWER0eNpgJ/Z1MZv2mTc7+xh6WOPxbRVcmgbv4hGU+uSQ/2xFZQ=="
},
- "node_modules/case-anything": {
- "version": "2.1.10",
- "resolved": "https://registry.npmjs.org/case-anything/-/case-anything-2.1.10.tgz",
- "integrity": "sha512-JczJwVrCP0jPKh05McyVsuOg6AYosrB9XWZKbQzXeDAm2ClE/PJE/BcrrQrVyGYH7Jg8V/LDupmyL4kFlVsVFQ==",
+ "node_modules/callsites": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
+ "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==",
"engines": {
- "node": ">=12.13"
- },
- "funding": {
- "url": "https://github.com/sponsors/mesqueeb"
+ "node": ">=6"
}
},
"node_modules/chai": {
@@ -457,18 +962,47 @@
"node": ">=4"
}
},
- "node_modules/check-error": {
- "version": "1.0.2",
- "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.2.tgz",
- "integrity": "sha512-BrgHpW9NURQgzoNyjfq0Wu6VFO6D7IZEmJNdtgNqpzGG8RuNFHt2jQxWlAs4HMe119chBnv+34syEZtc6IhLtA==",
+ "node_modules/chalk": {
+ "version": "4.1.2",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
+ "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
+ "dependencies": {
+ "ansi-styles": "^4.1.0",
+ "supports-color": "^7.1.0"
+ },
"engines": {
- "node": "*"
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/chalk?sponsor=1"
}
},
- "node_modules/chroma-js": {
- "version": "2.4.2",
- "resolved": "https://registry.npmjs.org/chroma-js/-/chroma-js-2.4.2.tgz",
- "integrity": "sha512-U9eDw6+wt7V8z5NncY2jJfZa+hUH8XEj8FQHgFJTrUFnJfXYf4Ml4adI2vXZOjqRDpFWtYVWypDfZwnJ+HIR4A=="
+ "node_modules/chalk/node_modules/ansi-styles": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
+ "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+ "dependencies": {
+ "color-convert": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+ }
+ },
+ "node_modules/check-error": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.2.tgz",
+ "integrity": "sha512-BrgHpW9NURQgzoNyjfq0Wu6VFO6D7IZEmJNdtgNqpzGG8RuNFHt2jQxWlAs4HMe119chBnv+34syEZtc6IhLtA==",
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/chroma-js": {
+ "version": "2.4.2",
+ "resolved": "https://registry.npmjs.org/chroma-js/-/chroma-js-2.4.2.tgz",
+ "integrity": "sha512-U9eDw6+wt7V8z5NncY2jJfZa+hUH8XEj8FQHgFJTrUFnJfXYf4Ml4adI2vXZOjqRDpFWtYVWypDfZwnJ+HIR4A=="
},
"node_modules/cli-color": {
"version": "2.0.3",
@@ -485,6 +1019,22 @@
"node": ">=0.10"
}
},
+ "node_modules/color-convert": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
+ "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+ "dependencies": {
+ "color-name": "~1.1.4"
+ },
+ "engines": {
+ "node": ">=7.0.0"
+ }
+ },
+ "node_modules/color-name": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
+ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="
+ },
"node_modules/concat-map": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
@@ -511,14 +1061,26 @@
"node_modules/convert-source-map": {
"version": "1.9.0",
"resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz",
- "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==",
- "dev": true
+ "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A=="
},
"node_modules/create-require": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz",
"integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ=="
},
+ "node_modules/cross-spawn": {
+ "version": "7.0.3",
+ "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz",
+ "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==",
+ "dependencies": {
+ "path-key": "^3.1.0",
+ "shebang-command": "^2.0.0",
+ "which": "^2.0.1"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
"node_modules/d": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/d/-/d-1.0.1.tgz",
@@ -566,6 +1128,11 @@
"node": ">=6"
}
},
+ "node_modules/deep-is": {
+ "version": "0.1.4",
+ "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz",
+ "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ=="
+ },
"node_modules/deepmerge": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.0.tgz",
@@ -574,6 +1141,64 @@
"node": ">=0.10.0"
}
},
+ "node_modules/default-browser": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/default-browser/-/default-browser-4.0.0.tgz",
+ "integrity": "sha512-wX5pXO1+BrhMkSbROFsyxUm0i/cJEScyNhA4PPxc41ICuv05ZZB/MX28s8aZx6xjmatvebIapF6hLEKEcpneUA==",
+ "dependencies": {
+ "bundle-name": "^3.0.0",
+ "default-browser-id": "^3.0.0",
+ "execa": "^7.1.1",
+ "titleize": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=14.16"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/default-browser-id": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/default-browser-id/-/default-browser-id-3.0.0.tgz",
+ "integrity": "sha512-OZ1y3y0SqSICtE8DE4S8YOE9UZOJ8wO16fKWVP5J1Qz42kV9jcnMVFrEE/noXb/ss3Q4pZIH79kxofzyNNtUNA==",
+ "dependencies": {
+ "bplist-parser": "^0.2.0",
+ "untildify": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/define-lazy-prop": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-3.0.0.tgz",
+ "integrity": "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/define-properties": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.0.tgz",
+ "integrity": "sha512-xvqAVKGfT1+UAvPwKTVw/njhdQ8ZhXK4lI0bCIuCMrp2up9nPnaDftrLtmpTazqd1o+UY4zgzU+avtMbDP+ldA==",
+ "dependencies": {
+ "has-property-descriptors": "^1.0.0",
+ "object-keys": "^1.1.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
"node_modules/diff": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz",
@@ -582,6 +1207,124 @@
"node": ">=0.3.1"
}
},
+ "node_modules/dir-glob": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz",
+ "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==",
+ "dependencies": {
+ "path-type": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/doctrine": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz",
+ "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==",
+ "dependencies": {
+ "esutils": "^2.0.2"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/enhanced-resolve": {
+ "version": "5.15.0",
+ "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.15.0.tgz",
+ "integrity": "sha512-LXYT42KJ7lpIKECr2mAXIaMldcNCh/7E0KBKOu4KSfkHmP+mZmSs+8V5gBAqisWBy0OO4W5Oyys0GO1Y8KtdKg==",
+ "dependencies": {
+ "graceful-fs": "^4.2.4",
+ "tapable": "^2.2.0"
+ },
+ "engines": {
+ "node": ">=10.13.0"
+ }
+ },
+ "node_modules/es-abstract": {
+ "version": "1.21.2",
+ "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.21.2.tgz",
+ "integrity": "sha512-y/B5POM2iBnIxCiernH1G7rC9qQoM77lLIMQLuob0zhp8C56Po81+2Nj0WFKnd0pNReDTnkYryc+zhOzpEIROg==",
+ "dependencies": {
+ "array-buffer-byte-length": "^1.0.0",
+ "available-typed-arrays": "^1.0.5",
+ "call-bind": "^1.0.2",
+ "es-set-tostringtag": "^2.0.1",
+ "es-to-primitive": "^1.2.1",
+ "function.prototype.name": "^1.1.5",
+ "get-intrinsic": "^1.2.0",
+ "get-symbol-description": "^1.0.0",
+ "globalthis": "^1.0.3",
+ "gopd": "^1.0.1",
+ "has": "^1.0.3",
+ "has-property-descriptors": "^1.0.0",
+ "has-proto": "^1.0.1",
+ "has-symbols": "^1.0.3",
+ "internal-slot": "^1.0.5",
+ "is-array-buffer": "^3.0.2",
+ "is-callable": "^1.2.7",
+ "is-negative-zero": "^2.0.2",
+ "is-regex": "^1.1.4",
+ "is-shared-array-buffer": "^1.0.2",
+ "is-string": "^1.0.7",
+ "is-typed-array": "^1.1.10",
+ "is-weakref": "^1.0.2",
+ "object-inspect": "^1.12.3",
+ "object-keys": "^1.1.1",
+ "object.assign": "^4.1.4",
+ "regexp.prototype.flags": "^1.4.3",
+ "safe-regex-test": "^1.0.0",
+ "string.prototype.trim": "^1.2.7",
+ "string.prototype.trimend": "^1.0.6",
+ "string.prototype.trimstart": "^1.0.6",
+ "typed-array-length": "^1.0.4",
+ "unbox-primitive": "^1.0.2",
+ "which-typed-array": "^1.1.9"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/es-set-tostringtag": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.0.1.tgz",
+ "integrity": "sha512-g3OMbtlwY3QewlqAiMLI47KywjWZoEytKr8pf6iTC8uJq5bIAH52Z9pnQ8pVL6whrCto53JZDuUIsifGeLorTg==",
+ "dependencies": {
+ "get-intrinsic": "^1.1.3",
+ "has": "^1.0.3",
+ "has-tostringtag": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-shim-unscopables": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.0.0.tgz",
+ "integrity": "sha512-Jm6GPcCdC30eMLbZ2x8z2WuRwAws3zTBBKuusffYVUrNj/GVSUAZ+xKMaUpfNDR5IbyNA5LJbaecoUVbmUcB1w==",
+ "dependencies": {
+ "has": "^1.0.3"
+ }
+ },
+ "node_modules/es-to-primitive": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz",
+ "integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==",
+ "dependencies": {
+ "is-callable": "^1.1.4",
+ "is-date-object": "^1.0.1",
+ "is-symbol": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
"node_modules/es5-ext": {
"version": "0.10.62",
"resolved": "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.62.tgz",
@@ -1,24 +1,22 @@
{
"name": "styles",
"version": "1.0.0",
- "description": "",
- "main": "index.js",
+ "description": "Typescript app that builds Zed's themes",
+ "main": "./src/build_themes.ts",
"scripts": {
- "build": "ts-node ./src/buildThemes.ts",
- "build-licenses": "ts-node ./src/buildLicenses.ts",
- "build-tokens": "ts-node ./src/buildTokens.ts",
- "build-types": "ts-node ./src/buildTypes.ts",
+ "build": "ts-node ./src/build_themes.ts",
+ "build-licenses": "ts-node ./src/build_licenses.ts",
+ "build-tokens": "ts-node ./src/build_tokens.ts",
+ "build-types": "ts-node ./src/build_types.ts",
"test": "vitest"
},
- "author": "",
+ "author": "Zed Industries (https://github.com/zed-industries/)",
"license": "ISC",
"dependencies": {
"@tokens-studio/types": "^0.2.3",
"@types/chroma-js": "^2.4.0",
"@types/node": "^18.14.1",
"ayu": "^8.0.1",
- "bezier-easing": "^2.1.0",
- "case-anything": "^2.1.10",
"chroma-js": "^2.4.2",
"deepmerge": "^4.3.0",
"json-schema-to-typescript": "^13.0.2",
@@ -26,15 +24,13 @@
"ts-deepmerge": "^6.0.3",
"ts-node": "^10.9.1",
"utility-types": "^3.10.0",
- "vitest": "^0.32.0"
- },
- "prettier": {
- "semi": false,
- "printWidth": 80,
- "htmlWhitespaceSensitivity": "strict",
- "tabWidth": 4
- },
- "devDependencies": {
- "@vitest/coverage-v8": "^0.32.0"
+ "vitest": "^0.32.0",
+ "@typescript-eslint/eslint-plugin": "^5.60.1",
+ "@typescript-eslint/parser": "^5.60.1",
+ "@vitest/coverage-v8": "^0.32.0",
+ "eslint": "^8.43.0",
+ "eslint-import-resolver-typescript": "^3.5.5",
+ "eslint-plugin-import": "^2.27.5",
+ "typescript": "^5.1.5"
}
}
@@ -1,50 +0,0 @@
-import * as fs from "fs"
-import toml from "toml"
-import { themes } from "./themes"
-import { ThemeConfig } from "./common"
-
-const ACCEPTED_LICENSES_FILE = `${__dirname}/../../script/licenses/zed-licenses.toml`
-
-// Use the cargo-about configuration file as the source of truth for supported licenses.
-function parseAcceptedToml(file: string): string[] {
- let buffer = fs.readFileSync(file).toString()
-
- let obj = toml.parse(buffer)
-
- if (!Array.isArray(obj.accepted)) {
- throw Error("Accepted license source is malformed")
- }
-
- return obj.accepted
-}
-
-function checkLicenses(themes: ThemeConfig[]) {
- for (const theme of themes) {
- if (!theme.licenseFile) {
- throw Error(`Theme ${theme.name} should have a LICENSE file`)
- }
- }
-}
-
-function generateLicenseFile(themes: ThemeConfig[]) {
- checkLicenses(themes)
- for (const theme of themes) {
- const licenseText = fs.readFileSync(theme.licenseFile).toString()
- writeLicense(theme.name, licenseText, theme.licenseUrl)
- }
-}
-
-function writeLicense(
- themeName: string,
- licenseText: string,
- licenseUrl?: string
-) {
- process.stdout.write(
- licenseUrl
- ? `## [${themeName}](${licenseUrl})\n\n${licenseText}\n********************************************************************************\n\n`
- : `## ${themeName}\n\n${licenseText}\n********************************************************************************\n\n`
- )
-}
-
-const acceptedLicenses = parseAcceptedToml(ACCEPTED_LICENSES_FILE)
-generateLicenseFile(themes)
@@ -1,43 +0,0 @@
-import * as fs from "fs"
-import { tmpdir } from "os"
-import * as path from "path"
-import app from "./styleTree/app"
-import { ColorScheme, createColorScheme } from "./theme/colorScheme"
-import snakeCase from "./utils/snakeCase"
-import { themes } from "./themes"
-
-const assetsDirectory = `${__dirname}/../../assets`
-const tempDirectory = fs.mkdtempSync(path.join(tmpdir(), "build-themes"))
-
-// Clear existing themes
-function clearThemes(themeDirectory: string) {
- if (!fs.existsSync(themeDirectory)) {
- fs.mkdirSync(themeDirectory, { recursive: true })
- } else {
- for (const file of fs.readdirSync(themeDirectory)) {
- if (file.endsWith(".json")) {
- fs.unlinkSync(path.join(themeDirectory, file))
- }
- }
- }
-}
-
-function writeThemes(colorSchemes: ColorScheme[], outputDirectory: string) {
- clearThemes(outputDirectory)
- for (let colorScheme of colorSchemes) {
- let styleTree = snakeCase(app(colorScheme))
- let styleTreeJSON = JSON.stringify(styleTree, null, 2)
- let tempPath = path.join(tempDirectory, `${colorScheme.name}.json`)
- let outPath = path.join(outputDirectory, `${colorScheme.name}.json`)
- fs.writeFileSync(tempPath, styleTreeJSON)
- fs.renameSync(tempPath, outPath)
- console.log(`- ${outPath} created`)
- }
-}
-
-const colorSchemes: ColorScheme[] = themes.map((theme) =>
- createColorScheme(theme)
-)
-
-// Write new themes to theme directory
-writeThemes(colorSchemes, `${assetsDirectory}/themes`)
@@ -1,87 +0,0 @@
-import * as fs from "fs"
-import * as path from "path"
-import { ColorScheme, createColorScheme } from "./common"
-import { themes } from "./themes"
-import { slugify } from "./utils/slugify"
-import { colorSchemeTokens } from "./theme/tokens/colorScheme"
-
-const TOKENS_DIRECTORY = path.join(__dirname, "..", "target", "tokens")
-const TOKENS_FILE = path.join(TOKENS_DIRECTORY, "$themes.json")
-const METADATA_FILE = path.join(TOKENS_DIRECTORY, "$metadata.json")
-
-function clearTokens(tokensDirectory: string) {
- if (!fs.existsSync(tokensDirectory)) {
- fs.mkdirSync(tokensDirectory, { recursive: true })
- } else {
- for (const file of fs.readdirSync(tokensDirectory)) {
- if (file.endsWith(".json")) {
- fs.unlinkSync(path.join(tokensDirectory, file))
- }
- }
- }
-}
-
-type TokenSet = {
- id: string
- name: string
- selectedTokenSets: { [key: string]: "enabled" }
-}
-
-function buildTokenSetOrder(colorSchemes: ColorScheme[]): {
- tokenSetOrder: string[]
-} {
- const tokenSetOrder: string[] = colorSchemes.map((scheme) =>
- scheme.name.toLowerCase().replace(/\s+/g, "_")
- )
- return { tokenSetOrder }
-}
-
-function buildThemesIndex(colorSchemes: ColorScheme[]): TokenSet[] {
- const themesIndex: TokenSet[] = colorSchemes.map((scheme, index) => {
- const id = `${scheme.isLight ? "light" : "dark"}_${scheme.name
- .toLowerCase()
- .replace(/\s+/g, "_")}_${index}`
- const selectedTokenSets: { [key: string]: "enabled" } = {}
- const tokenSet = scheme.name.toLowerCase().replace(/\s+/g, "_")
- selectedTokenSets[tokenSet] = "enabled"
-
- return {
- id,
- name: `${scheme.name} - ${scheme.isLight ? "Light" : "Dark"}`,
- selectedTokenSets,
- }
- })
-
- return themesIndex
-}
-
-function writeTokens(colorSchemes: ColorScheme[], tokensDirectory: string) {
- clearTokens(tokensDirectory)
-
- for (const colorScheme of colorSchemes) {
- const fileName = slugify(colorScheme.name) + ".json"
- const tokens = colorSchemeTokens(colorScheme)
- const tokensJSON = JSON.stringify(tokens, null, 2)
- const outPath = path.join(tokensDirectory, fileName)
- fs.writeFileSync(outPath, tokensJSON, { mode: 0o644 })
- console.log(`- ${outPath} created`)
- }
-
- const themeIndexData = buildThemesIndex(colorSchemes)
-
- const themesJSON = JSON.stringify(themeIndexData, null, 2)
- fs.writeFileSync(TOKENS_FILE, themesJSON, { mode: 0o644 })
- console.log(`- ${TOKENS_FILE} created`)
-
- const tokenSetOrderData = buildTokenSetOrder(colorSchemes)
-
- const metadataJSON = JSON.stringify(tokenSetOrderData, null, 2)
- fs.writeFileSync(METADATA_FILE, metadataJSON, { mode: 0o644 })
- console.log(`- ${METADATA_FILE} created`)
-}
-
-const colorSchemes: ColorScheme[] = themes.map((theme) =>
- createColorScheme(theme)
-)
-
-writeTokens(colorSchemes, TOKENS_DIRECTORY)
@@ -1,64 +0,0 @@
-import * as fs from "fs/promises"
-import * as fsSync from "fs"
-import * as path from "path"
-import { compile } from "json-schema-to-typescript"
-
-const BANNER = `/*
-* This file is autogenerated
-*/\n\n`
-const dirname = __dirname
-
-async function main() {
- let schemasPath = path.join(dirname, "../../", "crates/theme/schemas")
- let schemaFiles = (await fs.readdir(schemasPath)).filter((x) =>
- x.endsWith(".json")
- )
-
- let compiledTypes = new Set()
-
- for (let filename of schemaFiles) {
- let filePath = path.join(schemasPath, filename)
- const fileContents = await fs.readFile(filePath)
- let schema = JSON.parse(fileContents.toString())
- let compiled = await compile(schema, schema.title, {
- bannerComment: "",
- })
- let eachType = compiled.split("export")
- for (let type of eachType) {
- if (!type) {
- continue
- }
- compiledTypes.add("export " + type.trim())
- }
- }
-
- let output = BANNER + Array.from(compiledTypes).join("\n\n")
- let outputPath = path.join(dirname, "../../styles/src/types/zed.ts")
-
- try {
- let existing = await fs.readFile(outputPath)
- if (existing.toString() == output) {
- // Skip writing if it hasn't changed
- console.log("Schemas are up to date")
- return
- }
- } catch (e) {
- // It's fine if there's no output from a previous run.
- // @ts-ignore
- if (e.code !== "ENOENT") {
- throw e
- }
- }
-
- const typesDic = path.dirname(outputPath)
- if (!fsSync.existsSync(typesDic)) {
- await fs.mkdir(typesDic)
- }
- await fs.writeFile(outputPath, output)
- console.log(`Wrote Typescript types to ${outputPath}`)
-}
-
-main().catch((e) => {
- console.error(e)
- process.exit(1)
-})
@@ -0,0 +1,50 @@
+import * as fs from "fs"
+import toml from "toml"
+import { themes } from "./themes"
+import { ThemeConfig } from "./common"
+
+const ACCEPTED_LICENSES_FILE = `${__dirname}/../../script/licenses/zed-licenses.toml`
+
+// Use the cargo-about configuration file as the source of truth for supported licenses.
+function parse_accepted_toml(file: string): string[] {
+ const buffer = fs.readFileSync(file).toString()
+
+ const obj = toml.parse(buffer)
+
+ if (!Array.isArray(obj.accepted)) {
+ throw Error("Accepted license source is malformed")
+ }
+
+ return obj.accepted
+}
+
+function check_licenses(themes: ThemeConfig[]) {
+ for (const theme of themes) {
+ if (!theme.license_file) {
+ throw Error(`Theme ${theme.name} should have a LICENSE file`)
+ }
+ }
+}
+
+function generate_license_file(themes: ThemeConfig[]) {
+ check_licenses(themes)
+ for (const theme of themes) {
+ const license_text = fs.readFileSync(theme.license_file).toString()
+ write_license(theme.name, license_text, theme.license_url)
+ }
+}
+
+function write_license(
+ theme_name: string,
+ license_text: string,
+ license_url?: string
+) {
+ process.stdout.write(
+ license_url
+ ? `## [${theme_name}](${license_url})\n\n${license_text}\n********************************************************************************\n\n`
+ : `## ${theme_name}\n\n${license_text}\n********************************************************************************\n\n`
+ )
+}
+
+const accepted_licenses = parse_accepted_toml(ACCEPTED_LICENSES_FILE)
+generate_license_file(themes)
@@ -0,0 +1,43 @@
+import * as fs from "fs"
+import { tmpdir } from "os"
+import * as path from "path"
+import app from "./style_tree/app"
+import { ColorScheme, create_color_scheme } from "./theme/color_scheme"
+import { themes } from "./themes"
+
+const assets_directory = `${__dirname}/../../assets`
+const temp_directory = fs.mkdtempSync(path.join(tmpdir(), "build-themes"))
+
+function clear_themes(theme_directory: string) {
+ if (!fs.existsSync(theme_directory)) {
+ fs.mkdirSync(theme_directory, { recursive: true })
+ } else {
+ for (const file of fs.readdirSync(theme_directory)) {
+ if (file.endsWith(".json")) {
+ fs.unlinkSync(path.join(theme_directory, file))
+ }
+ }
+ }
+}
+
+function write_themes(themes: ColorScheme[], output_directory: string) {
+ clear_themes(output_directory)
+ for (const color_scheme of themes) {
+ const style_tree = app(color_scheme)
+ const style_tree_json = JSON.stringify(style_tree, null, 2)
+ const temp_path = path.join(temp_directory, `${color_scheme.name}.json`)
+ const out_path = path.join(
+ output_directory,
+ `${color_scheme.name}.json`
+ )
+ fs.writeFileSync(temp_path, style_tree_json)
+ fs.renameSync(temp_path, out_path)
+ console.log(`- ${out_path} created`)
+ }
+}
+
+const all_themes: ColorScheme[] = themes.map((theme) =>
+ create_color_scheme(theme)
+)
+
+write_themes(all_themes, `${assets_directory}/themes`)
@@ -0,0 +1,87 @@
+import * as fs from "fs"
+import * as path from "path"
+import { ColorScheme, create_color_scheme } from "./common"
+import { themes } from "./themes"
+import { slugify } from "./utils/slugify"
+import { theme_tokens } from "./theme/tokens/color_scheme"
+
+const TOKENS_DIRECTORY = path.join(__dirname, "..", "target", "tokens")
+const TOKENS_FILE = path.join(TOKENS_DIRECTORY, "$themes.json")
+const METADATA_FILE = path.join(TOKENS_DIRECTORY, "$metadata.json")
+
+function clear_tokens(tokens_directory: string) {
+ if (!fs.existsSync(tokens_directory)) {
+ fs.mkdirSync(tokens_directory, { recursive: true })
+ } else {
+ for (const file of fs.readdirSync(tokens_directory)) {
+ if (file.endsWith(".json")) {
+ fs.unlinkSync(path.join(tokens_directory, file))
+ }
+ }
+ }
+}
+
+type TokenSet = {
+ id: string
+ name: string
+ selected_token_sets: { [key: string]: "enabled" }
+}
+
+function build_token_set_order(theme: ColorScheme[]): {
+ token_set_order: string[]
+} {
+ const token_set_order: string[] = theme.map((scheme) =>
+ scheme.name.toLowerCase().replace(/\s+/g, "_")
+ )
+ return { token_set_order }
+}
+
+function build_themes_index(theme: ColorScheme[]): TokenSet[] {
+ const themes_index: TokenSet[] = theme.map((scheme, index) => {
+ const id = `${scheme.is_light ? "light" : "dark"}_${scheme.name
+ .toLowerCase()
+ .replace(/\s+/g, "_")}_${index}`
+ const selected_token_sets: { [key: string]: "enabled" } = {}
+ const token_set = scheme.name.toLowerCase().replace(/\s+/g, "_")
+ selected_token_sets[token_set] = "enabled"
+
+ return {
+ id,
+ name: `${scheme.name} - ${scheme.is_light ? "Light" : "Dark"}`,
+ selected_token_sets,
+ }
+ })
+
+ return themes_index
+}
+
+function write_tokens(themes: ColorScheme[], tokens_directory: string) {
+ clear_tokens(tokens_directory)
+
+ for (const theme of themes) {
+ const file_name = slugify(theme.name) + ".json"
+ const tokens = theme_tokens(theme)
+ const tokens_json = JSON.stringify(tokens, null, 2)
+ const out_path = path.join(tokens_directory, file_name)
+ fs.writeFileSync(out_path, tokens_json, { mode: 0o644 })
+ console.log(`- ${out_path} created`)
+ }
+
+ const theme_index_data = build_themes_index(themes)
+
+ const themes_json = JSON.stringify(theme_index_data, null, 2)
+ fs.writeFileSync(TOKENS_FILE, themes_json, { mode: 0o644 })
+ console.log(`- ${TOKENS_FILE} created`)
+
+ const token_set_order_data = build_token_set_order(themes)
+
+ const metadata_json = JSON.stringify(token_set_order_data, null, 2)
+ fs.writeFileSync(METADATA_FILE, metadata_json, { mode: 0o644 })
+ console.log(`- ${METADATA_FILE} created`)
+}
+
+const all_themes: ColorScheme[] = themes.map((theme) =>
+ create_color_scheme(theme)
+)
+
+write_tokens(all_themes, TOKENS_DIRECTORY)
@@ -0,0 +1,62 @@
+import * as fs from "fs/promises"
+import * as fsSync from "fs"
+import * as path from "path"
+import { compile } from "json-schema-to-typescript"
+
+const BANNER = `/*
+* This file is autogenerated
+*/\n\n`
+const dirname = __dirname
+
+async function main() {
+ const schemas_path = path.join(dirname, "../../", "crates/theme/schemas")
+ const schema_files = (await fs.readdir(schemas_path)).filter((x) =>
+ x.endsWith(".json")
+ )
+
+ const compiled_types = new Set()
+
+ for (const filename of schema_files) {
+ const file_path = path.join(schemas_path, filename)
+ const file_contents = await fs.readFile(file_path)
+ const schema = JSON.parse(file_contents.toString())
+ const compiled = await compile(schema, schema.title, {
+ bannerComment: "",
+ })
+ const each_type = compiled.split("export")
+ for (const type of each_type) {
+ if (!type) {
+ continue
+ }
+ compiled_types.add("export " + type.trim())
+ }
+ }
+
+ const output = BANNER + Array.from(compiled_types).join("\n\n")
+ const output_path = path.join(dirname, "../../styles/src/types/zed.ts")
+
+ try {
+ const existing = await fs.readFile(output_path)
+ if (existing.toString() == output) {
+ // Skip writing if it hasn't changed
+ console.log("Schemas are up to date")
+ return
+ }
+ } catch (e) {
+ if (e.code !== "ENOENT") {
+ throw e
+ }
+ }
+
+ const types_dic = path.dirname(output_path)
+ if (!fsSync.existsSync(types_dic)) {
+ await fs.mkdir(types_dic)
+ }
+ await fs.writeFile(output_path, output)
+ console.log(`Wrote Typescript types to ${output_path}`)
+}
+
+main().catch((e) => {
+ console.error(e)
+ process.exit(1)
+})
@@ -2,42 +2,24 @@ import chroma from "chroma-js"
export * from "./theme"
export { chroma }
-export const fontFamilies = {
+export const font_families = {
sans: "Zed Sans",
mono: "Zed Mono",
}
-export const fontSizes = {
- "3xs": 8,
+export const font_sizes = {
"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 type FontWeight = "normal" | "bold"
-export const fontWeights: { [key: string]: FontWeight } = {
- thin: "thin",
- extra_light: "extra_light",
- light: "light",
+export const font_weights: { [key: string]: FontWeight } = {
normal: "normal",
- medium: "medium",
- semibold: "semibold",
bold: "bold",
- extra_bold: "extra_bold",
- black: "black",
}
export const sizes = {
@@ -1,6 +1,6 @@
-import { ColorScheme } from "../common"
import { interactive, toggleable } from "../element"
-import { background, foreground } from "../styleTree/components"
+import { background, foreground } from "../style_tree/components"
+import { ColorScheme } from "../theme/color_scheme"
export type Margin = {
top: number
@@ -1,11 +1,11 @@
-import { ColorScheme } from "../common"
import { interactive, toggleable } from "../element"
import {
TextProperties,
background,
foreground,
text,
-} from "../styleTree/components"
+} from "../style_tree/components"
+import { ColorScheme } from "../theme/color_scheme"
import { Margin } from "./icon_button"
interface TextButtonOptions {
@@ -8,7 +8,7 @@ import { describe, it, expect } from "vitest"
describe("interactive", () => {
it("creates an Interactive<Element> with base properties and states", () => {
const result = interactive({
- base: { fontSize: 10, color: "#FFFFFF" },
+ base: { font_size: 10, color: "#FFFFFF" },
state: {
hovered: { color: "#EEEEEE" },
clicked: { color: "#CCCCCC" },
@@ -16,25 +16,25 @@ describe("interactive", () => {
})
expect(result).toEqual({
- default: { color: "#FFFFFF", fontSize: 10 },
- hovered: { color: "#EEEEEE", fontSize: 10 },
- clicked: { color: "#CCCCCC", fontSize: 10 },
+ default: { color: "#FFFFFF", font_size: 10 },
+ hovered: { color: "#EEEEEE", font_size: 10 },
+ clicked: { color: "#CCCCCC", font_size: 10 },
})
})
it("creates an Interactive<Element> with no base properties", () => {
const result = interactive({
state: {
- default: { color: "#FFFFFF", fontSize: 10 },
+ default: { color: "#FFFFFF", font_size: 10 },
hovered: { color: "#EEEEEE" },
clicked: { color: "#CCCCCC" },
},
})
expect(result).toEqual({
- default: { color: "#FFFFFF", fontSize: 10 },
- hovered: { color: "#EEEEEE", fontSize: 10 },
- clicked: { color: "#CCCCCC", fontSize: 10 },
+ default: { color: "#FFFFFF", font_size: 10 },
+ hovered: { color: "#EEEEEE", font_size: 10 },
+ clicked: { color: "#CCCCCC", font_size: 10 },
})
})
@@ -48,7 +48,7 @@ describe("interactive", () => {
it("throws error when no other state besides default is present", () => {
const state = {
- default: { fontSize: 10 },
+ default: { font_size: 10 },
}
expect(() => interactive({ state })).toThrow(NOT_ENOUGH_STATES_ERROR)
@@ -37,61 +37,61 @@ interface InteractiveProps<T> {
* @param state Object containing optional modified fields to be included in the resulting object for each state.
* @returns Interactive<T> object with fields from `base` and `state`.
*/
-export function interactive<T extends Object>({
+export function interactive<T extends object>({
base,
state,
}: InteractiveProps<T>): Interactive<T> {
if (!base && !state.default) throw new Error(NO_DEFAULT_OR_BASE_ERROR)
- let defaultState: T
+ let default_state: T
if (state.default && base) {
- defaultState = merge(base, state.default) as T
+ default_state = merge(base, state.default) as T
} else {
- defaultState = base ? base : (state.default as T)
+ default_state = base ? base : (state.default as T)
}
- let interactiveObj: Interactive<T> = {
- default: defaultState,
+ const interactive_obj: Interactive<T> = {
+ default: default_state,
}
- let stateCount = 0
+ let state_count = 0
if (state.hovered !== undefined) {
- interactiveObj.hovered = merge(
- interactiveObj.default,
+ interactive_obj.hovered = merge(
+ interactive_obj.default,
state.hovered
) as T
- stateCount++
+ state_count++
}
if (state.clicked !== undefined) {
- interactiveObj.clicked = merge(
- interactiveObj.default,
+ interactive_obj.clicked = merge(
+ interactive_obj.default,
state.clicked
) as T
- stateCount++
+ state_count++
}
if (state.selected !== undefined) {
- interactiveObj.selected = merge(
- interactiveObj.default,
+ interactive_obj.selected = merge(
+ interactive_obj.default,
state.selected
) as T
- stateCount++
+ state_count++
}
if (state.disabled !== undefined) {
- interactiveObj.disabled = merge(
- interactiveObj.default,
+ interactive_obj.disabled = merge(
+ interactive_obj.default,
state.disabled
) as T
- stateCount++
+ state_count++
}
- if (stateCount < 1) {
+ if (state_count < 1) {
throw new Error(NOT_ENOUGH_STATES_ERROR)
}
- return interactiveObj
+ return interactive_obj
}
@@ -35,13 +35,13 @@ export function toggleable<T extends object>(
if (!base && !state.inactive) throw new Error(NO_INACTIVE_OR_BASE_ERROR)
if (!state.active) throw new Error(NO_ACTIVE_ERROR)
- const inactiveState = base
+ const inactive_state = base
? ((state.inactive ? merge(base, state.inactive) : base) as T)
: (state.inactive as T)
- const toggleObj: Toggleable<T> = {
- inactive: inactiveState,
+ const toggle_obj: Toggleable<T> = {
+ inactive: inactive_state,
active: merge(base ?? {}, state.active) as T,
}
- return toggleObj
+ return toggle_obj
}
@@ -1,75 +0,0 @@
-import contactFinder from "./contactFinder"
-import contactsPopover from "./contactsPopover"
-import commandPalette from "./commandPalette"
-import editor from "./editor"
-import projectPanel from "./projectPanel"
-import search from "./search"
-import picker from "./picker"
-import workspace from "./workspace"
-import contextMenu from "./contextMenu"
-import sharedScreen from "./sharedScreen"
-import projectDiagnostics from "./projectDiagnostics"
-import contactNotification from "./contactNotification"
-import updateNotification from "./updateNotification"
-import simpleMessageNotification from "./simpleMessageNotification"
-import projectSharedNotification from "./projectSharedNotification"
-import tooltip from "./tooltip"
-import terminal from "./terminal"
-import contactList from "./contactList"
-import toolbarDropdownMenu from "./toolbarDropdownMenu"
-import incomingCallNotification from "./incomingCallNotification"
-import { ColorScheme } from "../theme/colorScheme"
-import feedback from "./feedback"
-import welcome from "./welcome"
-import copilot from "./copilot"
-import assistant from "./assistant"
-import { titlebar } from "./titlebar"
-
-export default function app(colorScheme: ColorScheme): Object {
- return {
- meta: {
- name: colorScheme.name,
- isLight: colorScheme.isLight,
- },
- commandPalette: commandPalette(colorScheme),
- contactNotification: contactNotification(colorScheme),
- projectSharedNotification: projectSharedNotification(colorScheme),
- incomingCallNotification: incomingCallNotification(colorScheme),
- picker: picker(colorScheme),
- workspace: workspace(colorScheme),
- titlebar: titlebar(colorScheme),
- copilot: copilot(colorScheme),
- welcome: welcome(colorScheme),
- contextMenu: contextMenu(colorScheme),
- editor: editor(colorScheme),
- projectDiagnostics: projectDiagnostics(colorScheme),
- projectPanel: projectPanel(colorScheme),
- contactsPopover: contactsPopover(colorScheme),
- contactFinder: contactFinder(colorScheme),
- contactList: contactList(colorScheme),
- toolbarDropdownMenu: toolbarDropdownMenu(colorScheme),
- search: search(colorScheme),
- sharedScreen: sharedScreen(colorScheme),
- updateNotification: updateNotification(colorScheme),
- simpleMessageNotification: simpleMessageNotification(colorScheme),
- tooltip: tooltip(colorScheme),
- terminal: terminal(colorScheme),
- assistant: assistant(colorScheme),
- feedback: feedback(colorScheme),
- colorScheme: {
- ...colorScheme,
- players: Object.values(colorScheme.players),
- ramps: {
- neutral: colorScheme.ramps.neutral.colors(100, "hex"),
- red: colorScheme.ramps.red.colors(100, "hex"),
- orange: colorScheme.ramps.orange.colors(100, "hex"),
- yellow: colorScheme.ramps.yellow.colors(100, "hex"),
- green: colorScheme.ramps.green.colors(100, "hex"),
- cyan: colorScheme.ramps.cyan.colors(100, "hex"),
- blue: colorScheme.ramps.blue.colors(100, "hex"),
- violet: colorScheme.ramps.violet.colors(100, "hex"),
- magenta: colorScheme.ramps.magenta.colors(100, "hex"),
- },
- },
- }
-}
@@ -1,70 +0,0 @@
-import picker from "./picker"
-import { ColorScheme } from "../theme/colorScheme"
-import { background, border, foreground, text } from "./components"
-
-export default function contactFinder(colorScheme: ColorScheme): any {
- let layer = colorScheme.middle
-
- const sideMargin = 6
- const contactButton = {
- background: background(layer, "variant"),
- color: foreground(layer, "variant"),
- iconWidth: 8,
- buttonWidth: 16,
- cornerRadius: 8,
- }
-
- const pickerStyle = picker(colorScheme)
- const pickerInput = {
- background: background(layer, "on"),
- cornerRadius: 6,
- text: text(layer, "mono"),
- placeholderText: text(layer, "mono", "on", "disabled", { size: "xs" }),
- selection: colorScheme.players[0],
- border: border(layer),
- padding: {
- bottom: 4,
- left: 8,
- right: 8,
- top: 4,
- },
- margin: {
- left: sideMargin,
- right: sideMargin,
- },
- }
-
- return {
- picker: {
- emptyContainer: {},
- item: {
- ...pickerStyle.item,
- margin: { left: sideMargin, right: sideMargin },
- },
- noMatches: pickerStyle.noMatches,
- inputEditor: pickerInput,
- emptyInputEditor: pickerInput,
- },
- rowHeight: 28,
- contactAvatar: {
- cornerRadius: 10,
- width: 18,
- },
- contactUsername: {
- padding: {
- left: 8,
- },
- },
- contactButton: {
- ...contactButton,
- hover: {
- background: background(layer, "variant", "hovered"),
- },
- },
- disabledContactButton: {
- ...contactButton,
- background: background(layer, "disabled"),
- color: foreground(layer, "disabled"),
- },
- }
-}
@@ -1,53 +0,0 @@
-import { ColorScheme } from "../theme/colorScheme"
-import { background, foreground, text } from "./components"
-import { interactive } from "../element"
-const avatarSize = 12
-const headerPadding = 8
-
-export default function contactNotification(colorScheme: ColorScheme): Object {
- let layer = colorScheme.lowest
- return {
- headerAvatar: {
- height: avatarSize,
- width: avatarSize,
- cornerRadius: 6,
- },
- headerMessage: {
- ...text(layer, "sans", { size: "xs" }),
- margin: { left: headerPadding, right: headerPadding },
- },
- headerHeight: 18,
- bodyMessage: {
- ...text(layer, "sans", { size: "xs" }),
- margin: { left: avatarSize + headerPadding, top: 6, bottom: 6 },
- },
- button: interactive({
- base: {
- ...text(layer, "sans", "on", { size: "xs" }),
- background: background(layer, "on"),
- padding: 4,
- cornerRadius: 6,
- margin: { left: 6 },
- },
-
- state: {
- hovered: {
- background: background(layer, "on", "hovered"),
- },
- },
- }),
-
- dismissButton: {
- default: {
- color: foreground(layer, "variant"),
- iconWidth: 8,
- iconHeight: 8,
- buttonWidth: 8,
- buttonHeight: 8,
- hover: {
- color: foreground(layer, "hovered"),
- },
- },
- },
- }
-}
@@ -1,16 +0,0 @@
-import { ColorScheme } from "../theme/colorScheme"
-import { background, border, text } from "./components"
-
-export default function contactsPopover(colorScheme: ColorScheme) {
- let layer = colorScheme.middle
- const sidePadding = 12
- return {
- background: background(layer),
- cornerRadius: 6,
- padding: { top: 6, bottom: 6 },
- shadow: colorScheme.popoverShadow,
- border: border(layer),
- width: 300,
- height: 400,
- }
-}
@@ -1,67 +0,0 @@
-import { ColorScheme } from "../theme/colorScheme"
-import { background, border, borderColor, text } from "./components"
-import { interactive, toggleable } from "../element"
-
-export default function contextMenu(colorScheme: ColorScheme) {
- let layer = colorScheme.middle
- return {
- background: background(layer),
- cornerRadius: 10,
- padding: 4,
- shadow: colorScheme.popoverShadow,
- border: border(layer),
- keystrokeMargin: 30,
- item: toggleable({
- base: interactive({
- base: {
- iconSpacing: 8,
- iconWidth: 14,
- padding: { left: 6, right: 6, top: 2, bottom: 2 },
- cornerRadius: 6,
- label: text(layer, "sans", { size: "sm" }),
- keystroke: {
- ...text(layer, "sans", "variant", {
- size: "sm",
- weight: "bold",
- }),
- padding: { left: 3, right: 3 },
- },
- },
- state: {
- hovered: {
- background: background(layer, "hovered"),
- label: text(layer, "sans", "hovered", { size: "sm" }),
- keystroke: {
- ...text(layer, "sans", "hovered", {
- size: "sm",
- weight: "bold",
- }),
- padding: { left: 3, right: 3 },
- },
- },
- clicked: {
- background: background(layer, "pressed"),
- },
- },
- }),
- state: {
- active: {
- default: {
- background: background(layer, "active"),
- },
- hovered: {
- background: background(layer, "hovered"),
- },
- clicked: {
- background: background(layer, "pressed"),
- },
- },
- },
- }),
-
- separator: {
- background: borderColor(layer),
- margin: { top: 2, bottom: 2 },
- },
- }
-}
@@ -1,314 +0,0 @@
-import { withOpacity } from "../theme/color"
-import { ColorScheme, Layer, StyleSets } from "../theme/colorScheme"
-import { background, border, borderColor, foreground, text } from "./components"
-import hoverPopover from "./hoverPopover"
-
-import { buildSyntax } from "../theme/syntax"
-import { interactive, toggleable } from "../element"
-
-export default function editor(colorScheme: ColorScheme) {
- const { isLight } = colorScheme
-
- let layer = colorScheme.highest
-
- const autocompleteItem = {
- cornerRadius: 6,
- padding: {
- bottom: 2,
- left: 6,
- right: 6,
- top: 2,
- },
- }
-
- function diagnostic(layer: Layer, styleSet: StyleSets) {
- return {
- textScaleFactor: 0.857,
- header: {
- border: border(layer, {
- top: true,
- }),
- },
- message: {
- text: text(layer, "sans", styleSet, "default", { size: "sm" }),
- highlightText: text(layer, "sans", styleSet, "default", {
- size: "sm",
- weight: "bold",
- }),
- },
- }
- }
-
- const syntax = buildSyntax(colorScheme)
-
- return {
- textColor: syntax.primary.color,
- background: background(layer),
- activeLineBackground: withOpacity(background(layer, "on"), 0.75),
- highlightedLineBackground: background(layer, "on"),
- // Inline autocomplete suggestions, Co-pilot suggestions, etc.
- suggestion: syntax.predictive,
- codeActions: {
- indicator: toggleable({
- base: interactive({
- base: {
- color: foreground(layer, "variant"),
- },
- state: {
- hovered: {
- color: foreground(layer, "variant", "hovered"),
- },
- clicked: {
- color: foreground(layer, "variant", "pressed"),
- },
- },
- }),
- state: {
- active: {
- default: {
- color: foreground(layer, "accent"),
- },
- hovered: {
- color: foreground(layer, "accent", "hovered"),
- },
- clicked: {
- color: foreground(layer, "accent", "pressed"),
- },
- },
- },
- }),
-
- verticalScale: 0.55,
- },
- folds: {
- iconMarginScale: 2.5,
- foldedIcon: "icons/chevron_right_8.svg",
- foldableIcon: "icons/chevron_down_8.svg",
- indicator: toggleable({
- base: interactive({
- base: {
- color: foreground(layer, "variant"),
- },
- state: {
- hovered: {
- color: foreground(layer, "on"),
- },
- clicked: {
- color: foreground(layer, "base"),
- },
- },
- }),
- state: {
- active: {
- default: {
- color: foreground(layer, "default"),
- },
- hovered: {
- color: foreground(layer, "variant"),
- },
- },
- },
- }),
- ellipses: {
- textColor: colorScheme.ramps.neutral(0.71).hex(),
- cornerRadiusFactor: 0.15,
- background: {
- // Copied from hover_popover highlight
- default: {
- color: colorScheme.ramps.neutral(0.5).alpha(0.0).hex(),
- },
-
- hovered: {
- color: colorScheme.ramps.neutral(0.5).alpha(0.5).hex(),
- },
-
- clicked: {
- color: colorScheme.ramps.neutral(0.5).alpha(0.7).hex(),
- },
- },
- },
- foldBackground: foreground(layer, "variant"),
- },
- diff: {
- deleted: isLight
- ? colorScheme.ramps.red(0.5).hex()
- : colorScheme.ramps.red(0.4).hex(),
- modified: isLight
- ? colorScheme.ramps.yellow(0.5).hex()
- : colorScheme.ramps.yellow(0.5).hex(),
- inserted: isLight
- ? colorScheme.ramps.green(0.4).hex()
- : colorScheme.ramps.green(0.5).hex(),
- removedWidthEm: 0.275,
- widthEm: 0.15,
- cornerRadius: 0.05,
- },
- /** Highlights matching occurrences of what is under the cursor
- * as well as matched brackets
- */
- documentHighlightReadBackground: withOpacity(
- foreground(layer, "accent"),
- 0.1
- ),
- documentHighlightWriteBackground: colorScheme.ramps
- .neutral(0.5)
- .alpha(0.4)
- .hex(), // TODO: This was blend * 2
- errorColor: background(layer, "negative"),
- gutterBackground: background(layer),
- gutterPaddingFactor: 3.5,
- lineNumber: withOpacity(foreground(layer), 0.35),
- lineNumberActive: foreground(layer),
- renameFade: 0.6,
- unnecessaryCodeFade: 0.5,
- selection: colorScheme.players[0],
- whitespace: colorScheme.ramps.neutral(0.5).hex(),
- guestSelections: [
- colorScheme.players[1],
- colorScheme.players[2],
- colorScheme.players[3],
- colorScheme.players[4],
- colorScheme.players[5],
- colorScheme.players[6],
- colorScheme.players[7],
- ],
- autocomplete: {
- background: background(colorScheme.middle),
- cornerRadius: 8,
- padding: 4,
- margin: {
- left: -14,
- },
- border: border(colorScheme.middle),
- shadow: colorScheme.popoverShadow,
- matchHighlight: foreground(colorScheme.middle, "accent"),
- item: autocompleteItem,
- hoveredItem: {
- ...autocompleteItem,
- matchHighlight: foreground(
- colorScheme.middle,
- "accent",
- "hovered"
- ),
- background: background(colorScheme.middle, "hovered"),
- },
- selectedItem: {
- ...autocompleteItem,
- matchHighlight: foreground(
- colorScheme.middle,
- "accent",
- "active"
- ),
- background: background(colorScheme.middle, "active"),
- },
- },
- diagnosticHeader: {
- background: background(colorScheme.middle),
- iconWidthFactor: 1.5,
- textScaleFactor: 0.857,
- border: border(colorScheme.middle, {
- bottom: true,
- top: true,
- }),
- code: {
- ...text(colorScheme.middle, "mono", { size: "sm" }),
- margin: {
- left: 10,
- },
- },
- source: {
- text: text(colorScheme.middle, "sans", {
- size: "sm",
- weight: "bold",
- }),
- },
- message: {
- highlightText: text(colorScheme.middle, "sans", {
- size: "sm",
- weight: "bold",
- }),
- text: text(colorScheme.middle, "sans", { size: "sm" }),
- },
- },
- diagnosticPathHeader: {
- background: background(colorScheme.middle),
- textScaleFactor: 0.857,
- filename: text(colorScheme.middle, "mono", { size: "sm" }),
- path: {
- ...text(colorScheme.middle, "mono", { size: "sm" }),
- margin: {
- left: 12,
- },
- },
- },
- errorDiagnostic: diagnostic(colorScheme.middle, "negative"),
- warningDiagnostic: diagnostic(colorScheme.middle, "warning"),
- informationDiagnostic: diagnostic(colorScheme.middle, "accent"),
- hintDiagnostic: diagnostic(colorScheme.middle, "warning"),
- invalidErrorDiagnostic: diagnostic(colorScheme.middle, "base"),
- invalidHintDiagnostic: diagnostic(colorScheme.middle, "base"),
- invalidInformationDiagnostic: diagnostic(colorScheme.middle, "base"),
- invalidWarningDiagnostic: diagnostic(colorScheme.middle, "base"),
- hoverPopover: hoverPopover(colorScheme),
- linkDefinition: {
- color: syntax.linkUri.color,
- underline: syntax.linkUri.underline,
- },
- jumpIcon: interactive({
- base: {
- color: foreground(layer, "on"),
- iconWidth: 20,
- buttonWidth: 20,
- cornerRadius: 6,
- padding: {
- top: 6,
- bottom: 6,
- left: 6,
- right: 6,
- },
- },
- state: {
- hovered: {
- background: background(layer, "on", "hovered"),
- },
- },
- }),
-
- scrollbar: {
- width: 12,
- minHeightFactor: 1.0,
- track: {
- border: border(layer, "variant", { left: true }),
- },
- thumb: {
- background: withOpacity(background(layer, "inverted"), 0.3),
- border: {
- width: 1,
- color: borderColor(layer, "variant"),
- top: false,
- right: true,
- left: true,
- bottom: false,
- },
- },
- git: {
- deleted: isLight
- ? withOpacity(colorScheme.ramps.red(0.5).hex(), 0.8)
- : withOpacity(colorScheme.ramps.red(0.4).hex(), 0.8),
- modified: isLight
- ? withOpacity(colorScheme.ramps.yellow(0.5).hex(), 0.8)
- : withOpacity(colorScheme.ramps.yellow(0.4).hex(), 0.8),
- inserted: isLight
- ? withOpacity(colorScheme.ramps.green(0.5).hex(), 0.8)
- : withOpacity(colorScheme.ramps.green(0.4).hex(), 0.8),
- },
- },
- compositionMark: {
- underline: {
- thickness: 1.0,
- color: borderColor(layer),
- },
- },
- syntax,
- }
-}
@@ -1,49 +0,0 @@
-import { ColorScheme } from "../theme/colorScheme"
-import { background, border, text } from "./components"
-import { interactive } from "../element"
-
-export default function feedback(colorScheme: ColorScheme) {
- let layer = colorScheme.highest
-
- return {
- submit_button: interactive({
- base: {
- ...text(layer, "mono", "on"),
- background: background(layer, "on"),
- cornerRadius: 6,
- border: border(layer, "on"),
- margin: {
- right: 4,
- },
- padding: {
- bottom: 2,
- left: 10,
- right: 10,
- top: 2,
- },
- },
- state: {
- clicked: {
- ...text(layer, "mono", "on", "pressed"),
- background: background(layer, "on", "pressed"),
- border: border(layer, "on", "pressed"),
- },
- hovered: {
- ...text(layer, "mono", "on", "hovered"),
- background: background(layer, "on", "hovered"),
- border: border(layer, "on", "hovered"),
- },
- },
- }),
- button_margin: 8,
- info_text_default: text(layer, "sans", "default", { size: "xs" }),
- link_text_default: text(layer, "sans", "default", {
- size: "xs",
- underline: true,
- }),
- link_text_hover: text(layer, "sans", "hovered", {
- size: "xs",
- underline: true,
- }),
- }
-}
@@ -1,46 +0,0 @@
-import { ColorScheme } from "../theme/colorScheme"
-import { background, border, foreground, text } from "./components"
-
-export default function HoverPopover(colorScheme: ColorScheme) {
- let layer = colorScheme.middle
- let baseContainer = {
- background: background(layer),
- cornerRadius: 8,
- padding: {
- left: 8,
- right: 8,
- top: 4,
- bottom: 4,
- },
- shadow: colorScheme.popoverShadow,
- border: border(layer),
- margin: {
- left: -8,
- },
- }
-
- return {
- container: baseContainer,
- infoContainer: {
- ...baseContainer,
- background: background(layer, "accent"),
- border: border(layer, "accent"),
- },
- warningContainer: {
- ...baseContainer,
- background: background(layer, "warning"),
- border: border(layer, "warning"),
- },
- errorContainer: {
- ...baseContainer,
- background: background(layer, "negative"),
- border: border(layer, "negative"),
- },
- blockStyle: {
- padding: { top: 4 },
- },
- prose: text(layer, "sans", { size: "sm" }),
- diagnosticSourceHighlight: { color: foreground(layer, "accent") },
- highlight: colorScheme.ramps.neutral(0.5).alpha(0.2).hex(), // TODO: blend was used here. Replace with something better
- }
-}
@@ -1,53 +0,0 @@
-import { ColorScheme } from "../theme/colorScheme"
-import { background, border, text } from "./components"
-
-export default function incomingCallNotification(
- colorScheme: ColorScheme
-): Object {
- let layer = colorScheme.middle
- const avatarSize = 48
- return {
- windowHeight: 74,
- windowWidth: 380,
- background: background(layer),
- callerContainer: {
- padding: 12,
- },
- callerAvatar: {
- height: avatarSize,
- width: avatarSize,
- cornerRadius: avatarSize / 2,
- },
- callerMetadata: {
- margin: { left: 10 },
- },
- callerUsername: {
- ...text(layer, "sans", { size: "sm", weight: "bold" }),
- margin: { top: -3 },
- },
- callerMessage: {
- ...text(layer, "sans", "variant", { size: "xs" }),
- margin: { top: -3 },
- },
- worktreeRoots: {
- ...text(layer, "sans", "variant", { size: "xs", weight: "bold" }),
- margin: { top: -3 },
- },
- buttonWidth: 96,
- acceptButton: {
- background: background(layer, "accent"),
- border: border(layer, { left: true, bottom: true }),
- ...text(layer, "sans", "positive", {
- size: "xs",
- weight: "extra_bold",
- }),
- },
- declineButton: {
- border: border(layer, { left: true }),
- ...text(layer, "sans", "negative", {
- size: "xs",
- weight: "extra_bold",
- }),
- },
- }
-}
@@ -1,13 +0,0 @@
-import { ColorScheme } from "../theme/colorScheme"
-import { background, text } from "./components"
-
-export default function projectDiagnostics(colorScheme: ColorScheme) {
- let layer = colorScheme.highest
- return {
- background: background(layer),
- tabIconSpacing: 4,
- tabIconWidth: 13,
- tabSummarySpacing: 10,
- emptyMessage: text(layer, "sans", "variant", { size: "md" }),
- }
-}
@@ -1,188 +0,0 @@
-import { ColorScheme } from "../theme/colorScheme"
-import { withOpacity } from "../theme/color"
-import {
- Border,
- TextStyle,
- background,
- border,
- foreground,
- text,
-} from "./components"
-import { interactive, toggleable } from "../element"
-import merge from "ts-deepmerge"
-export default function projectPanel(colorScheme: ColorScheme) {
- const { isLight } = colorScheme
-
- let layer = colorScheme.middle
-
- type EntryStateProps = {
- background?: string
- border?: Border
- text?: TextStyle
- iconColor?: string
- }
-
- type EntryState = {
- default: EntryStateProps
- hovered?: EntryStateProps
- clicked?: EntryStateProps
- }
-
- const entry = (unselected?: EntryState, selected?: EntryState) => {
- const git_status = {
- git: {
- modified: isLight
- ? colorScheme.ramps.yellow(0.6).hex()
- : colorScheme.ramps.yellow(0.5).hex(),
- inserted: isLight
- ? colorScheme.ramps.green(0.45).hex()
- : colorScheme.ramps.green(0.5).hex(),
- conflict: isLight
- ? colorScheme.ramps.red(0.6).hex()
- : colorScheme.ramps.red(0.5).hex(),
- },
- }
-
- const base_properties = {
- height: 22,
- background: background(layer),
- iconColor: foreground(layer, "variant"),
- iconSize: 7,
- iconSpacing: 5,
- text: text(layer, "mono", "variant", { size: "sm" }),
- status: {
- ...git_status,
- },
- }
-
- const selectedStyle: EntryState | undefined = selected
- ? selected
- : unselected
-
- const unselected_default_style = merge(
- base_properties,
- unselected?.default ?? {},
- {}
- )
- const unselected_hovered_style = merge(
- base_properties,
- unselected?.hovered ?? {},
- { background: background(layer, "variant", "hovered") }
- )
- const unselected_clicked_style = merge(
- base_properties,
- unselected?.clicked ?? {},
- { background: background(layer, "variant", "pressed") }
- )
- const selected_default_style = merge(
- base_properties,
- selectedStyle?.default ?? {},
- { background: background(layer) }
- )
- const selected_hovered_style = merge(
- base_properties,
- selectedStyle?.hovered ?? {},
- { background: background(layer, "variant", "hovered") }
- )
- const selected_clicked_style = merge(
- base_properties,
- selectedStyle?.clicked ?? {},
- { background: background(layer, "variant", "pressed") }
- )
-
- return toggleable({
- state: {
- inactive: interactive({
- state: {
- default: unselected_default_style,
- hovered: unselected_hovered_style,
- clicked: unselected_clicked_style,
- },
- }),
- active: interactive({
- state: {
- default: selected_default_style,
- hovered: selected_hovered_style,
- clicked: selected_clicked_style,
- },
- }),
- },
- })
- }
-
- const defaultEntry = entry()
-
- return {
- openProjectButton: interactive({
- base: {
- background: background(layer),
- border: border(layer, "active"),
- cornerRadius: 4,
- margin: {
- top: 16,
- left: 16,
- right: 16,
- },
- padding: {
- top: 3,
- bottom: 3,
- left: 7,
- right: 7,
- },
- ...text(layer, "sans", "default", { size: "sm" }),
- },
- state: {
- hovered: {
- ...text(layer, "sans", "default", { size: "sm" }),
- background: background(layer, "hovered"),
- border: border(layer, "active"),
- },
- clicked: {
- ...text(layer, "sans", "default", { size: "sm" }),
- background: background(layer, "pressed"),
- border: border(layer, "active"),
- },
- },
- }),
- background: background(layer),
- padding: { left: 6, right: 6, top: 0, bottom: 6 },
- indentWidth: 12,
- entry: defaultEntry,
- draggedEntry: {
- ...defaultEntry.inactive.default,
- text: text(layer, "mono", "on", { size: "sm" }),
- background: withOpacity(background(layer, "on"), 0.9),
- border: border(layer),
- },
- ignoredEntry: entry(
- {
- default: {
- text: text(layer, "mono", "disabled"),
- },
- },
- {
- default: {
- iconColor: foreground(layer, "variant"),
- },
- }
- ),
- cutEntry: entry(
- {
- default: {
- text: text(layer, "mono", "disabled"),
- },
- },
- {
- default: {
- background: background(layer, "active"),
- text: text(layer, "mono", "disabled", { size: "sm" }),
- },
- }
- ),
- filenameEditor: {
- background: background(layer, "on"),
- text: text(layer, "mono", "on", { size: "sm" }),
- selection: colorScheme.players[0],
- },
- }
-}
@@ -1,54 +0,0 @@
-import { ColorScheme } from "../theme/colorScheme"
-import { background, border, text } from "./components"
-
-export default function projectSharedNotification(
- colorScheme: ColorScheme
-): Object {
- let layer = colorScheme.middle
-
- const avatarSize = 48
- return {
- windowHeight: 74,
- windowWidth: 380,
- background: background(layer),
- ownerContainer: {
- padding: 12,
- },
- ownerAvatar: {
- height: avatarSize,
- width: avatarSize,
- cornerRadius: avatarSize / 2,
- },
- ownerMetadata: {
- margin: { left: 10 },
- },
- ownerUsername: {
- ...text(layer, "sans", { size: "sm", weight: "bold" }),
- margin: { top: -3 },
- },
- message: {
- ...text(layer, "sans", "variant", { size: "xs" }),
- margin: { top: -3 },
- },
- worktreeRoots: {
- ...text(layer, "sans", "variant", { size: "xs", weight: "bold" }),
- margin: { top: -3 },
- },
- buttonWidth: 96,
- openButton: {
- background: background(layer, "accent"),
- border: border(layer, { left: true, bottom: true }),
- ...text(layer, "sans", "accent", {
- size: "xs",
- weight: "extra_bold",
- }),
- },
- dismissButton: {
- border: border(layer, { left: true }),
- ...text(layer, "sans", "variant", {
- size: "xs",
- weight: "extra_bold",
- }),
- },
- }
-}
@@ -1,135 +0,0 @@
-import { ColorScheme } from "../theme/colorScheme"
-import { withOpacity } from "../theme/color"
-import { background, border, foreground, text } from "./components"
-import { interactive, toggleable } from "../element"
-
-export default function search(colorScheme: ColorScheme) {
- let layer = colorScheme.highest
-
- // Search input
- const editor = {
- background: background(layer),
- cornerRadius: 8,
- minWidth: 200,
- maxWidth: 500,
- placeholderText: text(layer, "mono", "disabled"),
- selection: colorScheme.players[0],
- text: text(layer, "mono", "default"),
- border: border(layer),
- margin: {
- right: 12,
- },
- padding: {
- top: 3,
- bottom: 3,
- left: 12,
- right: 8,
- },
- }
-
- const includeExcludeEditor = {
- ...editor,
- minWidth: 100,
- maxWidth: 250,
- }
-
- return {
- // TODO: Add an activeMatchBackground on the rust side to differentiate between active and inactive
- matchBackground: withOpacity(foreground(layer, "accent"), 0.4),
- optionButton: toggleable({
- base: interactive({
- base: {
- ...text(layer, "mono", "on"),
- background: background(layer, "on"),
- cornerRadius: 6,
- border: border(layer, "on"),
- margin: {
- right: 4,
- },
- padding: {
- bottom: 2,
- left: 10,
- right: 10,
- top: 2,
- },
- },
- state: {
- hovered: {
- ...text(layer, "mono", "on", "hovered"),
- background: background(layer, "on", "hovered"),
- border: border(layer, "on", "hovered"),
- },
- clicked: {
- ...text(layer, "mono", "on", "pressed"),
- background: background(layer, "on", "pressed"),
- border: border(layer, "on", "pressed"),
- },
- },
- }),
- state: {
- active: {
- default: {
- ...text(layer, "mono", "accent"),
- },
- hovered: {
- ...text(layer, "mono", "accent", "hovered"),
- },
- clicked: {
- ...text(layer, "mono", "accent", "pressed"),
- },
- },
- },
- }),
- editor,
- invalidEditor: {
- ...editor,
- border: border(layer, "negative"),
- },
- includeExcludeEditor,
- invalidIncludeExcludeEditor: {
- ...includeExcludeEditor,
- border: border(layer, "negative"),
- },
- matchIndex: {
- ...text(layer, "mono", "variant"),
- padding: {
- left: 6,
- },
- },
- optionButtonGroup: {
- padding: {
- left: 12,
- right: 12,
- },
- },
- includeExcludeInputs: {
- ...text(layer, "mono", "variant"),
- padding: {
- right: 6,
- },
- },
- resultsStatus: {
- ...text(layer, "mono", "on"),
- size: 18,
- },
- dismissButton: interactive({
- base: {
- color: foreground(layer, "variant"),
- iconWidth: 12,
- buttonWidth: 14,
- padding: {
- left: 10,
- right: 10,
- },
- },
- state: {
- hovered: {
- color: foreground(layer, "hovered"),
- },
- clicked: {
- color: foreground(layer, "pressed"),
- },
- },
- }),
- }
-}
@@ -1,9 +0,0 @@
-import { ColorScheme } from "../theme/colorScheme"
-import { background } from "./components"
-
-export default function sharedScreen(colorScheme: ColorScheme) {
- let layer = colorScheme.highest
- return {
- background: background(layer),
- }
-}
@@ -1,53 +0,0 @@
-import { ColorScheme } from "../theme/colorScheme"
-import { background, border, foreground, text } from "./components"
-import { interactive } from "../element"
-
-const headerPadding = 8
-
-export default function simpleMessageNotification(
- colorScheme: ColorScheme
-): Object {
- let layer = colorScheme.middle
- return {
- message: {
- ...text(layer, "sans", { size: "xs" }),
- margin: { left: headerPadding, right: headerPadding },
- },
- actionMessage: interactive({
- base: {
- ...text(layer, "sans", { size: "xs" }),
- border: border(layer, "active"),
- cornerRadius: 4,
- padding: {
- top: 3,
- bottom: 3,
- left: 7,
- right: 7,
- },
-
- margin: { left: headerPadding, top: 6, bottom: 6 },
- },
- state: {
- hovered: {
- ...text(layer, "sans", "default", { size: "xs" }),
- background: background(layer, "hovered"),
- border: border(layer, "active"),
- },
- },
- }),
- dismissButton: interactive({
- base: {
- color: foreground(layer),
- iconWidth: 8,
- iconHeight: 8,
- buttonWidth: 8,
- buttonHeight: 8,
- },
- state: {
- hovered: {
- color: foreground(layer, "hovered"),
- },
- },
- }),
- }
-}
@@ -1,52 +0,0 @@
-import { ColorScheme } from "../theme/colorScheme"
-
-export default function terminal(colorScheme: ColorScheme) {
- /**
- * Colors are controlled per-cell in the terminal grid.
- * Cells can be set to any of these more 'theme-capable' colors
- * or can be set directly with RGB values.
- * Here are the common interpretations of these names:
- * https://en.wikipedia.org/wiki/ANSI_escape_code#Colors
- */
- return {
- black: colorScheme.ramps.neutral(0).hex(),
- red: colorScheme.ramps.red(0.5).hex(),
- green: colorScheme.ramps.green(0.5).hex(),
- yellow: colorScheme.ramps.yellow(0.5).hex(),
- blue: colorScheme.ramps.blue(0.5).hex(),
- magenta: colorScheme.ramps.magenta(0.5).hex(),
- cyan: colorScheme.ramps.cyan(0.5).hex(),
- white: colorScheme.ramps.neutral(1).hex(),
- brightBlack: colorScheme.ramps.neutral(0.4).hex(),
- brightRed: colorScheme.ramps.red(0.25).hex(),
- brightGreen: colorScheme.ramps.green(0.25).hex(),
- brightYellow: colorScheme.ramps.yellow(0.25).hex(),
- brightBlue: colorScheme.ramps.blue(0.25).hex(),
- brightMagenta: colorScheme.ramps.magenta(0.25).hex(),
- brightCyan: colorScheme.ramps.cyan(0.25).hex(),
- brightWhite: colorScheme.ramps.neutral(1).hex(),
- /**
- * Default color for characters
- */
- foreground: colorScheme.ramps.neutral(1).hex(),
- /**
- * Default color for the rectangle background of a cell
- */
- background: colorScheme.ramps.neutral(0).hex(),
- modalBackground: colorScheme.ramps.neutral(0.1).hex(),
- /**
- * Default color for the cursor
- */
- cursor: colorScheme.players[0].cursor,
- dimBlack: colorScheme.ramps.neutral(1).hex(),
- dimRed: colorScheme.ramps.red(0.75).hex(),
- dimGreen: colorScheme.ramps.green(0.75).hex(),
- dimYellow: colorScheme.ramps.yellow(0.75).hex(),
- dimBlue: colorScheme.ramps.blue(0.75).hex(),
- dimMagenta: colorScheme.ramps.magenta(0.75).hex(),
- dimCyan: colorScheme.ramps.cyan(0.75).hex(),
- dimWhite: colorScheme.ramps.neutral(0.6).hex(),
- brightForeground: colorScheme.ramps.neutral(1).hex(),
- dimForeground: colorScheme.ramps.neutral(0).hex(),
- }
-}
@@ -1,47 +0,0 @@
-import merge from "ts-deepmerge"
-
-type ToggleState = "inactive" | "active"
-
-type Toggleable<T> = Record<ToggleState, T>
-
-const NO_INACTIVE_OR_BASE_ERROR =
- "A toggleable object must have an inactive state, or a base property."
-const NO_ACTIVE_ERROR = "A toggleable object must have an active state."
-
-interface ToggleableProps<T> {
- base?: T
- state: Partial<Record<ToggleState, T>>
-}
-
-/**
- * Helper function for creating Toggleable objects.
- * @template T The type of the object being toggled.
- * @param props Object containing the base (inactive) state and state modifications to create the active state.
- * @returns A Toggleable object containing both the inactive and active states.
- * @example
- * ```
- * toggleable({
- * base: { background: "#000000", text: "#CCCCCC" },
- * state: { active: { text: "#CCCCCC" } },
- * })
- * ```
- */
-export function toggleable<T extends object>(
- props: ToggleableProps<T>
-): Toggleable<T> {
- const { base, state } = props
-
- if (!base && !state.inactive) throw new Error(NO_INACTIVE_OR_BASE_ERROR)
- if (!state.active) throw new Error(NO_ACTIVE_ERROR)
-
- const inactiveState = base
- ? ((state.inactive ? merge(base, state.inactive) : base) as T)
- : (state.inactive as T)
-
- const toggleObj: Toggleable<T> = {
- inactive: inactiveState,
- active: merge(base ?? {}, state.active) as T,
- }
-
- return toggleObj
-}
@@ -1,64 +0,0 @@
-import { ColorScheme } from "../theme/colorScheme"
-import { background, border, text } from "./components"
-import { interactive, toggleable } from "../element"
-export default function dropdownMenu(colorScheme: ColorScheme) {
- let layer = colorScheme.middle
-
- return {
- rowHeight: 30,
- background: background(layer),
- border: border(layer),
- shadow: colorScheme.popoverShadow,
- header: interactive({
- base: {
- ...text(layer, "sans", { size: "sm" }),
- secondaryText: text(layer, "sans", {
- size: "sm",
- color: "#aaaaaa",
- }),
- secondaryTextSpacing: 10,
- padding: { left: 8, right: 8, top: 2, bottom: 2 },
- cornerRadius: 6,
- background: background(layer, "on"),
- },
- state: {
- hovered: {
- background: background(layer, "hovered"),
- },
- clicked: {
- background: background(layer, "pressed"),
- },
- },
- }),
- sectionHeader: {
- ...text(layer, "sans", { size: "sm" }),
- padding: { left: 8, right: 8, top: 8, bottom: 8 },
- },
- item: toggleable({
- base: interactive({
- base: {
- ...text(layer, "sans", { size: "sm" }),
- secondaryTextSpacing: 10,
- secondaryText: text(layer, "sans", { size: "sm" }),
- padding: { left: 18, right: 18, top: 2, bottom: 2 },
- },
- state: {
- hovered: {
- background: background(layer, "hovered"),
- ...text(layer, "sans", "hovered", { size: "sm" }),
- },
- },
- }),
- state: {
- active: {
- default: {
- background: background(layer, "active"),
- },
- hovered: {
- background: background(layer, "hovered"),
- },
- },
- },
- }),
- }
-}
@@ -1,23 +0,0 @@
-import { ColorScheme } from "../theme/colorScheme"
-import { background, border, text } from "./components"
-
-export default function tooltip(colorScheme: ColorScheme) {
- let layer = colorScheme.middle
- return {
- background: background(layer),
- border: border(layer),
- padding: { top: 4, bottom: 4, left: 8, right: 8 },
- margin: { top: 6, left: 6 },
- shadow: colorScheme.popoverShadow,
- cornerRadius: 6,
- text: text(layer, "sans", { size: "xs" }),
- keystroke: {
- background: background(layer, "on"),
- cornerRadius: 4,
- margin: { left: 6 },
- padding: { left: 4, right: 4 },
- ...text(layer, "mono", "on", { size: "xs", weight: "bold" }),
- },
- maxTextWidth: 200,
- }
-}
@@ -1,40 +0,0 @@
-import { ColorScheme } from "../theme/colorScheme"
-import { foreground, text } from "./components"
-import { interactive } from "../element"
-
-const headerPadding = 8
-
-export default function updateNotification(colorScheme: ColorScheme): Object {
- let layer = colorScheme.middle
- return {
- message: {
- ...text(layer, "sans", { size: "xs" }),
- margin: { left: headerPadding, right: headerPadding },
- },
- actionMessage: interactive({
- base: {
- ...text(layer, "sans", { size: "xs" }),
- margin: { left: headerPadding, top: 6, bottom: 6 },
- },
- state: {
- hovered: {
- color: foreground(layer, "hovered"),
- },
- },
- }),
- dismissButton: interactive({
- base: {
- color: foreground(layer),
- iconWidth: 8,
- iconHeight: 8,
- buttonWidth: 8,
- buttonHeight: 8,
- },
- state: {
- hovered: {
- color: foreground(layer, "hovered"),
- },
- },
- }),
- }
-}
@@ -1,134 +0,0 @@
-import { ColorScheme } from "../theme/colorScheme"
-import { withOpacity } from "../theme/color"
-import {
- border,
- background,
- foreground,
- text,
- TextProperties,
- svg,
-} from "./components"
-import { interactive } from "../element"
-
-export default function welcome(colorScheme: ColorScheme) {
- let layer = colorScheme.highest
-
- let checkboxBase = {
- cornerRadius: 4,
- padding: {
- left: 3,
- right: 3,
- top: 3,
- bottom: 3,
- },
- // shadow: colorScheme.popoverShadow,
- border: border(layer),
- margin: {
- right: 8,
- top: 5,
- bottom: 5,
- },
- }
-
- let interactive_text_size: TextProperties = { size: "sm" }
-
- return {
- pageWidth: 320,
- logo: svg(foreground(layer, "default"), "icons/logo_96.svg", 64, 64),
- logoSubheading: {
- ...text(layer, "sans", "variant", { size: "md" }),
- margin: {
- top: 10,
- bottom: 7,
- },
- },
- buttonGroup: {
- margin: {
- top: 8,
- bottom: 16,
- },
- },
- headingGroup: {
- margin: {
- top: 8,
- bottom: 12,
- },
- },
- checkboxGroup: {
- border: border(layer, "variant"),
- background: withOpacity(background(layer, "hovered"), 0.25),
- cornerRadius: 4,
- padding: {
- left: 12,
- top: 2,
- bottom: 2,
- },
- },
- button: interactive({
- base: {
- background: background(layer),
- border: border(layer, "active"),
- cornerRadius: 4,
- margin: {
- top: 4,
- bottom: 4,
- },
- padding: {
- top: 3,
- bottom: 3,
- left: 7,
- right: 7,
- },
- ...text(layer, "sans", "default", interactive_text_size),
- },
- state: {
- hovered: {
- ...text(layer, "sans", "default", interactive_text_size),
- background: background(layer, "hovered"),
- },
- },
- }),
-
- usageNote: {
- ...text(layer, "sans", "variant", { size: "2xs" }),
- padding: {
- top: -4,
- },
- },
- checkboxContainer: {
- margin: {
- top: 4,
- },
- padding: {
- bottom: 8,
- },
- },
- checkbox: {
- label: {
- ...text(layer, "sans", interactive_text_size),
- // Also supports margin, container, border, etc.
- },
- icon: svg(foreground(layer, "on"), "icons/check_12.svg", 12, 12),
- default: {
- ...checkboxBase,
- background: background(layer, "default"),
- border: border(layer, "active"),
- },
- checked: {
- ...checkboxBase,
- background: background(layer, "hovered"),
- border: border(layer, "active"),
- },
- hovered: {
- ...checkboxBase,
- background: background(layer, "hovered"),
- border: border(layer, "active"),
- },
- hoveredAndChecked: {
- ...checkboxBase,
- background: background(layer, "hovered"),
- border: border(layer, "active"),
- },
- },
- }
-}
@@ -1,200 +0,0 @@
-import { ColorScheme } from "../theme/colorScheme"
-import { withOpacity } from "../theme/color"
-import {
- background,
- border,
- borderColor,
- foreground,
- svg,
- text,
-} from "./components"
-import statusBar from "./statusBar"
-import tabBar from "./tabBar"
-import { interactive } from "../element"
-
-import { titlebar } from "./titlebar"
-export default function workspace(colorScheme: ColorScheme) {
- const layer = colorScheme.lowest
- const isLight = colorScheme.isLight
-
- return {
- background: background(colorScheme.lowest),
- blankPane: {
- logoContainer: {
- width: 256,
- height: 256,
- },
- logo: svg(
- withOpacity("#000000", colorScheme.isLight ? 0.6 : 0.8),
- "icons/logo_96.svg",
- 256,
- 256
- ),
-
- logoShadow: svg(
- withOpacity(
- colorScheme.isLight
- ? "#FFFFFF"
- : colorScheme.lowest.base.default.background,
- colorScheme.isLight ? 1 : 0.6
- ),
- "icons/logo_96.svg",
- 256,
- 256
- ),
- keyboardHints: {
- margin: {
- top: 96,
- },
- cornerRadius: 4,
- },
- keyboardHint: interactive({
- base: {
- ...text(layer, "sans", "variant", { size: "sm" }),
- padding: {
- top: 3,
- left: 8,
- right: 8,
- bottom: 3,
- },
- cornerRadius: 8,
- },
- state: {
- hovered: {
- ...text(layer, "sans", "active", { size: "sm" }),
- },
- },
- }),
-
- keyboardHintWidth: 320,
- },
- joiningProjectAvatar: {
- cornerRadius: 40,
- width: 80,
- },
- joiningProjectMessage: {
- padding: 12,
- ...text(layer, "sans", { size: "lg" }),
- },
- externalLocationMessage: {
- background: background(colorScheme.middle, "accent"),
- border: border(colorScheme.middle, "accent"),
- cornerRadius: 6,
- padding: 12,
- margin: { bottom: 8, right: 8 },
- ...text(colorScheme.middle, "sans", "accent", { size: "xs" }),
- },
- leaderBorderOpacity: 0.7,
- leaderBorderWidth: 2.0,
- tabBar: tabBar(colorScheme),
- modal: {
- margin: {
- bottom: 52,
- top: 52,
- },
- cursor: "Arrow",
- },
- zoomedBackground: {
- cursor: "Arrow",
- background: isLight
- ? withOpacity(background(colorScheme.lowest), 0.8)
- : withOpacity(background(colorScheme.highest), 0.6),
- },
- zoomedPaneForeground: {
- margin: 16,
- shadow: colorScheme.modalShadow,
- border: border(colorScheme.lowest, { overlay: true }),
- },
- zoomedPanelForeground: {
- margin: 16,
- border: border(colorScheme.lowest, { overlay: true }),
- },
- dock: {
- left: {
- border: border(layer, { right: true }),
- },
- bottom: {
- border: border(layer, { top: true }),
- },
- right: {
- border: border(layer, { left: true }),
- },
- },
- paneDivider: {
- color: borderColor(layer),
- width: 1,
- },
- statusBar: statusBar(colorScheme),
- titlebar: titlebar(colorScheme),
- toolbar: {
- height: 34,
- background: background(colorScheme.highest),
- border: border(colorScheme.highest, { bottom: true }),
- itemSpacing: 8,
- navButton: interactive({
- base: {
- color: foreground(colorScheme.highest, "on"),
- iconWidth: 12,
- buttonWidth: 24,
- cornerRadius: 6,
- },
- state: {
- hovered: {
- color: foreground(colorScheme.highest, "on", "hovered"),
- background: background(
- colorScheme.highest,
- "on",
- "hovered"
- ),
- },
- disabled: {
- color: foreground(
- colorScheme.highest,
- "on",
- "disabled"
- ),
- },
- },
- }),
- padding: { left: 8, right: 8, top: 4, bottom: 4 },
- },
- breadcrumbHeight: 24,
- breadcrumbs: interactive({
- base: {
- ...text(colorScheme.highest, "sans", "variant"),
- cornerRadius: 6,
- padding: {
- left: 6,
- right: 6,
- },
- },
- state: {
- hovered: {
- color: foreground(colorScheme.highest, "on", "hovered"),
- background: background(
- colorScheme.highest,
- "on",
- "hovered"
- ),
- },
- },
- }),
- disconnectedOverlay: {
- ...text(layer, "sans"),
- background: withOpacity(background(layer), 0.8),
- },
- notification: {
- margin: { top: 10 },
- background: background(colorScheme.middle),
- cornerRadius: 6,
- padding: 12,
- border: border(colorScheme.middle),
- shadow: colorScheme.popoverShadow,
- },
- notifications: {
- width: 400,
- margin: { right: 10, bottom: 10 },
- },
- dropTargetOverlayColor: withOpacity(foreground(layer, "variant"), 0.5),
- }
-}
@@ -0,0 +1,75 @@
+import contact_finder from "./contact_finder"
+import contacts_popover from "./contacts_popover"
+import command_palette from "./command_palette"
+import project_panel from "./project_panel"
+import search from "./search"
+import picker from "./picker"
+import workspace from "./workspace"
+import context_menu from "./context_menu"
+import shared_screen from "./shared_screen"
+import project_diagnostics from "./project_diagnostics"
+import contact_notification from "./contact_notification"
+import update_notification from "./update_notification"
+import simple_message_notification from "./simple_message_notification"
+import project_shared_notification from "./project_shared_notification"
+import tooltip from "./tooltip"
+import terminal from "./terminal"
+import contact_list from "./contact_list"
+import toolbar_dropdown_menu from "./toolbar_dropdown_menu"
+import incoming_call_notification from "./incoming_call_notification"
+import { ColorScheme } from "../theme/color_scheme"
+import welcome from "./welcome"
+import copilot from "./copilot"
+import assistant from "./assistant"
+import { titlebar } from "./titlebar"
+import editor from "./editor"
+import feedback from "./feedback"
+
+export default function app(theme: ColorScheme): any {
+ return {
+ meta: {
+ name: theme.name,
+ is_light: theme.is_light,
+ },
+ command_palette: command_palette(theme),
+ contact_notification: contact_notification(theme),
+ project_shared_notification: project_shared_notification(theme),
+ incoming_call_notification: incoming_call_notification(theme),
+ picker: picker(theme),
+ workspace: workspace(theme),
+ titlebar: titlebar(theme),
+ copilot: copilot(theme),
+ welcome: welcome(theme),
+ context_menu: context_menu(theme),
+ editor: editor(theme),
+ project_diagnostics: project_diagnostics(theme),
+ project_panel: project_panel(theme),
+ contacts_popover: contacts_popover(theme),
+ contact_finder: contact_finder(theme),
+ contact_list: contact_list(theme),
+ toolbar_dropdown_menu: toolbar_dropdown_menu(theme),
+ search: search(theme),
+ shared_screen: shared_screen(theme),
+ update_notification: update_notification(theme),
+ simple_message_notification: simple_message_notification(theme),
+ tooltip: tooltip(theme),
+ terminal: terminal(theme),
+ assistant: assistant(theme),
+ feedback: feedback(theme),
+ color_scheme: {
+ ...theme,
+ players: Object.values(theme.players),
+ ramps: {
+ neutral: theme.ramps.neutral.colors(100, "hex"),
+ red: theme.ramps.red.colors(100, "hex"),
+ orange: theme.ramps.orange.colors(100, "hex"),
+ yellow: theme.ramps.yellow.colors(100, "hex"),
+ green: theme.ramps.green.colors(100, "hex"),
+ cyan: theme.ramps.cyan.colors(100, "hex"),
+ blue: theme.ramps.blue.colors(100, "hex"),
+ violet: theme.ramps.violet.colors(100, "hex"),
+ magenta: theme.ramps.magenta.colors(100, "hex"),
+ },
+ },
+ }
+}
@@ -1,23 +1,21 @@
-import { ColorScheme } from "../theme/colorScheme"
+import { ColorScheme } from "../theme/color_scheme"
import { text, border, background, foreground } from "./components"
-import editor from "./editor"
import { interactive } from "../element"
-export default function assistant(colorScheme: ColorScheme) {
- const layer = colorScheme.highest
+export default function assistant(theme: ColorScheme): any {
return {
container: {
- background: editor(colorScheme).background,
+ background: background(theme.highest),
padding: { left: 12 },
},
- messageHeader: {
+ message_header: {
margin: { bottom: 6, top: 6 },
- background: editor(colorScheme).background,
+ background: background(theme.highest),
},
- hamburgerButton: interactive({
+ hamburger_button: interactive({
base: {
icon: {
- color: foreground(layer, "variant"),
+ color: foreground(theme.highest, "variant"),
asset: "icons/hamburger_15.svg",
dimensions: {
width: 15,
@@ -31,15 +29,15 @@ export default function assistant(colorScheme: ColorScheme) {
state: {
hovered: {
icon: {
- color: foreground(layer, "hovered"),
+ color: foreground(theme.highest, "hovered"),
},
},
},
}),
- splitButton: interactive({
+ split_button: interactive({
base: {
icon: {
- color: foreground(layer, "variant"),
+ color: foreground(theme.highest, "variant"),
asset: "icons/split_message_15.svg",
dimensions: {
width: 15,
@@ -53,15 +51,15 @@ export default function assistant(colorScheme: ColorScheme) {
state: {
hovered: {
icon: {
- color: foreground(layer, "hovered"),
+ color: foreground(theme.highest, "hovered"),
},
},
},
}),
- quoteButton: interactive({
+ quote_button: interactive({
base: {
icon: {
- color: foreground(layer, "variant"),
+ color: foreground(theme.highest, "variant"),
asset: "icons/quote_15.svg",
dimensions: {
width: 15,
@@ -75,15 +73,15 @@ export default function assistant(colorScheme: ColorScheme) {
state: {
hovered: {
icon: {
- color: foreground(layer, "hovered"),
+ color: foreground(theme.highest, "hovered"),
},
},
},
}),
- assistButton: interactive({
+ assist_button: interactive({
base: {
icon: {
- color: foreground(layer, "variant"),
+ color: foreground(theme.highest, "variant"),
asset: "icons/assist_15.svg",
dimensions: {
width: 15,
@@ -97,15 +95,15 @@ export default function assistant(colorScheme: ColorScheme) {
state: {
hovered: {
icon: {
- color: foreground(layer, "hovered"),
+ color: foreground(theme.highest, "hovered"),
},
},
},
}),
- zoomInButton: interactive({
+ zoom_in_button: interactive({
base: {
icon: {
- color: foreground(layer, "variant"),
+ color: foreground(theme.highest, "variant"),
asset: "icons/maximize_8.svg",
dimensions: {
width: 12,
@@ -119,15 +117,15 @@ export default function assistant(colorScheme: ColorScheme) {
state: {
hovered: {
icon: {
- color: foreground(layer, "hovered"),
+ color: foreground(theme.highest, "hovered"),
},
},
},
}),
- zoomOutButton: interactive({
+ zoom_out_button: interactive({
base: {
icon: {
- color: foreground(layer, "variant"),
+ color: foreground(theme.highest, "variant"),
asset: "icons/minimize_8.svg",
dimensions: {
width: 12,
@@ -141,15 +139,15 @@ export default function assistant(colorScheme: ColorScheme) {
state: {
hovered: {
icon: {
- color: foreground(layer, "hovered"),
+ color: foreground(theme.highest, "hovered"),
},
},
},
}),
- plusButton: interactive({
+ plus_button: interactive({
base: {
icon: {
- color: foreground(layer, "variant"),
+ color: foreground(theme.highest, "variant"),
asset: "icons/plus_12.svg",
dimensions: {
width: 12,
@@ -163,109 +161,109 @@ export default function assistant(colorScheme: ColorScheme) {
state: {
hovered: {
icon: {
- color: foreground(layer, "hovered"),
+ color: foreground(theme.highest, "hovered"),
},
},
},
}),
title: {
- ...text(layer, "sans", "default", { size: "sm" }),
+ ...text(theme.highest, "sans", "default", { size: "sm" }),
},
- savedConversation: {
+ saved_conversation: {
container: interactive({
base: {
- background: background(layer, "on"),
+ background: background(theme.highest, "on"),
padding: { top: 4, bottom: 4 },
},
state: {
hovered: {
- background: background(layer, "on", "hovered"),
+ background: background(theme.highest, "on", "hovered"),
},
},
}),
- savedAt: {
+ saved_at: {
margin: { left: 8 },
- ...text(layer, "sans", "default", { size: "xs" }),
+ ...text(theme.highest, "sans", "default", { size: "xs" }),
},
title: {
margin: { left: 16 },
- ...text(layer, "sans", "default", {
+ ...text(theme.highest, "sans", "default", {
size: "sm",
weight: "bold",
}),
},
},
- userSender: {
+ user_sender: {
default: {
- ...text(layer, "sans", "default", {
+ ...text(theme.highest, "sans", "default", {
size: "sm",
weight: "bold",
}),
},
},
- assistantSender: {
+ assistant_sender: {
default: {
- ...text(layer, "sans", "accent", {
+ ...text(theme.highest, "sans", "accent", {
size: "sm",
weight: "bold",
}),
},
},
- systemSender: {
+ system_sender: {
default: {
- ...text(layer, "sans", "variant", {
+ ...text(theme.highest, "sans", "variant", {
size: "sm",
weight: "bold",
}),
},
},
- sentAt: {
+ sent_at: {
margin: { top: 2, left: 8 },
- ...text(layer, "sans", "default", { size: "2xs" }),
+ ...text(theme.highest, "sans", "default", { size: "2xs" }),
},
model: interactive({
base: {
- background: background(layer, "on"),
+ background: background(theme.highest, "on"),
margin: { left: 12, right: 12, top: 12 },
padding: 4,
- cornerRadius: 4,
- ...text(layer, "sans", "default", { size: "xs" }),
+ corner_radius: 4,
+ ...text(theme.highest, "sans", "default", { size: "xs" }),
},
state: {
hovered: {
- background: background(layer, "on", "hovered"),
- border: border(layer, "on", { overlay: true }),
+ background: background(theme.highest, "on", "hovered"),
+ border: border(theme.highest, "on", { overlay: true }),
},
},
}),
- remainingTokens: {
- background: background(layer, "on"),
+ remaining_tokens: {
+ background: background(theme.highest, "on"),
margin: { top: 12, right: 24 },
padding: 4,
- cornerRadius: 4,
- ...text(layer, "sans", "positive", { size: "xs" }),
+ corner_radius: 4,
+ ...text(theme.highest, "sans", "positive", { size: "xs" }),
},
- noRemainingTokens: {
- background: background(layer, "on"),
+ no_remaining_tokens: {
+ background: background(theme.highest, "on"),
margin: { top: 12, right: 24 },
padding: 4,
- cornerRadius: 4,
- ...text(layer, "sans", "negative", { size: "xs" }),
+ corner_radius: 4,
+ ...text(theme.highest, "sans", "negative", { size: "xs" }),
},
- errorIcon: {
+ error_icon: {
margin: { left: 8 },
- color: foreground(layer, "negative"),
+ color: foreground(theme.highest, "negative"),
width: 12,
},
- apiKeyEditor: {
- background: background(layer, "on"),
- cornerRadius: 6,
- text: text(layer, "mono", "on"),
- placeholderText: text(layer, "mono", "on", "disabled", {
+ api_key_editor: {
+ background: background(theme.highest, "on"),
+ corner_radius: 6,
+ text: text(theme.highest, "mono", "on"),
+ placeholder_text: text(theme.highest, "mono", "on", "disabled", {
size: "xs",
}),
- selection: colorScheme.players[0],
- border: border(layer, "on"),
+ selection: theme.players[0],
+ border: border(theme.highest, "on"),
padding: {
bottom: 4,
left: 8,
@@ -273,9 +271,9 @@ export default function assistant(colorScheme: ColorScheme) {
top: 4,
},
},
- apiKeyPrompt: {
+ api_key_prompt: {
padding: 10,
- ...text(layer, "sans", "default", { size: "xs" }),
+ ...text(theme.highest, "sans", "default", { size: "xs" }),
},
}
}
@@ -1,16 +1,16 @@
-import { ColorScheme } from "../theme/colorScheme"
-import { withOpacity } from "../theme/color"
+import { ColorScheme } from "../theme/color_scheme"
+import { with_opacity } from "../theme/color"
import { text, background } from "./components"
import { toggleable } from "../element"
-export default function commandPalette(colorScheme: ColorScheme) {
- let layer = colorScheme.highest
-
+export default function command_palette(theme: ColorScheme): any {
const key = toggleable({
base: {
- text: text(layer, "mono", "variant", "default", { size: "xs" }),
- cornerRadius: 2,
- background: background(layer, "on"),
+ text: text(theme.highest, "mono", "variant", "default", {
+ size: "xs",
+ }),
+ corner_radius: 2,
+ background: background(theme.highest, "on"),
padding: {
top: 1,
bottom: 1,
@@ -25,14 +25,16 @@ export default function commandPalette(colorScheme: ColorScheme) {
},
state: {
active: {
- text: text(layer, "mono", "on", "default", { size: "xs" }),
- background: withOpacity(background(layer, "on"), 0.2),
+ text: text(theme.highest, "mono", "on", "default", {
+ size: "xs",
+ }),
+ background: with_opacity(background(theme.highest, "on"), 0.2),
},
},
})
return {
- keystrokeSpacing: 8,
+ keystroke_spacing: 8,
// TODO: This should be a Toggle<ContainedText> on the rust side so we don't have to do this
key: {
inactive: { ...key.inactive },
@@ -1,7 +1,7 @@
-import { fontFamilies, fontSizes, FontWeight } from "../common"
-import { Layer, Styles, StyleSets, Style } from "../theme/colorScheme"
+import { font_families, font_sizes, FontWeight } from "../common"
+import { Layer, Styles, StyleSets, Style } from "../theme/color_scheme"
-function isStyleSet(key: any): key is StyleSets {
+function is_style_set(key: any): key is StyleSets {
return [
"base",
"variant",
@@ -13,7 +13,7 @@ function isStyleSet(key: any): key is StyleSets {
].includes(key)
}
-function isStyle(key: any): key is Styles {
+function is_style(key: any): key is Styles {
return [
"default",
"active",
@@ -23,78 +23,70 @@ function isStyle(key: any): key is Styles {
"inverted",
].includes(key)
}
-function getStyle(
+function get_style(
layer: Layer,
- possibleStyleSetOrStyle?: any,
- possibleStyle?: any
+ possible_style_set_or_style?: any,
+ possible_style?: any
): Style {
- let styleSet: StyleSets = "base"
+ let style_set: StyleSets = "base"
let style: Styles = "default"
- if (isStyleSet(possibleStyleSetOrStyle)) {
- styleSet = possibleStyleSetOrStyle
- } else if (isStyle(possibleStyleSetOrStyle)) {
- style = possibleStyleSetOrStyle
+ if (is_style_set(possible_style_set_or_style)) {
+ style_set = possible_style_set_or_style
+ } else if (is_style(possible_style_set_or_style)) {
+ style = possible_style_set_or_style
}
- if (isStyle(possibleStyle)) {
- style = possibleStyle
+ if (is_style(possible_style)) {
+ style = possible_style
}
- return layer[styleSet][style]
+ return layer[style_set][style]
}
export function background(layer: Layer, style?: Styles): string
export function background(
layer: Layer,
- styleSet?: StyleSets,
+ style_set?: StyleSets,
style?: Styles
): string
export function background(
layer: Layer,
- styleSetOrStyles?: StyleSets | Styles,
+ style_set_or_styles?: StyleSets | Styles,
style?: Styles
): string {
- return getStyle(layer, styleSetOrStyles, style).background
+ return get_style(layer, style_set_or_styles, style).background
}
-export function borderColor(layer: Layer, style?: Styles): string
-export function borderColor(
+export function border_color(layer: Layer, style?: Styles): string
+export function border_color(
layer: Layer,
- styleSet?: StyleSets,
+ style_set?: StyleSets,
style?: Styles
): string
-export function borderColor(
+export function border_color(
layer: Layer,
- styleSetOrStyles?: StyleSets | Styles,
+ style_set_or_styles?: StyleSets | Styles,
style?: Styles
): string {
- return getStyle(layer, styleSetOrStyles, style).border
+ return get_style(layer, style_set_or_styles, style).border
}
export function foreground(layer: Layer, style?: Styles): string
export function foreground(
layer: Layer,
- styleSet?: StyleSets,
+ style_set?: StyleSets,
style?: Styles
): string
export function foreground(
layer: Layer,
- styleSetOrStyles?: StyleSets | Styles,
+ style_set_or_styles?: StyleSets | Styles,
style?: Styles
): string {
- return getStyle(layer, styleSetOrStyles, style).foreground
-}
-
-interface Text extends Object {
- family: keyof typeof fontFamilies
- color: string
- size: number
- weight?: FontWeight
- underline?: boolean
+ return get_style(layer, style_set_or_styles, style).foreground
}
export interface TextStyle extends Object {
- family: keyof typeof fontFamilies
+ family: keyof typeof font_families
color: string
size: number
weight?: FontWeight
@@ -102,7 +94,7 @@ export interface TextStyle extends Object {
}
export interface TextProperties {
- size?: keyof typeof fontSizes
+ size?: keyof typeof font_sizes
weight?: FontWeight
underline?: boolean
color?: string
@@ -182,49 +174,53 @@ interface FontFeatures {
export function text(
layer: Layer,
- fontFamily: keyof typeof fontFamilies,
- styleSet: StyleSets,
+ font_family: keyof typeof font_families,
+ style_set: StyleSets,
style: Styles,
properties?: TextProperties
-): Text
+): TextStyle
export function text(
layer: Layer,
- fontFamily: keyof typeof fontFamilies,
- styleSet: StyleSets,
+ font_family: keyof typeof font_families,
+ style_set: StyleSets,
properties?: TextProperties
-): Text
+): TextStyle
export function text(
layer: Layer,
- fontFamily: keyof typeof fontFamilies,
+ font_family: keyof typeof font_families,
style: Styles,
properties?: TextProperties
-): Text
+): TextStyle
export function text(
layer: Layer,
- fontFamily: keyof typeof fontFamilies,
+ font_family: keyof typeof font_families,
properties?: TextProperties
-): Text
+): TextStyle
export function text(
layer: Layer,
- fontFamily: keyof typeof fontFamilies,
- styleSetStyleOrProperties?: StyleSets | Styles | TextProperties,
- styleOrProperties?: Styles | TextProperties,
+ font_family: keyof typeof font_families,
+ style_set_style_or_properties?: StyleSets | Styles | TextProperties,
+ style_or_properties?: Styles | TextProperties,
properties?: TextProperties
) {
- let style = getStyle(layer, styleSetStyleOrProperties, styleOrProperties)
+ const style = get_style(
+ layer,
+ style_set_style_or_properties,
+ style_or_properties
+ )
- if (typeof styleSetStyleOrProperties === "object") {
- properties = styleSetStyleOrProperties
+ if (typeof style_set_style_or_properties === "object") {
+ properties = style_set_style_or_properties
}
- if (typeof styleOrProperties === "object") {
- properties = styleOrProperties
+ if (typeof style_or_properties === "object") {
+ properties = style_or_properties
}
- let size = fontSizes[properties?.size || "sm"]
- let color = properties?.color || style.foreground
+ const size = font_sizes[properties?.size || "sm"]
+ const color = properties?.color || style.foreground
return {
- family: fontFamilies[fontFamily],
+ family: font_families[font_family],
...properties,
color,
size,
@@ -252,13 +248,13 @@ export interface BorderProperties {
export function border(
layer: Layer,
- styleSet: StyleSets,
+ style_set: StyleSets,
style: Styles,
properties?: BorderProperties
): Border
export function border(
layer: Layer,
- styleSet: StyleSets,
+ style_set: StyleSets,
properties?: BorderProperties
): Border
export function border(
@@ -269,17 +265,17 @@ export function border(
export function border(layer: Layer, properties?: BorderProperties): Border
export function border(
layer: Layer,
- styleSetStyleOrProperties?: StyleSets | Styles | BorderProperties,
- styleOrProperties?: Styles | BorderProperties,
+ style_set_or_properties?: StyleSets | Styles | BorderProperties,
+ style_or_properties?: Styles | BorderProperties,
properties?: BorderProperties
): Border {
- let style = getStyle(layer, styleSetStyleOrProperties, styleOrProperties)
+ const style = get_style(layer, style_set_or_properties, style_or_properties)
- if (typeof styleSetStyleOrProperties === "object") {
- properties = styleSetStyleOrProperties
+ if (typeof style_set_or_properties === "object") {
+ properties = style_set_or_properties
}
- if (typeof styleOrProperties === "object") {
- properties = styleOrProperties
+ if (typeof style_or_properties === "object") {
+ properties = style_or_properties
}
return {
@@ -291,9 +287,9 @@ export function border(
export function svg(
color: string,
- asset: String,
- width: Number,
- height: Number
+ asset: string,
+ width: number,
+ height: number
) {
return {
color,
@@ -0,0 +1,70 @@
+import picker from "./picker"
+import { ColorScheme } from "../theme/color_scheme"
+import { background, border, foreground, text } from "./components"
+
+export default function contact_finder(theme: ColorScheme): any {
+ const side_margin = 6
+ const contact_button = {
+ background: background(theme.middle, "variant"),
+ color: foreground(theme.middle, "variant"),
+ icon_width: 8,
+ button_width: 16,
+ corner_radius: 8,
+ }
+
+ const picker_style = picker(theme)
+ const picker_input = {
+ background: background(theme.middle, "on"),
+ corner_radius: 6,
+ text: text(theme.middle, "mono"),
+ placeholder_text: text(theme.middle, "mono", "on", "disabled", {
+ size: "xs",
+ }),
+ selection: theme.players[0],
+ border: border(theme.middle),
+ padding: {
+ bottom: 4,
+ left: 8,
+ right: 8,
+ top: 4,
+ },
+ margin: {
+ left: side_margin,
+ right: side_margin,
+ },
+ }
+
+ return {
+ picker: {
+ empty_container: {},
+ item: {
+ ...picker_style.item,
+ margin: { left: side_margin, right: side_margin },
+ },
+ no_matches: picker_style.no_matches,
+ input_editor: picker_input,
+ empty_input_editor: picker_input,
+ },
+ row_height: 28,
+ contact_avatar: {
+ corner_radius: 10,
+ width: 18,
+ },
+ contact_username: {
+ padding: {
+ left: 8,
+ },
+ },
+ contact_button: {
+ ...contact_button,
+ hover: {
+ background: background(theme.middle, "variant", "hovered"),
+ },
+ },
+ disabled_contact_button: {
+ ...contact_button,
+ background: background(theme.middle, "disabled"),
+ color: foreground(theme.middle, "disabled"),
+ },
+ }
+}
@@ -1,56 +1,62 @@
-import { ColorScheme } from "../theme/colorScheme"
-import { background, border, borderColor, foreground, text } from "./components"
+import { ColorScheme } from "../theme/color_scheme"
+import {
+ background,
+ border,
+ border_color,
+ foreground,
+ text,
+} from "./components"
import { interactive, toggleable } from "../element"
-export default function contactsPanel(colorScheme: ColorScheme) {
- const nameMargin = 8
- const sidePadding = 12
+export default function contacts_panel(theme: ColorScheme): any {
+ const name_margin = 8
+ const side_padding = 12
- let layer = colorScheme.middle
+ const layer = theme.middle
- const contactButton = {
+ const contact_button = {
background: background(layer, "on"),
color: foreground(layer, "on"),
- iconWidth: 8,
- buttonWidth: 16,
- cornerRadius: 8,
+ icon_width: 8,
+ button_width: 16,
+ corner_radius: 8,
}
- const projectRow = {
- guestAvatarSpacing: 4,
+ const project_row = {
+ guest_avatar_spacing: 4,
height: 24,
- guestAvatar: {
- cornerRadius: 8,
+ guest_avatar: {
+ corner_radius: 8,
width: 14,
},
name: {
...text(layer, "mono", { size: "sm" }),
margin: {
- left: nameMargin,
+ left: name_margin,
right: 6,
},
},
guests: {
margin: {
- left: nameMargin,
- right: nameMargin,
+ left: name_margin,
+ right: name_margin,
},
},
padding: {
- left: sidePadding,
- right: sidePadding,
+ left: side_padding,
+ right: side_padding,
},
}
return {
background: background(layer),
padding: { top: 12 },
- userQueryEditor: {
+ user_query_editor: {
background: background(layer, "on"),
- cornerRadius: 6,
+ corner_radius: 6,
text: text(layer, "mono", "on"),
- placeholderText: text(layer, "mono", "on", "disabled", {
+ placeholder_text: text(layer, "mono", "on", "disabled", {
size: "xs",
}),
- selection: colorScheme.players[0],
+ selection: theme.players[0],
border: border(layer, "on"),
padding: {
bottom: 4,
@@ -62,23 +68,23 @@ export default function contactsPanel(colorScheme: ColorScheme) {
left: 6,
},
},
- userQueryEditorHeight: 33,
- addContactButton: {
+ user_query_editor_height: 33,
+ add_contact_button: {
margin: { left: 6, right: 12 },
color: foreground(layer, "on"),
- buttonWidth: 28,
- iconWidth: 16,
+ button_width: 28,
+ icon_width: 16,
},
- rowHeight: 28,
- sectionIconSize: 8,
- headerRow: toggleable({
+ row_height: 28,
+ section_icon_size: 8,
+ header_row: toggleable({
base: interactive({
base: {
...text(layer, "mono", { size: "sm" }),
margin: { top: 14 },
padding: {
- left: sidePadding,
- right: sidePadding,
+ left: side_padding,
+ right: side_padding,
},
background: background(layer, "default"), // posiewic: breaking change
},
@@ -106,11 +112,11 @@ export default function contactsPanel(colorScheme: ColorScheme) {
},
},
}),
- leaveCall: interactive({
+ leave_call: interactive({
base: {
background: background(layer),
border: border(layer),
- cornerRadius: 6,
+ corner_radius: 6,
margin: {
top: 1,
},
@@ -130,12 +136,12 @@ export default function contactsPanel(colorScheme: ColorScheme) {
},
},
}),
- contactRow: {
+ contact_row: {
inactive: {
default: {
padding: {
- left: sidePadding,
- right: sidePadding,
+ left: side_padding,
+ right: side_padding,
},
},
},
@@ -143,84 +149,83 @@ export default function contactsPanel(colorScheme: ColorScheme) {
default: {
background: background(layer, "active"),
padding: {
- left: sidePadding,
- right: sidePadding,
+ left: side_padding,
+ right: side_padding,
},
},
},
},
-
- contactAvatar: {
- cornerRadius: 10,
+ contact_avatar: {
+ corner_radius: 10,
width: 18,
},
- contactStatusFree: {
- cornerRadius: 4,
+ contact_status_free: {
+ corner_radius: 4,
padding: 4,
margin: { top: 12, left: 12 },
background: foreground(layer, "positive"),
},
- contactStatusBusy: {
- cornerRadius: 4,
+ contact_status_busy: {
+ corner_radius: 4,
padding: 4,
margin: { top: 12, left: 12 },
background: foreground(layer, "negative"),
},
- contactUsername: {
+ contact_username: {
...text(layer, "mono", { size: "sm" }),
margin: {
- left: nameMargin,
+ left: name_margin,
},
},
- contactButtonSpacing: nameMargin,
- contactButton: interactive({
- base: { ...contactButton },
+ contact_button_spacing: name_margin,
+ contact_button: interactive({
+ base: { ...contact_button },
state: {
hovered: {
background: background(layer, "hovered"),
},
},
}),
- disabledButton: {
- ...contactButton,
+ disabled_button: {
+ ...contact_button,
background: background(layer, "on"),
color: foreground(layer, "on"),
},
- callingIndicator: {
+ calling_indicator: {
...text(layer, "mono", "variant", { size: "xs" }),
},
- treeBranch: toggleable({
+ tree_branch: toggleable({
base: interactive({
base: {
- color: borderColor(layer),
+ color: border_color(layer),
width: 1,
},
state: {
hovered: {
- color: borderColor(layer),
+ color: border_color(layer),
},
},
}),
state: {
active: {
default: {
- color: borderColor(layer),
+ color: border_color(layer),
},
},
},
}),
- projectRow: toggleable({
+ project_row: toggleable({
base: interactive({
base: {
- ...projectRow,
+ ...project_row,
background: background(layer),
icon: {
- margin: { left: nameMargin },
+ margin: { left: name_margin },
color: foreground(layer, "variant"),
width: 12,
},
name: {
- ...projectRow.name,
+ ...project_row.name,
...text(layer, "mono", { size: "sm" }),
},
},
@@ -0,0 +1,53 @@
+import { ColorScheme } from "../theme/color_scheme"
+import { background, foreground, text } from "./components"
+import { interactive } from "../element"
+
+export default function contact_notification(theme: ColorScheme): any {
+ const avatar_size = 12
+ const header_padding = 8
+
+ return {
+ header_avatar: {
+ height: avatar_size,
+ width: avatar_size,
+ corner_radius: 6,
+ },
+ header_message: {
+ ...text(theme.lowest, "sans", { size: "xs" }),
+ margin: { left: header_padding, right: header_padding },
+ },
+ header_height: 18,
+ body_message: {
+ ...text(theme.lowest, "sans", { size: "xs" }),
+ margin: { left: avatar_size + header_padding, top: 6, bottom: 6 },
+ },
+ button: interactive({
+ base: {
+ ...text(theme.lowest, "sans", "on", { size: "xs" }),
+ background: background(theme.lowest, "on"),
+ padding: 4,
+ corner_radius: 6,
+ margin: { left: 6 },
+ },
+
+ state: {
+ hovered: {
+ background: background(theme.lowest, "on", "hovered"),
+ },
+ },
+ }),
+
+ dismiss_button: {
+ default: {
+ color: foreground(theme.lowest, "variant"),
+ icon_width: 8,
+ icon_height: 8,
+ button_width: 8,
+ button_height: 8,
+ hover: {
+ color: foreground(theme.lowest, "hovered"),
+ },
+ },
+ },
+ }
+}
@@ -0,0 +1,14 @@
+import { ColorScheme } from "../theme/color_scheme"
+import { background, border } from "./components"
+
+export default function contacts_popover(theme: ColorScheme): any {
+ return {
+ background: background(theme.middle),
+ corner_radius: 6,
+ padding: { top: 6, bottom: 6 },
+ shadow: theme.popover_shadow,
+ border: border(theme.middle),
+ width: 300,
+ height: 400,
+ }
+}
@@ -0,0 +1,68 @@
+import { ColorScheme } from "../theme/color_scheme"
+import { background, border, border_color, text } from "./components"
+import { interactive, toggleable } from "../element"
+
+export default function context_menu(theme: ColorScheme): any {
+ return {
+ background: background(theme.middle),
+ corner_radius: 10,
+ padding: 4,
+ shadow: theme.popover_shadow,
+ border: border(theme.middle),
+ keystroke_margin: 30,
+ item: toggleable({
+ base: interactive({
+ base: {
+ icon_spacing: 8,
+ icon_width: 14,
+ padding: { left: 6, right: 6, top: 2, bottom: 2 },
+ corner_radius: 6,
+ label: text(theme.middle, "sans", { size: "sm" }),
+ keystroke: {
+ ...text(theme.middle, "sans", "variant", {
+ size: "sm",
+ weight: "bold",
+ }),
+ padding: { left: 3, right: 3 },
+ },
+ },
+ state: {
+ hovered: {
+ background: background(theme.middle, "hovered"),
+ label: text(theme.middle, "sans", "hovered", {
+ size: "sm",
+ }),
+ keystroke: {
+ ...text(theme.middle, "sans", "hovered", {
+ size: "sm",
+ weight: "bold",
+ }),
+ padding: { left: 3, right: 3 },
+ },
+ },
+ clicked: {
+ background: background(theme.middle, "pressed"),
+ },
+ },
+ }),
+ state: {
+ active: {
+ default: {
+ background: background(theme.middle, "active"),
+ },
+ hovered: {
+ background: background(theme.middle, "hovered"),
+ },
+ clicked: {
+ background: background(theme.middle, "pressed"),
+ },
+ },
+ },
+ }),
+
+ separator: {
+ background: border_color(theme.middle),
+ margin: { top: 2, bottom: 2 },
+ },
+ }
+}
@@ -1,18 +1,16 @@
-import { ColorScheme } from "../theme/colorScheme"
+import { ColorScheme } from "../theme/color_scheme"
import { background, border, foreground, svg, text } from "./components"
import { interactive } from "../element"
-export default function copilot(colorScheme: ColorScheme) {
- let layer = colorScheme.middle
+export default function copilot(theme: ColorScheme): any {
+ const content_width = 264
- let content_width = 264
-
- let ctaButton =
+ const cta_button =
// Copied from welcome screen. FIXME: Move this into a ZDS component
interactive({
base: {
- background: background(layer),
- border: border(layer, "default"),
- cornerRadius: 4,
+ background: background(theme.middle),
+ border: border(theme.middle, "default"),
+ corner_radius: 4,
margin: {
top: 4,
bottom: 4,
@@ -25,49 +23,52 @@ export default function copilot(colorScheme: ColorScheme) {
left: 7,
right: 7,
},
- ...text(layer, "sans", "default", { size: "sm" }),
+ ...text(theme.middle, "sans", "default", { size: "sm" }),
},
state: {
hovered: {
- ...text(layer, "sans", "default", { size: "sm" }),
- background: background(layer, "hovered"),
- border: border(layer, "active"),
+ ...text(theme.middle, "sans", "default", { size: "sm" }),
+ background: background(theme.middle, "hovered"),
+ border: border(theme.middle, "active"),
},
},
})
return {
- outLinkIcon: interactive({
+ out_link_icon: interactive({
base: {
icon: svg(
- foreground(layer, "variant"),
+ foreground(theme.middle, "variant"),
"icons/link_out_12.svg",
12,
12
),
container: {
- cornerRadius: 6,
+ corner_radius: 6,
padding: { left: 6 },
},
},
state: {
hovered: {
icon: {
- color: foreground(layer, "hovered"),
+ color: foreground(theme.middle, "hovered"),
},
},
},
}),
modal: {
- titleText: {
+ title_text: {
default: {
- ...text(layer, "sans", { size: "xs", weight: "bold" }),
+ ...text(theme.middle, "sans", {
+ size: "xs",
+ weight: "bold",
+ }),
},
},
titlebar: {
- background: background(colorScheme.lowest),
- border: border(layer, "active"),
+ background: background(theme.lowest),
+ border: border(theme.middle, "active"),
padding: {
top: 4,
bottom: 4,
@@ -76,7 +77,7 @@ export default function copilot(colorScheme: ColorScheme) {
},
},
container: {
- background: background(colorScheme.lowest),
+ background: background(theme.lowest),
padding: {
top: 0,
left: 0,
@@ -84,16 +85,16 @@ export default function copilot(colorScheme: ColorScheme) {
bottom: 8,
},
},
- closeIcon: interactive({
+ close_icon: interactive({
base: {
icon: svg(
- foreground(layer, "variant"),
+ foreground(theme.middle, "variant"),
"icons/x_mark_8.svg",
8,
8
),
container: {
- cornerRadius: 2,
+ corner_radius: 2,
padding: {
top: 4,
bottom: 4,
@@ -108,7 +109,7 @@ export default function copilot(colorScheme: ColorScheme) {
state: {
hovered: {
icon: svg(
- foreground(layer, "on"),
+ foreground(theme.middle, "on"),
"icons/x_mark_8.svg",
8,
8
@@ -116,7 +117,7 @@ export default function copilot(colorScheme: ColorScheme) {
},
clicked: {
icon: svg(
- foreground(layer, "base"),
+ foreground(theme.middle, "base"),
"icons/x_mark_8.svg",
8,
8
@@ -133,11 +134,11 @@ export default function copilot(colorScheme: ColorScheme) {
auth: {
content_width,
- ctaButton,
+ cta_button,
header: {
icon: svg(
- foreground(layer, "default"),
+ foreground(theme.middle, "default"),
"icons/zed_plus_copilot_32.svg",
92,
32
@@ -154,7 +155,7 @@ export default function copilot(colorScheme: ColorScheme) {
prompting: {
subheading: {
- ...text(layer, "sans", { size: "xs" }),
+ ...text(theme.middle, "sans", { size: "xs" }),
margin: {
top: 6,
bottom: 12,
@@ -164,19 +165,22 @@ export default function copilot(colorScheme: ColorScheme) {
},
hint: {
- ...text(layer, "sans", { size: "xs", color: "#838994" }),
+ ...text(theme.middle, "sans", {
+ size: "xs",
+ color: "#838994",
+ }),
margin: {
top: 6,
bottom: 2,
},
},
- deviceCode: {
- text: text(layer, "mono", { size: "sm" }),
+ device_code: {
+ text: text(theme.middle, "mono", { size: "sm" }),
cta: {
- ...ctaButton,
- background: background(colorScheme.lowest),
- border: border(colorScheme.lowest, "inverted"),
+ ...cta_button,
+ background: background(theme.lowest),
+ border: border(theme.lowest, "inverted"),
padding: {
top: 0,
bottom: 0,
@@ -189,7 +193,7 @@ export default function copilot(colorScheme: ColorScheme) {
},
},
left: content_width / 2,
- leftContainer: {
+ left_container: {
padding: {
top: 3,
bottom: 3,
@@ -198,9 +202,9 @@ export default function copilot(colorScheme: ColorScheme) {
},
},
right: (content_width * 1) / 3,
- rightContainer: interactive({
+ right_container: interactive({
base: {
- border: border(colorScheme.lowest, "inverted", {
+ border: border(theme.lowest, "inverted", {
bottom: false,
right: false,
top: false,
@@ -215,7 +219,7 @@ export default function copilot(colorScheme: ColorScheme) {
},
state: {
hovered: {
- border: border(layer, "active", {
+ border: border(theme.middle, "active", {
bottom: false,
right: false,
top: false,
@@ -227,9 +231,9 @@ export default function copilot(colorScheme: ColorScheme) {
},
},
- notAuthorized: {
+ not_authorized: {
subheading: {
- ...text(layer, "sans", { size: "xs" }),
+ ...text(theme.middle, "sans", { size: "xs" }),
margin: {
top: 16,
@@ -240,13 +244,13 @@ export default function copilot(colorScheme: ColorScheme) {
},
warning: {
- ...text(layer, "sans", {
+ ...text(theme.middle, "sans", {
size: "xs",
- color: foreground(layer, "warning"),
+ color: foreground(theme.middle, "warning"),
}),
- border: border(layer, "warning"),
- background: background(layer, "warning"),
- cornerRadius: 2,
+ border: border(theme.middle, "warning"),
+ background: background(theme.middle, "warning"),
+ corner_radius: 2,
padding: {
top: 4,
left: 4,
@@ -263,7 +267,7 @@ export default function copilot(colorScheme: ColorScheme) {
authorized: {
subheading: {
- ...text(layer, "sans", { size: "xs" }),
+ ...text(theme.middle, "sans", { size: "xs" }),
margin: {
top: 16,
@@ -272,7 +276,10 @@ export default function copilot(colorScheme: ColorScheme) {
},
hint: {
- ...text(layer, "sans", { size: "xs", color: "#838994" }),
+ ...text(theme.middle, "sans", {
+ size: "xs",
+ color: "#838994",
+ }),
margin: {
top: 24,
bottom: 4,
@@ -0,0 +1,313 @@
+import { with_opacity } from "../theme/color"
+import { ColorScheme, Layer, StyleSets } from "../theme/color_scheme"
+import {
+ background,
+ border,
+ border_color,
+ foreground,
+ text,
+} from "./components"
+import hover_popover from "./hover_popover"
+
+import { build_syntax } from "../theme/syntax"
+import { interactive, toggleable } from "../element"
+
+export default function editor(theme: ColorScheme): any {
+ const { is_light } = theme
+
+ const layer = theme.highest
+
+ const autocomplete_item = {
+ corner_radius: 6,
+ padding: {
+ bottom: 2,
+ left: 6,
+ right: 6,
+ top: 2,
+ },
+ }
+
+ function diagnostic(layer: Layer, style_set: StyleSets) {
+ return {
+ text_scale_factor: 0.857,
+ header: {
+ border: border(layer, {
+ top: true,
+ }),
+ },
+ message: {
+ text: text(layer, "sans", style_set, "default", { size: "sm" }),
+ highlight_text: text(layer, "sans", style_set, "default", {
+ size: "sm",
+ weight: "bold",
+ }),
+ },
+ }
+ }
+
+ const syntax = build_syntax(theme)
+
+ return {
+ text_color: syntax.primary.color,
+ background: background(layer),
+ active_line_background: with_opacity(background(layer, "on"), 0.75),
+ highlighted_line_background: background(layer, "on"),
+ // Inline autocomplete suggestions, Co-pilot suggestions, etc.
+ hint: syntax.hint,
+ suggestion: syntax.predictive,
+ code_actions: {
+ indicator: toggleable({
+ base: interactive({
+ base: {
+ color: foreground(layer, "variant"),
+ },
+ state: {
+ hovered: {
+ color: foreground(layer, "variant", "hovered"),
+ },
+ clicked: {
+ color: foreground(layer, "variant", "pressed"),
+ },
+ },
+ }),
+ state: {
+ active: {
+ default: {
+ color: foreground(layer, "accent"),
+ },
+ hovered: {
+ color: foreground(layer, "accent", "hovered"),
+ },
+ clicked: {
+ color: foreground(layer, "accent", "pressed"),
+ },
+ },
+ },
+ }),
+
+ vertical_scale: 0.55,
+ },
+ folds: {
+ icon_margin_scale: 2.5,
+ folded_icon: "icons/chevron_right_8.svg",
+ foldable_icon: "icons/chevron_down_8.svg",
+ indicator: toggleable({
+ base: interactive({
+ base: {
+ color: foreground(layer, "variant"),
+ },
+ state: {
+ hovered: {
+ color: foreground(layer, "on"),
+ },
+ clicked: {
+ color: foreground(layer, "base"),
+ },
+ },
+ }),
+ state: {
+ active: {
+ default: {
+ color: foreground(layer, "default"),
+ },
+ hovered: {
+ color: foreground(layer, "variant"),
+ },
+ },
+ },
+ }),
+ ellipses: {
+ text_color: theme.ramps.neutral(0.71).hex(),
+ corner_radius_factor: 0.15,
+ background: {
+ // Copied from hover_popover highlight
+ default: {
+ color: theme.ramps.neutral(0.5).alpha(0.0).hex(),
+ },
+
+ hovered: {
+ color: theme.ramps.neutral(0.5).alpha(0.5).hex(),
+ },
+
+ clicked: {
+ color: theme.ramps.neutral(0.5).alpha(0.7).hex(),
+ },
+ },
+ },
+ fold_background: foreground(layer, "variant"),
+ },
+ diff: {
+ deleted: is_light
+ ? theme.ramps.red(0.5).hex()
+ : theme.ramps.red(0.4).hex(),
+ modified: is_light
+ ? theme.ramps.yellow(0.5).hex()
+ : theme.ramps.yellow(0.5).hex(),
+ inserted: is_light
+ ? theme.ramps.green(0.4).hex()
+ : theme.ramps.green(0.5).hex(),
+ removed_width_em: 0.275,
+ width_em: 0.15,
+ corner_radius: 0.05,
+ },
+ /** Highlights matching occurrences of what is under the cursor
+ * as well as matched brackets
+ */
+ document_highlight_read_background: with_opacity(
+ foreground(layer, "accent"),
+ 0.1
+ ),
+ document_highlight_write_background: theme.ramps
+ .neutral(0.5)
+ .alpha(0.4)
+ .hex(), // TODO: This was blend * 2
+ error_color: background(layer, "negative"),
+ gutter_background: background(layer),
+ gutter_padding_factor: 3.5,
+ line_number: with_opacity(foreground(layer), 0.35),
+ line_number_active: foreground(layer),
+ rename_fade: 0.6,
+ unnecessary_code_fade: 0.5,
+ selection: theme.players[0],
+ whitespace: theme.ramps.neutral(0.5).hex(),
+ guest_selections: [
+ theme.players[1],
+ theme.players[2],
+ theme.players[3],
+ theme.players[4],
+ theme.players[5],
+ theme.players[6],
+ theme.players[7],
+ ],
+ autocomplete: {
+ background: background(theme.middle),
+ corner_radius: 8,
+ padding: 4,
+ margin: {
+ left: -14,
+ },
+ border: border(theme.middle),
+ shadow: theme.popover_shadow,
+ match_highlight: foreground(theme.middle, "accent"),
+ item: autocomplete_item,
+ hovered_item: {
+ ...autocomplete_item,
+ match_highlight: foreground(theme.middle, "accent", "hovered"),
+ background: background(theme.middle, "hovered"),
+ },
+ selected_item: {
+ ...autocomplete_item,
+ match_highlight: foreground(theme.middle, "accent", "active"),
+ background: background(theme.middle, "active"),
+ },
+ },
+ diagnostic_header: {
+ background: background(theme.middle),
+ icon_width_factor: 1.5,
+ text_scale_factor: 0.857,
+ border: border(theme.middle, {
+ bottom: true,
+ top: true,
+ }),
+ code: {
+ ...text(theme.middle, "mono", { size: "sm" }),
+ margin: {
+ left: 10,
+ },
+ },
+ source: {
+ text: text(theme.middle, "sans", {
+ size: "sm",
+ weight: "bold",
+ }),
+ },
+ message: {
+ highlight_text: text(theme.middle, "sans", {
+ size: "sm",
+ weight: "bold",
+ }),
+ text: text(theme.middle, "sans", { size: "sm" }),
+ },
+ },
+ diagnostic_path_header: {
+ background: background(theme.middle),
+ text_scale_factor: 0.857,
+ filename: text(theme.middle, "mono", { size: "sm" }),
+ path: {
+ ...text(theme.middle, "mono", { size: "sm" }),
+ margin: {
+ left: 12,
+ },
+ },
+ },
+ error_diagnostic: diagnostic(theme.middle, "negative"),
+ warning_diagnostic: diagnostic(theme.middle, "warning"),
+ information_diagnostic: diagnostic(theme.middle, "accent"),
+ hint_diagnostic: diagnostic(theme.middle, "warning"),
+ invalid_error_diagnostic: diagnostic(theme.middle, "base"),
+ invalid_hint_diagnostic: diagnostic(theme.middle, "base"),
+ invalid_information_diagnostic: diagnostic(theme.middle, "base"),
+ invalid_warning_diagnostic: diagnostic(theme.middle, "base"),
+ hover_popover: hover_popover(theme),
+ link_definition: {
+ color: syntax.link_uri.color,
+ underline: syntax.link_uri.underline,
+ },
+ jump_icon: interactive({
+ base: {
+ color: foreground(layer, "on"),
+ icon_width: 20,
+ button_width: 20,
+ corner_radius: 6,
+ padding: {
+ top: 6,
+ bottom: 6,
+ left: 6,
+ right: 6,
+ },
+ },
+ state: {
+ hovered: {
+ background: background(layer, "on", "hovered"),
+ },
+ },
+ }),
+
+ scrollbar: {
+ width: 12,
+ min_height_factor: 1.0,
+ track: {
+ border: border(layer, "variant", { left: true }),
+ },
+ thumb: {
+ background: with_opacity(background(layer, "inverted"), 0.3),
+ border: {
+ width: 1,
+ color: border_color(layer, "variant"),
+ top: false,
+ right: true,
+ left: true,
+ bottom: false,
+ },
+ },
+ git: {
+ deleted: is_light
+ ? with_opacity(theme.ramps.red(0.5).hex(), 0.8)
+ : with_opacity(theme.ramps.red(0.4).hex(), 0.8),
+ modified: is_light
+ ? with_opacity(theme.ramps.yellow(0.5).hex(), 0.8)
+ : with_opacity(theme.ramps.yellow(0.4).hex(), 0.8),
+ inserted: is_light
+ ? with_opacity(theme.ramps.green(0.5).hex(), 0.8)
+ : with_opacity(theme.ramps.green(0.4).hex(), 0.8),
+ },
+ },
+ composition_mark: {
+ underline: {
+ thickness: 1.0,
+ color: border_color(layer),
+ },
+ },
+ syntax,
+ }
+}
@@ -0,0 +1,49 @@
+import { ColorScheme } from "../theme/color_scheme"
+import { background, border, text } from "./components"
+import { interactive } from "../element"
+
+export default function feedback(theme: ColorScheme): any {
+ return {
+ submit_button: interactive({
+ base: {
+ ...text(theme.highest, "mono", "on"),
+ background: background(theme.highest, "on"),
+ corner_radius: 6,
+ border: border(theme.highest, "on"),
+ margin: {
+ right: 4,
+ },
+ padding: {
+ bottom: 2,
+ left: 10,
+ right: 10,
+ top: 2,
+ },
+ },
+ state: {
+ clicked: {
+ ...text(theme.highest, "mono", "on", "pressed"),
+ background: background(theme.highest, "on", "pressed"),
+ border: border(theme.highest, "on", "pressed"),
+ },
+ hovered: {
+ ...text(theme.highest, "mono", "on", "hovered"),
+ background: background(theme.highest, "on", "hovered"),
+ border: border(theme.highest, "on", "hovered"),
+ },
+ },
+ }),
+ button_margin: 8,
+ info_text_default: text(theme.highest, "sans", "default", {
+ size: "xs",
+ }),
+ link_text_default: text(theme.highest, "sans", "default", {
+ size: "xs",
+ underline: true,
+ }),
+ link_text_hover: text(theme.highest, "sans", "hovered", {
+ size: "xs",
+ underline: true,
+ }),
+ }
+}
@@ -0,0 +1,47 @@
+import { ColorScheme } from "../theme/color_scheme"
+import { background, border, foreground, text } from "./components"
+
+export default function hover_popover(theme: ColorScheme): any {
+ const base_container = {
+ background: background(theme.middle),
+ corner_radius: 8,
+ padding: {
+ left: 8,
+ right: 8,
+ top: 4,
+ bottom: 4,
+ },
+ shadow: theme.popover_shadow,
+ border: border(theme.middle),
+ margin: {
+ left: -8,
+ },
+ }
+
+ return {
+ container: base_container,
+ info_container: {
+ ...base_container,
+ background: background(theme.middle, "accent"),
+ border: border(theme.middle, "accent"),
+ },
+ warning_container: {
+ ...base_container,
+ background: background(theme.middle, "warning"),
+ border: border(theme.middle, "warning"),
+ },
+ error_container: {
+ ...base_container,
+ background: background(theme.middle, "negative"),
+ border: border(theme.middle, "negative"),
+ },
+ block_style: {
+ padding: { top: 4 },
+ },
+ prose: text(theme.middle, "sans", { size: "sm" }),
+ diagnostic_source_highlight: {
+ color: foreground(theme.middle, "accent"),
+ },
+ highlight: theme.ramps.neutral(0.5).alpha(0.2).hex(), // TODO: blend was used here. Replace with something better
+ }
+}
@@ -0,0 +1,55 @@
+import { ColorScheme } from "../theme/color_scheme"
+import { background, border, text } from "./components"
+
+export default function incoming_call_notification(
+ theme: ColorScheme
+): unknown {
+ const avatar_size = 48
+ return {
+ window_height: 74,
+ window_width: 380,
+ background: background(theme.middle),
+ caller_container: {
+ padding: 12,
+ },
+ caller_avatar: {
+ height: avatar_size,
+ width: avatar_size,
+ corner_radius: avatar_size / 2,
+ },
+ caller_metadata: {
+ margin: { left: 10 },
+ },
+ caller_username: {
+ ...text(theme.middle, "sans", { size: "sm", weight: "bold" }),
+ margin: { top: -3 },
+ },
+ caller_message: {
+ ...text(theme.middle, "sans", "variant", { size: "xs" }),
+ margin: { top: -3 },
+ },
+ worktree_roots: {
+ ...text(theme.middle, "sans", "variant", {
+ size: "xs",
+ weight: "bold",
+ }),
+ margin: { top: -3 },
+ },
+ button_width: 96,
+ accept_button: {
+ background: background(theme.middle, "accent"),
+ border: border(theme.middle, { left: true, bottom: true }),
+ ...text(theme.middle, "sans", "positive", {
+ size: "xs",
+ weight: "bold",
+ }),
+ },
+ decline_button: {
+ border: border(theme.middle, { left: true }),
+ ...text(theme.middle, "sans", "negative", {
+ size: "xs",
+ weight: "bold",
+ }),
+ },
+ }
+}
@@ -1,24 +1,23 @@
-import { ColorScheme } from "../theme/colorScheme"
-import { withOpacity } from "../theme/color"
+import { ColorScheme } from "../theme/color_scheme"
+import { with_opacity } from "../theme/color"
import { background, border, text } from "./components"
import { interactive, toggleable } from "../element"
-export default function picker(colorScheme: ColorScheme): any {
- let layer = colorScheme.lowest
+export default function picker(theme: ColorScheme): any {
const container = {
- background: background(layer),
- border: border(layer),
- shadow: colorScheme.modalShadow,
- cornerRadius: 12,
+ background: background(theme.lowest),
+ border: border(theme.lowest),
+ shadow: theme.modal_shadow,
+ corner_radius: 12,
padding: {
bottom: 4,
},
}
- const inputEditor = {
- placeholderText: text(layer, "sans", "on", "disabled"),
- selection: colorScheme.players[0],
- text: text(layer, "mono", "on"),
- border: border(layer, { bottom: true }),
+ const input_editor = {
+ placeholder_text: text(theme.lowest, "sans", "on", "disabled"),
+ selection: theme.players[0],
+ text: text(theme.lowest, "mono", "on"),
+ border: border(theme.lowest, { bottom: true }),
padding: {
bottom: 8,
left: 16,
@@ -29,13 +28,13 @@ export default function picker(colorScheme: ColorScheme): any {
bottom: 4,
},
}
- const emptyInputEditor: any = { ...inputEditor }
- delete emptyInputEditor.border
- delete emptyInputEditor.margin
+ const empty_input_editor: any = { ...input_editor }
+ delete empty_input_editor.border
+ delete empty_input_editor.margin
return {
...container,
- emptyContainer: {
+ empty_container: {
...container,
padding: {},
},
@@ -53,22 +52,22 @@ export default function picker(colorScheme: ColorScheme): any {
left: 4,
right: 4,
},
- cornerRadius: 8,
- text: text(layer, "sans", "variant"),
- highlightText: text(layer, "sans", "accent", {
+ corner_radius: 8,
+ text: text(theme.lowest, "sans", "variant"),
+ highlight_text: text(theme.lowest, "sans", "accent", {
weight: "bold",
}),
},
state: {
hovered: {
- background: withOpacity(
- background(layer, "hovered"),
+ background: with_opacity(
+ background(theme.lowest, "hovered"),
0.5
),
},
clicked: {
- background: withOpacity(
- background(layer, "pressed"),
+ background: with_opacity(
+ background(theme.lowest, "pressed"),
0.5
),
},
@@ -77,20 +76,20 @@ export default function picker(colorScheme: ColorScheme): any {
state: {
active: {
default: {
- background: withOpacity(
- background(layer, "base", "active"),
+ background: with_opacity(
+ background(theme.lowest, "base", "active"),
0.5
),
},
hovered: {
- background: withOpacity(
- background(layer, "hovered"),
+ background: with_opacity(
+ background(theme.lowest, "hovered"),
0.5
),
},
clicked: {
- background: withOpacity(
- background(layer, "pressed"),
+ background: with_opacity(
+ background(theme.lowest, "pressed"),
0.5
),
},
@@ -98,10 +97,10 @@ export default function picker(colorScheme: ColorScheme): any {
},
}),
- inputEditor,
- emptyInputEditor,
- noMatches: {
- text: text(layer, "sans", "variant"),
+ input_editor,
+ empty_input_editor,
+ no_matches: {
+ text: text(theme.lowest, "sans", "variant"),
padding: {
bottom: 8,
left: 16,
@@ -0,0 +1,12 @@
+import { ColorScheme } from "../theme/color_scheme"
+import { background, text } from "./components"
+
+export default function project_diagnostics(theme: ColorScheme): any {
+ return {
+ background: background(theme.highest),
+ tab_icon_spacing: 4,
+ tab_icon_width: 13,
+ tab_summary_spacing: 10,
+ empty_message: text(theme.highest, "sans", "variant", { size: "md" }),
+ }
+}
@@ -0,0 +1,202 @@
+import { ColorScheme } from "../theme/color_scheme"
+import { with_opacity } from "../theme/color"
+import {
+ Border,
+ TextStyle,
+ background,
+ border,
+ foreground,
+ text,
+} from "./components"
+import { interactive, toggleable } from "../element"
+import merge from "ts-deepmerge"
+export default function project_panel(theme: ColorScheme): any {
+ const { is_light } = theme
+
+ type EntryStateProps = {
+ background?: string
+ border?: Border
+ text?: TextStyle
+ icon_color?: string
+ }
+
+ type EntryState = {
+ default: EntryStateProps
+ hovered?: EntryStateProps
+ clicked?: EntryStateProps
+ }
+
+ const entry = (unselected?: EntryState, selected?: EntryState) => {
+ const git_status = {
+ git: {
+ modified: is_light
+ ? theme.ramps.yellow(0.6).hex()
+ : theme.ramps.yellow(0.5).hex(),
+ inserted: is_light
+ ? theme.ramps.green(0.45).hex()
+ : theme.ramps.green(0.5).hex(),
+ conflict: is_light
+ ? theme.ramps.red(0.6).hex()
+ : theme.ramps.red(0.5).hex(),
+ },
+ }
+
+ const base_properties = {
+ height: 22,
+ background: background(theme.middle),
+ icon_color: foreground(theme.middle, "variant"),
+ icon_size: 7,
+ icon_spacing: 5,
+ text: text(theme.middle, "sans", "variant", { size: "sm" }),
+ status: {
+ ...git_status,
+ },
+ }
+
+ const selected_style: EntryState | undefined = selected
+ ? selected
+ : unselected
+
+ const unselected_default_style = merge(
+ base_properties,
+ unselected?.default ?? {},
+ {}
+ )
+ const unselected_hovered_style = merge(
+ base_properties,
+ { background: background(theme.middle, "hovered") },
+ unselected?.hovered ?? {},
+ )
+ const unselected_clicked_style = merge(
+ base_properties,
+ { background: background(theme.middle, "pressed"), }
+ ,
+ unselected?.clicked ?? {},
+ )
+ const selected_default_style = merge(
+ base_properties,
+ {
+ background: background(theme.lowest),
+ text: text(theme.lowest, "sans", { size: "sm" }),
+ },
+ selected_style?.default ?? {},
+
+ )
+ const selected_hovered_style = merge(
+ base_properties,
+ {
+ background: background(theme.lowest, "hovered"),
+ text: text(theme.lowest, "sans", { size: "sm" }),
+
+ },
+ selected_style?.hovered ?? {},
+
+ )
+ const selected_clicked_style = merge(
+ base_properties,
+ {
+ background: background(theme.lowest, "pressed"),
+ text: text(theme.lowest, "sans", { size: "sm" }),
+ },
+ selected_style?.clicked ?? {},
+
+ )
+
+ return toggleable({
+ state: {
+ inactive: interactive({
+ state: {
+ default: unselected_default_style,
+ hovered: unselected_hovered_style,
+ clicked: unselected_clicked_style,
+ },
+ }),
+ active: interactive({
+ state: {
+ default: selected_default_style,
+ hovered: selected_hovered_style,
+ clicked: selected_clicked_style,
+ },
+ }),
+ },
+ })
+ }
+
+ const default_entry = entry()
+
+ return {
+ open_project_button: interactive({
+ base: {
+ background: background(theme.middle),
+ border: border(theme.middle, "active"),
+ corner_radius: 4,
+ margin: {
+ top: 16,
+ left: 16,
+ right: 16,
+ },
+ padding: {
+ top: 3,
+ bottom: 3,
+ left: 7,
+ right: 7,
+ },
+ ...text(theme.middle, "sans", "default", { size: "sm" }),
+ },
+ state: {
+ hovered: {
+ ...text(theme.middle, "sans", "default", { size: "sm" }),
+ background: background(theme.middle, "hovered"),
+ border: border(theme.middle, "active"),
+ },
+ clicked: {
+ ...text(theme.middle, "sans", "default", { size: "sm" }),
+ background: background(theme.middle, "pressed"),
+ border: border(theme.middle, "active"),
+ },
+ },
+ }),
+ background: background(theme.middle),
+ padding: { left: 6, right: 6, top: 0, bottom: 6 },
+ indent_width: 12,
+ entry: default_entry,
+ dragged_entry: {
+ ...default_entry.inactive.default,
+ text: text(theme.middle, "sans", "on", { size: "sm" }),
+ background: with_opacity(background(theme.middle, "on"), 0.9),
+ border: border(theme.middle),
+ },
+ ignored_entry: entry(
+ {
+ default: {
+ text: text(theme.middle, "sans", "disabled"),
+ },
+ },
+ {
+ default: {
+ icon_color: foreground(theme.middle, "variant"),
+ },
+ }
+ ),
+ cut_entry: entry(
+ {
+ default: {
+ text: text(theme.middle, "sans", "disabled"),
+ },
+ },
+ {
+ default: {
+ background: background(theme.middle, "active"),
+ text: text(theme.middle, "sans", "disabled", {
+ size: "sm",
+ }),
+ },
+ }
+ ),
+ filename_editor: {
+ background: background(theme.middle, "on"),
+ text: text(theme.middle, "sans", "on", { size: "sm" }),
+ selection: theme.players[0],
+ },
+ }
+}
@@ -0,0 +1,55 @@
+import { ColorScheme } from "../theme/color_scheme"
+import { background, border, text } from "./components"
+
+export default function project_shared_notification(
+ theme: ColorScheme
+): unknown {
+ const avatar_size = 48
+ return {
+ window_height: 74,
+ window_width: 380,
+ background: background(theme.middle),
+ owner_container: {
+ padding: 12,
+ },
+ owner_avatar: {
+ height: avatar_size,
+ width: avatar_size,
+ corner_radius: avatar_size / 2,
+ },
+ owner_metadata: {
+ margin: { left: 10 },
+ },
+ owner_username: {
+ ...text(theme.middle, "sans", { size: "sm", weight: "bold" }),
+ margin: { top: -3 },
+ },
+ message: {
+ ...text(theme.middle, "sans", "variant", { size: "xs" }),
+ margin: { top: -3 },
+ },
+ worktree_roots: {
+ ...text(theme.middle, "sans", "variant", {
+ size: "xs",
+ weight: "bold",
+ }),
+ margin: { top: -3 },
+ },
+ button_width: 96,
+ open_button: {
+ background: background(theme.middle, "accent"),
+ border: border(theme.middle, { left: true, bottom: true }),
+ ...text(theme.middle, "sans", "accent", {
+ size: "xs",
+ weight: "bold",
+ }),
+ },
+ dismiss_button: {
+ border: border(theme.middle, { left: true }),
+ ...text(theme.middle, "sans", "variant", {
+ size: "xs",
+ weight: "bold",
+ }),
+ },
+ }
+}
@@ -0,0 +1,136 @@
+import { ColorScheme } from "../theme/color_scheme"
+import { with_opacity } from "../theme/color"
+import { background, border, foreground, text } from "./components"
+import { interactive, toggleable } from "../element"
+
+export default function search(theme: ColorScheme): any {
+ // Search input
+ const editor = {
+ background: background(theme.highest),
+ corner_radius: 8,
+ min_width: 200,
+ max_width: 500,
+ placeholder_text: text(theme.highest, "mono", "disabled"),
+ selection: theme.players[0],
+ text: text(theme.highest, "mono", "default"),
+ border: border(theme.highest),
+ margin: {
+ right: 12,
+ },
+ padding: {
+ top: 3,
+ bottom: 3,
+ left: 12,
+ right: 8,
+ },
+ }
+
+ const include_exclude_editor = {
+ ...editor,
+ min_width: 100,
+ max_width: 250,
+ }
+
+ return {
+ // TODO: Add an activeMatchBackground on the rust side to differentiate between active and inactive
+ match_background: with_opacity(
+ foreground(theme.highest, "accent"),
+ 0.4
+ ),
+ option_button: toggleable({
+ base: interactive({
+ base: {
+ ...text(theme.highest, "mono", "on"),
+ background: background(theme.highest, "on"),
+ corner_radius: 6,
+ border: border(theme.highest, "on"),
+ margin: {
+ right: 4,
+ },
+ padding: {
+ bottom: 2,
+ left: 10,
+ right: 10,
+ top: 2,
+ },
+ },
+ state: {
+ hovered: {
+ ...text(theme.highest, "mono", "on", "hovered"),
+ background: background(theme.highest, "on", "hovered"),
+ border: border(theme.highest, "on", "hovered"),
+ },
+ clicked: {
+ ...text(theme.highest, "mono", "on", "pressed"),
+ background: background(theme.highest, "on", "pressed"),
+ border: border(theme.highest, "on", "pressed"),
+ },
+ },
+ }),
+ state: {
+ active: {
+ default: {
+ ...text(theme.highest, "mono", "accent"),
+ },
+ hovered: {
+ ...text(theme.highest, "mono", "accent", "hovered"),
+ },
+ clicked: {
+ ...text(theme.highest, "mono", "accent", "pressed"),
+ },
+ },
+ },
+ }),
+ editor,
+ invalid_editor: {
+ ...editor,
+ border: border(theme.highest, "negative"),
+ },
+ include_exclude_editor,
+ invalid_include_exclude_editor: {
+ ...include_exclude_editor,
+ border: border(theme.highest, "negative"),
+ },
+ match_index: {
+ ...text(theme.highest, "mono", "variant"),
+ padding: {
+ left: 6,
+ },
+ },
+ option_button_group: {
+ padding: {
+ left: 12,
+ right: 12,
+ },
+ },
+ include_exclude_inputs: {
+ ...text(theme.highest, "mono", "variant"),
+ padding: {
+ right: 6,
+ },
+ },
+ results_status: {
+ ...text(theme.highest, "mono", "on"),
+ size: 18,
+ },
+ dismiss_button: interactive({
+ base: {
+ color: foreground(theme.highest, "variant"),
+ icon_width: 12,
+ button_width: 14,
+ padding: {
+ left: 10,
+ right: 10,
+ },
+ },
+ state: {
+ hovered: {
+ color: foreground(theme.highest, "hovered"),
+ },
+ clicked: {
+ color: foreground(theme.highest, "pressed"),
+ },
+ },
+ }),
+ }
+}
@@ -0,0 +1,8 @@
+import { ColorScheme } from "../theme/color_scheme"
+import { background } from "./components"
+
+export default function sharedScreen(theme: ColorScheme) {
+ return {
+ background: background(theme.highest),
+ }
+}
@@ -0,0 +1,50 @@
+import { ColorScheme } from "../theme/color_scheme"
+import { background, border, foreground, text } from "./components"
+import { interactive } from "../element"
+
+export default function simple_message_notification(theme: ColorScheme): any {
+ const header_padding = 8
+
+ return {
+ message: {
+ ...text(theme.middle, "sans", { size: "xs" }),
+ margin: { left: header_padding, right: header_padding },
+ },
+ action_message: interactive({
+ base: {
+ ...text(theme.middle, "sans", { size: "xs" }),
+ border: border(theme.middle, "active"),
+ corner_radius: 4,
+ padding: {
+ top: 3,
+ bottom: 3,
+ left: 7,
+ right: 7,
+ },
+
+ margin: { left: header_padding, top: 6, bottom: 6 },
+ },
+ state: {
+ hovered: {
+ ...text(theme.middle, "sans", "default", { size: "xs" }),
+ background: background(theme.middle, "hovered"),
+ border: border(theme.middle, "active"),
+ },
+ },
+ }),
+ dismiss_button: interactive({
+ base: {
+ color: foreground(theme.middle),
+ icon_width: 8,
+ icon_height: 8,
+ button_width: 8,
+ button_height: 8,
+ },
+ state: {
+ hovered: {
+ color: foreground(theme.middle, "hovered"),
+ },
+ },
+ }),
+ }
+}
@@ -1,22 +1,22 @@
-import { ColorScheme } from "../theme/colorScheme"
+import { ColorScheme } from "../theme/color_scheme"
import { background, border, foreground, text } from "./components"
import { interactive, toggleable } from "../element"
-export default function statusBar(colorScheme: ColorScheme) {
- let layer = colorScheme.lowest
+export default function status_bar(theme: ColorScheme): any {
+ const layer = theme.lowest
- const statusContainer = {
- cornerRadius: 6,
+ const status_container = {
+ corner_radius: 6,
padding: { top: 3, bottom: 3, left: 6, right: 6 },
}
- const diagnosticStatusContainer = {
- cornerRadius: 6,
+ const diagnostic_status_container = {
+ corner_radius: 6,
padding: { top: 1, bottom: 1, left: 6, right: 6 },
}
return {
height: 30,
- itemSpacing: 8,
+ item_spacing: 8,
padding: {
top: 1,
bottom: 1,
@@ -24,8 +24,8 @@ export default function statusBar(colorScheme: ColorScheme) {
right: 6,
},
border: border(layer, { top: true, overlay: true }),
- cursorPosition: text(layer, "sans", "variant"),
- activeLanguage: interactive({
+ cursor_position: text(layer, "sans", "variant"),
+ active_language: interactive({
base: {
padding: { left: 6, right: 6 },
...text(layer, "sans", "variant"),
@@ -36,83 +36,83 @@ export default function statusBar(colorScheme: ColorScheme) {
},
},
}),
- autoUpdateProgressMessage: text(layer, "sans", "variant"),
- autoUpdateDoneMessage: text(layer, "sans", "variant"),
- lspStatus: interactive({
+ auto_update_progress_message: text(layer, "sans", "variant"),
+ auto_update_done_message: text(layer, "sans", "variant"),
+ lsp_status: interactive({
base: {
- ...diagnosticStatusContainer,
- iconSpacing: 4,
- iconWidth: 14,
+ ...diagnostic_status_container,
+ icon_spacing: 4,
+ icon_width: 14,
height: 18,
message: text(layer, "sans"),
- iconColor: foreground(layer),
+ icon_color: foreground(layer),
},
state: {
hovered: {
message: text(layer, "sans"),
- iconColor: foreground(layer),
+ icon_color: foreground(layer),
background: background(layer, "hovered"),
},
},
}),
- diagnosticMessage: interactive({
+ diagnostic_message: interactive({
base: {
...text(layer, "sans"),
},
state: { hovered: text(layer, "sans", "hovered") },
}),
- diagnosticSummary: interactive({
+ diagnostic_summary: interactive({
base: {
height: 20,
- iconWidth: 16,
- iconSpacing: 2,
- summarySpacing: 6,
+ icon_width: 16,
+ icon_spacing: 2,
+ summary_spacing: 6,
text: text(layer, "sans", { size: "sm" }),
- iconColorOk: foreground(layer, "variant"),
- iconColorWarning: foreground(layer, "warning"),
- iconColorError: foreground(layer, "negative"),
- containerOk: {
- cornerRadius: 6,
+ icon_color_ok: foreground(layer, "variant"),
+ icon_color_warning: foreground(layer, "warning"),
+ icon_color_error: foreground(layer, "negative"),
+ container_ok: {
+ corner_radius: 6,
padding: { top: 3, bottom: 3, left: 7, right: 7 },
},
- containerWarning: {
- ...diagnosticStatusContainer,
+ container_warning: {
+ ...diagnostic_status_container,
background: background(layer, "warning"),
border: border(layer, "warning"),
},
- containerError: {
- ...diagnosticStatusContainer,
+ container_error: {
+ ...diagnostic_status_container,
background: background(layer, "negative"),
border: border(layer, "negative"),
},
},
state: {
hovered: {
- iconColorOk: foreground(layer, "on"),
- containerOk: {
+ icon_color_ok: foreground(layer, "on"),
+ container_ok: {
background: background(layer, "on", "hovered"),
},
- containerWarning: {
+ container_warning: {
background: background(layer, "warning", "hovered"),
border: border(layer, "warning", "hovered"),
},
- containerError: {
+ container_error: {
background: background(layer, "negative", "hovered"),
border: border(layer, "negative", "hovered"),
},
},
},
}),
- panelButtons: {
- groupLeft: {},
- groupBottom: {},
- groupRight: {},
+ panel_buttons: {
+ group_left: {},
+ group_bottom: {},
+ group_right: {},
button: toggleable({
base: interactive({
base: {
- ...statusContainer,
- iconSize: 16,
- iconColor: foreground(layer, "variant"),
+ ...status_container,
+ icon_size: 16,
+ icon_color: foreground(layer, "variant"),
label: {
margin: { left: 6 },
...text(layer, "sans", { size: "sm" }),
@@ -120,7 +120,7 @@ export default function statusBar(colorScheme: ColorScheme) {
},
state: {
hovered: {
- iconColor: foreground(layer, "hovered"),
+ icon_color: foreground(layer, "hovered"),
background: background(layer, "variant"),
},
},
@@ -128,22 +128,22 @@ export default function statusBar(colorScheme: ColorScheme) {
state: {
active: {
default: {
- iconColor: foreground(layer, "active"),
+ icon_color: foreground(layer, "active"),
background: background(layer, "active"),
},
hovered: {
- iconColor: foreground(layer, "hovered"),
+ icon_color: foreground(layer, "hovered"),
background: background(layer, "hovered"),
},
clicked: {
- iconColor: foreground(layer, "pressed"),
+ icon_color: foreground(layer, "pressed"),
background: background(layer, "pressed"),
},
},
},
}),
badge: {
- cornerRadius: 3,
+ corner_radius: 3,
padding: 2,
margin: { bottom: -1, right: -1 },
border: border(layer),
@@ -1,13 +1,13 @@
-import { ColorScheme } from "../theme/colorScheme"
-import { withOpacity } from "../theme/color"
+import { ColorScheme } from "../theme/color_scheme"
+import { with_opacity } from "../theme/color"
import { text, border, background, foreground } from "./components"
import { interactive, toggleable } from "../element"
-export default function tabBar(colorScheme: ColorScheme) {
+export default function tab_bar(theme: ColorScheme): any {
const height = 32
- let activeLayer = colorScheme.highest
- let layer = colorScheme.middle
+ const active_layer = theme.highest
+ const layer = theme.middle
const tab = {
height,
@@ -25,16 +25,16 @@ export default function tabBar(colorScheme: ColorScheme) {
spacing: 8,
// Tab type icons (e.g. Project Search)
- typeIconWidth: 14,
+ type_icon_width: 14,
// Close icons
- closeIconWidth: 8,
- iconClose: foreground(layer, "variant"),
- iconCloseActive: foreground(layer, "hovered"),
+ close_icon_width: 8,
+ icon_close: foreground(layer, "variant"),
+ icon_close_active: foreground(layer, "hovered"),
// Indicators
- iconConflict: foreground(layer, "warning"),
- iconDirty: foreground(layer, "accent"),
+ icon_conflict: foreground(layer, "warning"),
+ icon_dirty: foreground(layer, "accent"),
// When two tabs of the same name are open, a label appears next to them
description: {
@@ -43,25 +43,25 @@ export default function tabBar(colorScheme: ColorScheme) {
},
}
- const activePaneActiveTab = {
+ const active_pane_active_tab = {
...tab,
- background: background(activeLayer),
- text: text(activeLayer, "sans", "active", { size: "sm" }),
+ background: background(active_layer),
+ text: text(active_layer, "sans", "active", { size: "sm" }),
border: {
...tab.border,
bottom: false,
},
}
- const inactivePaneInactiveTab = {
+ const inactive_pane_inactive_tab = {
...tab,
background: background(layer),
text: text(layer, "sans", "variant", { size: "sm" }),
}
- const inactivePaneActiveTab = {
+ const inactive_pane_active_tab = {
...tab,
- background: background(activeLayer),
+ background: background(active_layer),
text: text(layer, "sans", "variant", { size: "sm" }),
border: {
...tab.border,
@@ -69,31 +69,31 @@ export default function tabBar(colorScheme: ColorScheme) {
},
}
- const draggedTab = {
- ...activePaneActiveTab,
- background: withOpacity(tab.background, 0.9),
+ const dragged_tab = {
+ ...active_pane_active_tab,
+ background: with_opacity(tab.background, 0.9),
border: undefined as any,
- shadow: colorScheme.popoverShadow,
+ shadow: theme.popover_shadow,
}
return {
height,
background: background(layer),
- activePane: {
- activeTab: activePaneActiveTab,
- inactiveTab: tab,
+ active_pane: {
+ active_tab: active_pane_active_tab,
+ inactive_tab: tab,
},
- inactivePane: {
- activeTab: inactivePaneActiveTab,
- inactiveTab: inactivePaneInactiveTab,
+ inactive_pane: {
+ active_tab: inactive_pane_active_tab,
+ inactive_tab: inactive_pane_inactive_tab,
},
- draggedTab,
- paneButton: toggleable({
+ dragged_tab,
+ pane_button: toggleable({
base: interactive({
base: {
color: foreground(layer, "variant"),
- iconWidth: 12,
- buttonWidth: activePaneActiveTab.height,
+ icon_width: 12,
+ button_width: active_pane_active_tab.height,
},
state: {
hovered: {
@@ -118,7 +118,7 @@ export default function tabBar(colorScheme: ColorScheme) {
},
},
}),
- paneButtonContainer: {
+ pane_button_container: {
background: tab.background,
border: {
...tab.border,
@@ -0,0 +1,52 @@
+import { ColorScheme } from "../theme/color_scheme"
+
+export default function terminal(theme: ColorScheme) {
+ /**
+ * Colors are controlled per-cell in the terminal grid.
+ * Cells can be set to any of these more 'theme-capable' colors
+ * or can be set directly with RGB values.
+ * Here are the common interpretations of these names:
+ * https://en.wikipedia.org/wiki/ANSI_escape_code#Colors
+ */
+ return {
+ black: theme.ramps.neutral(0).hex(),
+ red: theme.ramps.red(0.5).hex(),
+ green: theme.ramps.green(0.5).hex(),
+ yellow: theme.ramps.yellow(0.5).hex(),
+ blue: theme.ramps.blue(0.5).hex(),
+ magenta: theme.ramps.magenta(0.5).hex(),
+ cyan: theme.ramps.cyan(0.5).hex(),
+ white: theme.ramps.neutral(1).hex(),
+ bright_black: theme.ramps.neutral(0.4).hex(),
+ bright_red: theme.ramps.red(0.25).hex(),
+ bright_green: theme.ramps.green(0.25).hex(),
+ bright_yellow: theme.ramps.yellow(0.25).hex(),
+ bright_blue: theme.ramps.blue(0.25).hex(),
+ bright_magenta: theme.ramps.magenta(0.25).hex(),
+ bright_cyan: theme.ramps.cyan(0.25).hex(),
+ bright_white: theme.ramps.neutral(1).hex(),
+ /**
+ * Default color for characters
+ */
+ foreground: theme.ramps.neutral(1).hex(),
+ /**
+ * Default color for the rectangle background of a cell
+ */
+ background: theme.ramps.neutral(0).hex(),
+ modal_background: theme.ramps.neutral(0.1).hex(),
+ /**
+ * Default color for the cursor
+ */
+ cursor: theme.players[0].cursor,
+ dim_black: theme.ramps.neutral(1).hex(),
+ dim_red: theme.ramps.red(0.75).hex(),
+ dim_green: theme.ramps.green(0.75).hex(),
+ dim_yellow: theme.ramps.yellow(0.75).hex(),
+ dim_blue: theme.ramps.blue(0.75).hex(),
+ dim_magenta: theme.ramps.magenta(0.75).hex(),
+ dim_cyan: theme.ramps.cyan(0.75).hex(),
+ dim_white: theme.ramps.neutral(0.6).hex(),
+ bright_foreground: theme.ramps.neutral(1).hex(),
+ dim_foreground: theme.ramps.neutral(0).hex(),
+ }
+}
@@ -2,7 +2,7 @@ import { ColorScheme } from "../common"
import { icon_button, toggleable_icon_button } from "../component/icon_button"
import { toggleable_text_button } from "../component/text_button"
import { interactive, toggleable } from "../element"
-import { withOpacity } from "../theme/color"
+import { with_opacity } from "../theme/color"
import { background, border, foreground, text } from "./components"
const ITEM_SPACING = 8
@@ -17,8 +17,8 @@ function build_spacing(
group: spacing,
item: spacing / 2,
half_item: spacing / 4,
- marginY: (container_height - element_height) / 2,
- marginX: (container_height - element_height) / 2,
+ margin_y: (container_height - element_height) / 2,
+ margin_x: (container_height - element_height) / 2,
}
}
@@ -26,15 +26,15 @@ function call_controls(theme: ColorScheme) {
const button_height = 18
const space = build_spacing(TITLEBAR_HEIGHT, button_height, ITEM_SPACING)
- const marginY = {
- top: space.marginY,
- bottom: space.marginY,
+ const margin_y = {
+ top: space.margin_y,
+ bottom: space.margin_y,
}
return {
toggle_microphone_button: toggleable_icon_button(theme, {
margin: {
- ...marginY,
+ ...margin_y,
left: space.group,
right: space.half_item,
},
@@ -43,7 +43,7 @@ function call_controls(theme: ColorScheme) {
toggle_speakers_button: toggleable_icon_button(theme, {
margin: {
- ...marginY,
+ ...margin_y,
left: space.half_item,
right: space.half_item,
},
@@ -51,7 +51,7 @@ function call_controls(theme: ColorScheme) {
screen_share_button: toggleable_icon_button(theme, {
margin: {
- ...marginY,
+ ...margin_y,
left: space.half_item,
right: space.group,
},
@@ -78,7 +78,7 @@ function user_menu(theme: ColorScheme) {
const button = toggleable({
base: interactive({
base: {
- cornerRadius: 6,
+ corner_radius: 6,
height: button_height,
width: online ? 37 : 24,
padding: {
@@ -150,20 +150,20 @@ function user_menu(theme: ColorScheme) {
}
}
return {
- userMenuButtonOnline: build_button({ online: true }),
- userMenuButtonOffline: build_button({ online: false }),
+ user_menu_button_online: build_button({ online: true }),
+ user_menu_button_offline: build_button({ online: false }),
}
}
-export function titlebar(theme: ColorScheme) {
- const avatarWidth = 15
- const avatarOuterWidth = avatarWidth + 4
- const followerAvatarWidth = 14
- const followerAvatarOuterWidth = followerAvatarWidth + 4
+export function titlebar(theme: ColorScheme): any {
+ const avatar_width = 15
+ const avatar_outer_width = avatar_width + 4
+ const follower_avatar_width = 14
+ const follower_avatar_outer_width = follower_avatar_width + 4
return {
item_spacing: ITEM_SPACING,
- facePileSpacing: 2,
+ face_pile_spacing: 2,
height: TITLEBAR_HEIGHT,
background: background(theme.lowest),
border: border(theme.lowest, { bottom: true }),
@@ -177,21 +177,21 @@ export function titlebar(theme: ColorScheme) {
highlight_color: text(theme.lowest, "sans", "active").color,
// Collaborators
- leaderAvatar: {
- width: avatarWidth,
- outerWidth: avatarOuterWidth,
- cornerRadius: avatarWidth / 2,
- outerCornerRadius: avatarOuterWidth / 2,
+ leader_avatar: {
+ width: avatar_width,
+ outer_width: avatar_outer_width,
+ corner_radius: avatar_width / 2,
+ outer_corner_radius: avatar_outer_width / 2,
},
- followerAvatar: {
- width: followerAvatarWidth,
- outerWidth: followerAvatarOuterWidth,
- cornerRadius: followerAvatarWidth / 2,
- outerCornerRadius: followerAvatarOuterWidth / 2,
+ follower_avatar: {
+ width: follower_avatar_width,
+ outer_width: follower_avatar_outer_width,
+ corner_radius: follower_avatar_width / 2,
+ outer_corner_radius: follower_avatar_outer_width / 2,
},
- inactiveAvatarGrayscale: true,
- followerAvatarOverlap: 8,
- leaderSelection: {
+ inactive_avatar_grayscale: true,
+ follower_avatar_overlap: 8,
+ leader_selection: {
margin: {
top: 4,
bottom: 4,
@@ -202,16 +202,16 @@ export function titlebar(theme: ColorScheme) {
top: 2,
bottom: 2,
},
- cornerRadius: 6,
+ corner_radius: 6,
},
- avatarRibbon: {
+ avatar_ribbon: {
height: 3,
width: 14,
// TODO: Chore: Make avatarRibbon colors driven by the theme rather than being hard coded.
},
sign_in_button: toggleable_text_button(theme, {}),
- offlineIcon: {
+ offline_icon: {
color: foreground(theme.lowest, "variant"),
width: 16,
margin: {
@@ -223,9 +223,9 @@ export function titlebar(theme: ColorScheme) {
},
// When the collaboration server is out of date, show a warning
- outdatedWarning: {
+ outdated_warning: {
...text(theme.lowest, "sans", "warning", { size: "xs" }),
- background: withOpacity(background(theme.lowest, "warning"), 0.3),
+ background: with_opacity(background(theme.lowest, "warning"), 0.3),
border: border(theme.lowest, "warning"),
margin: {
left: ITEM_SPACING,
@@ -234,7 +234,7 @@ export function titlebar(theme: ColorScheme) {
left: 8,
right: 8,
},
- cornerRadius: 6,
+ corner_radius: 6,
},
leave_call_button: icon_button(theme, {
@@ -253,14 +253,14 @@ export function titlebar(theme: ColorScheme) {
}),
// Jewel that notifies you that there are new contact requests
- toggleContactsBadge: {
- cornerRadius: 3,
+ toggle_contacts_badge: {
+ corner_radius: 3,
padding: 2,
margin: { top: 3, left: 3 },
border: border(theme.lowest),
background: foreground(theme.lowest, "accent"),
},
- shareButton: toggleable_text_button(theme, {}),
+ share_button: toggleable_text_button(theme, {}),
user_menu: user_menu(theme),
}
}
@@ -0,0 +1,64 @@
+import { ColorScheme } from "../theme/color_scheme"
+import { background, border, text } from "./components"
+import { interactive, toggleable } from "../element"
+export default function dropdown_menu(theme: ColorScheme): any {
+ return {
+ row_height: 30,
+ background: background(theme.middle),
+ border: border(theme.middle),
+ shadow: theme.popover_shadow,
+ header: interactive({
+ base: {
+ ...text(theme.middle, "sans", { size: "sm" }),
+ secondary_text: text(theme.middle, "sans", {
+ size: "sm",
+ color: "#aaaaaa",
+ }),
+ secondary_text_spacing: 10,
+ padding: { left: 8, right: 8, top: 2, bottom: 2 },
+ corner_radius: 6,
+ background: background(theme.middle, "on"),
+ },
+ state: {
+ hovered: {
+ background: background(theme.middle, "hovered"),
+ },
+ clicked: {
+ background: background(theme.middle, "pressed"),
+ },
+ },
+ }),
+ section_header: {
+ ...text(theme.middle, "sans", { size: "sm" }),
+ padding: { left: 8, right: 8, top: 8, bottom: 8 },
+ },
+ item: toggleable({
+ base: interactive({
+ base: {
+ ...text(theme.middle, "sans", { size: "sm" }),
+ secondary_text_spacing: 10,
+ secondary_text: text(theme.middle, "sans", { size: "sm" }),
+ padding: { left: 18, right: 18, top: 2, bottom: 2 },
+ },
+ state: {
+ hovered: {
+ background: background(theme.middle, "hovered"),
+ ...text(theme.middle, "sans", "hovered", {
+ size: "sm",
+ }),
+ },
+ },
+ }),
+ state: {
+ active: {
+ default: {
+ background: background(theme.middle, "active"),
+ },
+ hovered: {
+ background: background(theme.middle, "hovered"),
+ },
+ },
+ },
+ }),
+ }
+}
@@ -0,0 +1,22 @@
+import { ColorScheme } from "../theme/color_scheme"
+import { background, border, text } from "./components"
+
+export default function tooltip(theme: ColorScheme): any {
+ return {
+ background: background(theme.middle),
+ border: border(theme.middle),
+ padding: { top: 4, bottom: 4, left: 8, right: 8 },
+ margin: { top: 6, left: 6 },
+ shadow: theme.popover_shadow,
+ corner_radius: 6,
+ text: text(theme.middle, "sans", { size: "xs" }),
+ keystroke: {
+ background: background(theme.middle, "on"),
+ corner_radius: 4,
+ margin: { left: 6 },
+ padding: { left: 4, right: 4 },
+ ...text(theme.middle, "mono", "on", { size: "xs", weight: "bold" }),
+ },
+ max_text_width: 200,
+ }
+}
@@ -0,0 +1,39 @@
+import { ColorScheme } from "../theme/color_scheme"
+import { foreground, text } from "./components"
+import { interactive } from "../element"
+
+export default function update_notification(theme: ColorScheme): any {
+ const header_padding = 8
+
+ return {
+ message: {
+ ...text(theme.middle, "sans", { size: "xs" }),
+ margin: { left: header_padding, right: header_padding },
+ },
+ action_message: interactive({
+ base: {
+ ...text(theme.middle, "sans", { size: "xs" }),
+ margin: { left: header_padding, top: 6, bottom: 6 },
+ },
+ state: {
+ hovered: {
+ color: foreground(theme.middle, "hovered"),
+ },
+ },
+ }),
+ dismiss_button: interactive({
+ base: {
+ color: foreground(theme.middle),
+ icon_width: 8,
+ icon_height: 8,
+ button_width: 8,
+ button_height: 8,
+ },
+ state: {
+ hovered: {
+ color: foreground(theme.middle, "hovered"),
+ },
+ },
+ }),
+ }
+}
@@ -0,0 +1,155 @@
+import { ColorScheme } from "../theme/color_scheme"
+import { with_opacity } from "../theme/color"
+import {
+ border,
+ background,
+ foreground,
+ text,
+ TextProperties,
+ svg,
+} from "./components"
+import { interactive } from "../element"
+
+export default function welcome(theme: ColorScheme): any {
+ const checkbox_base = {
+ corner_radius: 4,
+ padding: {
+ left: 3,
+ right: 3,
+ top: 3,
+ bottom: 3,
+ },
+ // shadow: theme.popover_shadow,
+ border: border(theme.highest),
+ margin: {
+ right: 8,
+ top: 5,
+ bottom: 5,
+ },
+ }
+
+ const interactive_text_size: TextProperties = { size: "sm" }
+
+ return {
+ page_width: 320,
+ logo: svg(
+ foreground(theme.highest, "default"),
+ "icons/logo_96.svg",
+ 64,
+ 64
+ ),
+ logo_subheading: {
+ ...text(theme.highest, "sans", "variant", { size: "md" }),
+ margin: {
+ top: 10,
+ bottom: 7,
+ },
+ },
+ button_group: {
+ margin: {
+ top: 8,
+ bottom: 16,
+ },
+ },
+ heading_group: {
+ margin: {
+ top: 8,
+ bottom: 12,
+ },
+ },
+ checkbox_group: {
+ border: border(theme.highest, "variant"),
+ background: with_opacity(
+ background(theme.highest, "hovered"),
+ 0.25
+ ),
+ corner_radius: 4,
+ padding: {
+ left: 12,
+ top: 2,
+ bottom: 2,
+ },
+ },
+ button: interactive({
+ base: {
+ background: background(theme.highest),
+ border: border(theme.highest, "active"),
+ corner_radius: 4,
+ margin: {
+ top: 4,
+ bottom: 4,
+ },
+ padding: {
+ top: 3,
+ bottom: 3,
+ left: 7,
+ right: 7,
+ },
+ ...text(
+ theme.highest,
+ "sans",
+ "default",
+ interactive_text_size
+ ),
+ },
+ state: {
+ hovered: {
+ ...text(
+ theme.highest,
+ "sans",
+ "default",
+ interactive_text_size
+ ),
+ background: background(theme.highest, "hovered"),
+ },
+ },
+ }),
+
+ usage_note: {
+ ...text(theme.highest, "sans", "variant", { size: "2xs" }),
+ padding: {
+ top: -4,
+ },
+ },
+ checkbox_container: {
+ margin: {
+ top: 4,
+ },
+ padding: {
+ bottom: 8,
+ },
+ },
+ checkbox: {
+ label: {
+ ...text(theme.highest, "sans", interactive_text_size),
+ // Also supports margin, container, border, etc.
+ },
+ icon: svg(
+ foreground(theme.highest, "on"),
+ "icons/check_12.svg",
+ 12,
+ 12
+ ),
+ default: {
+ ...checkbox_base,
+ background: background(theme.highest, "default"),
+ border: border(theme.highest, "active"),
+ },
+ checked: {
+ ...checkbox_base,
+ background: background(theme.highest, "hovered"),
+ border: border(theme.highest, "active"),
+ },
+ hovered: {
+ ...checkbox_base,
+ background: background(theme.highest, "hovered"),
+ border: border(theme.highest, "active"),
+ },
+ hovered_and_checked: {
+ ...checkbox_base,
+ background: background(theme.highest, "hovered"),
+ border: border(theme.highest, "active"),
+ },
+ },
+ }
+}
@@ -0,0 +1,190 @@
+import { ColorScheme } from "../theme/color_scheme"
+import { with_opacity } from "../theme/color"
+import {
+ background,
+ border,
+ border_color,
+ foreground,
+ svg,
+ text,
+} from "./components"
+import statusBar from "./status_bar"
+import tabBar from "./tab_bar"
+import { interactive } from "../element"
+
+import { titlebar } from "./titlebar"
+export default function workspace(theme: ColorScheme): any {
+ const { is_light } = theme
+
+ return {
+ background: background(theme.lowest),
+ blank_pane: {
+ logo_container: {
+ width: 256,
+ height: 256,
+ },
+ logo: svg(
+ with_opacity("#000000", theme.is_light ? 0.6 : 0.8),
+ "icons/logo_96.svg",
+ 256,
+ 256
+ ),
+
+ logo_shadow: svg(
+ with_opacity(
+ theme.is_light
+ ? "#FFFFFF"
+ : theme.lowest.base.default.background,
+ theme.is_light ? 1 : 0.6
+ ),
+ "icons/logo_96.svg",
+ 256,
+ 256
+ ),
+ keyboard_hints: {
+ margin: {
+ top: 96,
+ },
+ corner_radius: 4,
+ },
+ keyboard_hint: interactive({
+ base: {
+ ...text(theme.lowest, "sans", "variant", { size: "sm" }),
+ padding: {
+ top: 3,
+ left: 8,
+ right: 8,
+ bottom: 3,
+ },
+ corner_radius: 8,
+ },
+ state: {
+ hovered: {
+ ...text(theme.lowest, "sans", "active", { size: "sm" }),
+ },
+ },
+ }),
+
+ keyboard_hint_width: 320,
+ },
+ joining_project_avatar: {
+ corner_radius: 40,
+ width: 80,
+ },
+ joining_project_message: {
+ padding: 12,
+ ...text(theme.lowest, "sans", { size: "lg" }),
+ },
+ external_location_message: {
+ background: background(theme.middle, "accent"),
+ border: border(theme.middle, "accent"),
+ corner_radius: 6,
+ padding: 12,
+ margin: { bottom: 8, right: 8 },
+ ...text(theme.middle, "sans", "accent", { size: "xs" }),
+ },
+ leader_border_opacity: 0.7,
+ leader_border_width: 2.0,
+ tab_bar: tabBar(theme),
+ modal: {
+ margin: {
+ bottom: 52,
+ top: 52,
+ },
+ cursor: "Arrow",
+ },
+ zoomed_background: {
+ cursor: "Arrow",
+ background: is_light
+ ? with_opacity(background(theme.lowest), 0.8)
+ : with_opacity(background(theme.highest), 0.6),
+ },
+ zoomed_pane_foreground: {
+ margin: 16,
+ shadow: theme.modal_shadow,
+ border: border(theme.lowest, { overlay: true }),
+ },
+ zoomed_panel_foreground: {
+ margin: 16,
+ border: border(theme.lowest, { overlay: true }),
+ },
+ dock: {
+ left: {
+ border: border(theme.lowest, { right: true }),
+ },
+ bottom: {
+ border: border(theme.lowest, { top: true }),
+ },
+ right: {
+ border: border(theme.lowest, { left: true }),
+ },
+ },
+ pane_divider: {
+ color: border_color(theme.lowest),
+ width: 1,
+ },
+ status_bar: statusBar(theme),
+ titlebar: titlebar(theme),
+ toolbar: {
+ height: 34,
+ background: background(theme.highest),
+ border: border(theme.highest, { bottom: true }),
+ item_spacing: 8,
+ nav_button: interactive({
+ base: {
+ color: foreground(theme.highest, "on"),
+ icon_width: 12,
+ button_width: 24,
+ corner_radius: 6,
+ },
+ state: {
+ hovered: {
+ color: foreground(theme.highest, "on", "hovered"),
+ background: background(theme.highest, "on", "hovered"),
+ },
+ disabled: {
+ color: foreground(theme.highest, "on", "disabled"),
+ },
+ },
+ }),
+ padding: { left: 8, right: 8, top: 4, bottom: 4 },
+ },
+ breadcrumb_height: 24,
+ breadcrumbs: interactive({
+ base: {
+ ...text(theme.highest, "sans", "variant"),
+ corner_radius: 6,
+ padding: {
+ left: 6,
+ right: 6,
+ },
+ },
+ state: {
+ hovered: {
+ color: foreground(theme.highest, "on", "hovered"),
+ background: background(theme.highest, "on", "hovered"),
+ },
+ },
+ }),
+ disconnected_overlay: {
+ ...text(theme.lowest, "sans"),
+ background: with_opacity(background(theme.lowest), 0.8),
+ },
+ notification: {
+ margin: { top: 10 },
+ background: background(theme.middle),
+ corner_radius: 6,
+ padding: 12,
+ border: border(theme.middle),
+ shadow: theme.popover_shadow,
+ },
+ notifications: {
+ width: 400,
+ margin: { right: 10, bottom: 10 },
+ },
+ drop_target_overlay_color: with_opacity(
+ foreground(theme.lowest, "variant"),
+ 0.5
+ ),
+ }
+}
@@ -1,11 +0,0 @@
-/** Converts a percentage scale value (0-100) to normalized scale (0-1) value. */
-export function percentageToNormalized(value: number) {
- const normalized = value / 100
- return normalized
-}
-
-/** Converts a normalized scale (0-1) value to a percentage scale (0-100) value. */
-export function normalizedToPercetage(value: number) {
- const percentage = value * 100
- return percentage
-}
@@ -1,26 +0,0 @@
-import bezier from "bezier-easing"
-import { Curve } from "../ref/curves"
-
-/**
- * Formats our Curve data structure into a bezier easing function.
- * @param {Curve} curve - The curve to format.
- * @param {Boolean} inverted - Whether or not to invert the curve.
- * @returns {EasingFunction} The formatted easing function.
- */
-export function curve(curve: Curve, inverted?: Boolean) {
- if (inverted) {
- return bezier(
- curve.value[3],
- curve.value[2],
- curve.value[1],
- curve.value[0]
- )
- }
-
- return bezier(
- curve.value[0],
- curve.value[1],
- curve.value[2],
- curve.value[3]
- )
-}
@@ -1,159 +0,0 @@
-import bezier from "bezier-easing"
-import chroma from "chroma-js"
-import { Color, ColorFamily, ColorFamilyConfig, ColorScale } from "../types"
-import { percentageToNormalized } from "./convert"
-import { curve } from "./curve"
-
-// Re-export interface in a more standard format
-export type EasingFunction = bezier.EasingFunction
-
-/**
- * Generates a color, outputs it in multiple formats, and returns a variety of useful metadata.
- *
- * @param {EasingFunction} hueEasing - An easing function for the hue component of the color.
- * @param {EasingFunction} saturationEasing - An easing function for the saturation component of the color.
- * @param {EasingFunction} lightnessEasing - An easing function for the lightness component of the color.
- * @param {ColorFamilyConfig} family - Configuration for the color family.
- * @param {number} step - The current step.
- * @param {number} steps - The total number of steps in the color scale.
- *
- * @returns {Color} The generated color, with its calculated contrast against black and white, as well as its LCH values, RGBA array, hexadecimal representation, and a flag indicating if it is light or dark.
- */
-function generateColor(
- hueEasing: EasingFunction,
- saturationEasing: EasingFunction,
- lightnessEasing: EasingFunction,
- family: ColorFamilyConfig,
- step: number,
- steps: number
-) {
- const { hue, saturation, lightness } = family.color
-
- const stepHue = hueEasing(step / steps) * (hue.end - hue.start) + hue.start
- const stepSaturation =
- saturationEasing(step / steps) * (saturation.end - saturation.start) +
- saturation.start
- const stepLightness =
- lightnessEasing(step / steps) * (lightness.end - lightness.start) +
- lightness.start
-
- const color = chroma.hsl(
- stepHue,
- percentageToNormalized(stepSaturation),
- percentageToNormalized(stepLightness)
- )
-
- const contrast = {
- black: {
- value: chroma.contrast(color, "black"),
- aaPass: chroma.contrast(color, "black") >= 4.5,
- aaaPass: chroma.contrast(color, "black") >= 7,
- },
- white: {
- value: chroma.contrast(color, "white"),
- aaPass: chroma.contrast(color, "white") >= 4.5,
- aaaPass: chroma.contrast(color, "white") >= 7,
- },
- }
-
- const lch = color.lch()
- const rgba = color.rgba()
- const hex = color.hex()
-
- // 55 is a magic number. It's the lightness value at which we consider a color to be "light".
- // It was picked by eye with some testing. We might want to use a more scientific approach in the future.
- const isLight = lch[0] > 55
-
- const result: Color = {
- step,
- lch,
- hex,
- rgba,
- contrast,
- isLight,
- }
-
- return result
-}
-
-/**
- * Generates a color scale based on a color family configuration.
- *
- * @param {ColorFamilyConfig} config - The configuration for the color family.
- * @param {Boolean} inverted - Specifies whether the color scale should be inverted or not.
- *
- * @returns {ColorScale} The generated color scale.
- *
- * @example
- * ```ts
- * const colorScale = generateColorScale({
- * name: "blue",
- * color: {
- * hue: {
- * start: 210,
- * end: 240,
- * curve: "easeInOut"
- * },
- * saturation: {
- * start: 100,
- * end: 100,
- * curve: "easeInOut"
- * },
- * lightness: {
- * start: 50,
- * end: 50,
- * curve: "easeInOut"
- * }
- * }
- * });
- * ```
- */
-
-export function generateColorScale(
- config: ColorFamilyConfig,
- inverted: Boolean = false
-) {
- const { hue, saturation, lightness } = config.color
-
- // 101 steps means we get values from 0-100
- const NUM_STEPS = 101
-
- const hueEasing = curve(hue.curve, inverted)
- const saturationEasing = curve(saturation.curve, inverted)
- const lightnessEasing = curve(lightness.curve, inverted)
-
- let scale: ColorScale = {
- colors: [],
- values: [],
- }
-
- for (let i = 0; i < NUM_STEPS; i++) {
- const color = generateColor(
- hueEasing,
- saturationEasing,
- lightnessEasing,
- config,
- i,
- NUM_STEPS
- )
-
- scale.colors.push(color)
- scale.values.push(color.hex)
- }
-
- return scale
-}
-
-/** Generates a color family with a scale and an inverted scale. */
-export function generateColorFamily(config: ColorFamilyConfig) {
- const scale = generateColorScale(config, false)
- const invertedScale = generateColorScale(config, true)
-
- const family: ColorFamily = {
- name: config.name,
- scale,
- invertedScale,
- }
-
- return family
-}
@@ -1,445 +0,0 @@
-import { generateColorFamily } from "../lib/generate"
-import { curve } from "./curves"
-
-// These are the source colors for the color scales in the system.
-// These should never directly be used directly in components or themes as they generate thousands of lines of code.
-// Instead, use the outputs from the reference palette which exports a smaller subset of colors.
-
-// Token or user-facing colors should use short, clear names and a 100-900 scale to match the font weight scale.
-
-// Light Gray ======================================== //
-
-export const lightgray = generateColorFamily({
- name: "lightgray",
- color: {
- hue: {
- start: 210,
- end: 210,
- curve: curve.linear,
- },
- saturation: {
- start: 10,
- end: 15,
- curve: curve.saturation,
- },
- lightness: {
- start: 97,
- end: 50,
- curve: curve.linear,
- },
- },
-})
-
-// Light Dark ======================================== //
-
-export const darkgray = generateColorFamily({
- name: "darkgray",
- color: {
- hue: {
- start: 210,
- end: 210,
- curve: curve.linear,
- },
- saturation: {
- start: 15,
- end: 20,
- curve: curve.saturation,
- },
- lightness: {
- start: 55,
- end: 8,
- curve: curve.linear,
- },
- },
-})
-
-// Red ======================================== //
-
-export const red = generateColorFamily({
- name: "red",
- color: {
- hue: {
- start: 0,
- end: 0,
- curve: curve.linear,
- },
- saturation: {
- start: 95,
- end: 75,
- curve: curve.saturation,
- },
- lightness: {
- start: 97,
- end: 25,
- curve: curve.lightness,
- },
- },
-})
-
-// Sunset ======================================== //
-
-export const sunset = generateColorFamily({
- name: "sunset",
- color: {
- hue: {
- start: 15,
- end: 15,
- curve: curve.linear,
- },
- saturation: {
- start: 100,
- end: 90,
- curve: curve.saturation,
- },
- lightness: {
- start: 97,
- end: 25,
- curve: curve.lightness,
- },
- },
-})
-
-// Orange ======================================== //
-
-export const orange = generateColorFamily({
- name: "orange",
- color: {
- hue: {
- start: 25,
- end: 25,
- curve: curve.linear,
- },
- saturation: {
- start: 100,
- end: 95,
- curve: curve.saturation,
- },
- lightness: {
- start: 97,
- end: 20,
- curve: curve.lightness,
- },
- },
-})
-
-// Amber ======================================== //
-
-export const amber = generateColorFamily({
- name: "amber",
- color: {
- hue: {
- start: 38,
- end: 38,
- curve: curve.linear,
- },
- saturation: {
- start: 100,
- end: 100,
- curve: curve.saturation,
- },
- lightness: {
- start: 97,
- end: 18,
- curve: curve.lightness,
- },
- },
-})
-
-// Yellow ======================================== //
-
-export const yellow = generateColorFamily({
- name: "yellow",
- color: {
- hue: {
- start: 48,
- end: 48,
- curve: curve.linear,
- },
- saturation: {
- start: 90,
- end: 100,
- curve: curve.saturation,
- },
- lightness: {
- start: 97,
- end: 15,
- curve: curve.lightness,
- },
- },
-})
-
-// Lemon ======================================== //
-
-export const lemon = generateColorFamily({
- name: "lemon",
- color: {
- hue: {
- start: 55,
- end: 55,
- curve: curve.linear,
- },
- saturation: {
- start: 85,
- end: 95,
- curve: curve.saturation,
- },
- lightness: {
- start: 97,
- end: 15,
- curve: curve.lightness,
- },
- },
-})
-
-// Citron ======================================== //
-
-export const citron = generateColorFamily({
- name: "citron",
- color: {
- hue: {
- start: 70,
- end: 70,
- curve: curve.linear,
- },
- saturation: {
- start: 85,
- end: 90,
- curve: curve.saturation,
- },
- lightness: {
- start: 97,
- end: 15,
- curve: curve.lightness,
- },
- },
-})
-
-// Lime ======================================== //
-
-export const lime = generateColorFamily({
- name: "lime",
- color: {
- hue: {
- start: 85,
- end: 85,
- curve: curve.linear,
- },
- saturation: {
- start: 85,
- end: 80,
- curve: curve.saturation,
- },
- lightness: {
- start: 97,
- end: 18,
- curve: curve.lightness,
- },
- },
-})
-
-// Green ======================================== //
-
-export const green = generateColorFamily({
- name: "green",
- color: {
- hue: {
- start: 108,
- end: 108,
- curve: curve.linear,
- },
- saturation: {
- start: 60,
- end: 70,
- curve: curve.saturation,
- },
- lightness: {
- start: 97,
- end: 18,
- curve: curve.lightness,
- },
- },
-})
-
-// Mint ======================================== //
-
-export const mint = generateColorFamily({
- name: "mint",
- color: {
- hue: {
- start: 142,
- end: 142,
- curve: curve.linear,
- },
- saturation: {
- start: 60,
- end: 75,
- curve: curve.saturation,
- },
- lightness: {
- start: 97,
- end: 20,
- curve: curve.lightness,
- },
- },
-})
-
-// Cyan ======================================== //
-
-export const cyan = generateColorFamily({
- name: "cyan",
- color: {
- hue: {
- start: 179,
- end: 179,
- curve: curve.linear,
- },
- saturation: {
- start: 70,
- end: 80,
- curve: curve.saturation,
- },
- lightness: {
- start: 97,
- end: 20,
- curve: curve.lightness,
- },
- },
-})
-
-// Sky ======================================== //
-
-export const sky = generateColorFamily({
- name: "sky",
- color: {
- hue: {
- start: 195,
- end: 205,
- curve: curve.linear,
- },
- saturation: {
- start: 85,
- end: 90,
- curve: curve.saturation,
- },
- lightness: {
- start: 97,
- end: 15,
- curve: curve.lightness,
- },
- },
-})
-
-// Blue ======================================== //
-
-export const blue = generateColorFamily({
- name: "blue",
- color: {
- hue: {
- start: 218,
- end: 218,
- curve: curve.linear,
- },
- saturation: {
- start: 85,
- end: 70,
- curve: curve.saturation,
- },
- lightness: {
- start: 97,
- end: 15,
- curve: curve.lightness,
- },
- },
-})
-
-// Indigo ======================================== //
-
-export const indigo = generateColorFamily({
- name: "indigo",
- color: {
- hue: {
- start: 245,
- end: 245,
- curve: curve.linear,
- },
- saturation: {
- start: 60,
- end: 50,
- curve: curve.saturation,
- },
- lightness: {
- start: 97,
- end: 22,
- curve: curve.lightness,
- },
- },
-})
-
-// Purple ======================================== //
-
-export const purple = generateColorFamily({
- name: "purple",
- color: {
- hue: {
- start: 260,
- end: 270,
- curve: curve.linear,
- },
- saturation: {
- start: 65,
- end: 55,
- curve: curve.saturation,
- },
- lightness: {
- start: 97,
- end: 20,
- curve: curve.lightness,
- },
- },
-})
-
-// Pink ======================================== //
-
-export const pink = generateColorFamily({
- name: "pink",
- color: {
- hue: {
- start: 320,
- end: 330,
- curve: curve.linear,
- },
- saturation: {
- start: 70,
- end: 65,
- curve: curve.saturation,
- },
- lightness: {
- start: 97,
- end: 32,
- curve: curve.lightness,
- },
- },
-})
-
-// Rose ======================================== //
-
-export const rose = generateColorFamily({
- name: "rose",
- color: {
- hue: {
- start: 345,
- end: 345,
- curve: curve.linear,
- },
- saturation: {
- start: 90,
- end: 70,
- curve: curve.saturation,
- },
- lightness: {
- start: 97,
- end: 32,
- curve: curve.lightness,
- },
- },
-})
@@ -1,25 +0,0 @@
-export interface Curve {
- name: string
- value: number[]
-}
-
-export interface Curves {
- lightness: Curve
- saturation: Curve
- linear: Curve
-}
-
-export const curve: Curves = {
- lightness: {
- name: "lightnessCurve",
- value: [0.2, 0, 0.75, 1.0],
- },
- saturation: {
- name: "saturationCurve",
- value: [0.67, 0.6, 0.55, 1.0],
- },
- linear: {
- name: "linear",
- value: [0.5, 0.5, 0.5, 0.5],
- },
-}
@@ -1,32 +0,0 @@
-import chroma from "chroma-js"
-import * as colorFamily from "./ref/color"
-
-const color = {
- lightgray: chroma
- .scale(colorFamily.lightgray.scale.values)
- .mode("lch")
- .colors(9),
- darkgray: chroma
- .scale(colorFamily.darkgray.scale.values)
- .mode("lch")
- .colors(9),
- red: chroma.scale(colorFamily.red.scale.values).mode("lch").colors(9),
- sunset: chroma.scale(colorFamily.sunset.scale.values).mode("lch").colors(9),
- orange: chroma.scale(colorFamily.orange.scale.values).mode("lch").colors(9),
- amber: chroma.scale(colorFamily.amber.scale.values).mode("lch").colors(9),
- yellow: chroma.scale(colorFamily.yellow.scale.values).mode("lch").colors(9),
- lemon: chroma.scale(colorFamily.lemon.scale.values).mode("lch").colors(9),
- citron: chroma.scale(colorFamily.citron.scale.values).mode("lch").colors(9),
- lime: chroma.scale(colorFamily.lime.scale.values).mode("lch").colors(9),
- green: chroma.scale(colorFamily.green.scale.values).mode("lch").colors(9),
- mint: chroma.scale(colorFamily.mint.scale.values).mode("lch").colors(9),
- cyan: chroma.scale(colorFamily.cyan.scale.values).mode("lch").colors(9),
- sky: chroma.scale(colorFamily.sky.scale.values).mode("lch").colors(9),
- blue: chroma.scale(colorFamily.blue.scale.values).mode("lch").colors(9),
- indigo: chroma.scale(colorFamily.indigo.scale.values).mode("lch").colors(9),
- purple: chroma.scale(colorFamily.purple.scale.values).mode("lch").colors(9),
- pink: chroma.scale(colorFamily.pink.scale.values).mode("lch").colors(9),
- rose: chroma.scale(colorFamily.rose.scale.values).mode("lch").colors(9),
-}
-
-export { color }
@@ -1,66 +0,0 @@
-import { Curve } from "./ref/curves"
-
-export interface ColorAccessibilityValue {
- value: number
- aaPass: boolean
- aaaPass: boolean
-}
-
-/**
- * Calculates the color contrast between a specified color and its corresponding background and foreground colors.
- *
- * @note This implementation is currently basic β Currently we only calculate contrasts against black and white, in the future will allow for dynamic color contrast calculation based on the colors present in a given palette.
- * @note The goal is to align with WCAG3 accessibility standards as they become stabilized. See the [WCAG 3 Introduction](https://www.w3.org/WAI/standards-guidelines/wcag/wcag3-intro/) for more information.
- */
-export interface ColorAccessibility {
- black: ColorAccessibilityValue
- white: ColorAccessibilityValue
-}
-
-export type Color = {
- step: number
- contrast: ColorAccessibility
- hex: string
- lch: number[]
- rgba: number[]
- isLight: boolean
-}
-
-export interface ColorScale {
- colors: Color[]
- // An array of hex values for each color in the scale
- values: string[]
-}
-
-export type ColorFamily = {
- name: string
- scale: ColorScale
- invertedScale: ColorScale
-}
-
-export interface ColorFamilyHue {
- start: number
- end: number
- curve: Curve
-}
-
-export interface ColorFamilySaturation {
- start: number
- end: number
- curve: Curve
-}
-
-export interface ColorFamilyLightness {
- start: number
- end: number
- curve: Curve
-}
-
-export interface ColorFamilyConfig {
- name: string
- color: {
- hue: ColorFamilyHue
- saturation: ColorFamilySaturation
- lightness: ColorFamilyLightness
- }
-}
@@ -1,5 +1,5 @@
import chroma from "chroma-js"
-export function withOpacity(color: string, opacity: number): string {
+export function with_opacity(color: string, opacity: number): string {
return chroma(color).alpha(opacity).hex()
}
@@ -1,286 +0,0 @@
-import { Scale, Color } from "chroma-js"
-import { Syntax, ThemeSyntax, SyntaxHighlightStyle } from "./syntax"
-export { Syntax, ThemeSyntax, SyntaxHighlightStyle }
-import {
- ThemeConfig,
- ThemeAppearance,
- ThemeConfigInputColors,
-} from "./themeConfig"
-import { getRamps } from "./ramps"
-
-export interface ColorScheme {
- name: string
- isLight: boolean
-
- lowest: Layer
- middle: Layer
- highest: Layer
-
- ramps: RampSet
-
- popoverShadow: Shadow
- modalShadow: Shadow
-
- players: Players
- syntax?: Partial<ThemeSyntax>
-}
-
-export interface Meta {
- name: string
- author: string
- url: string
- license: License
-}
-
-export interface License {
- SPDX: SPDXExpression
-}
-
-// License name -> License text
-export interface Licenses {
- [key: string]: string
-}
-
-// FIXME: Add support for the SPDX expression syntax
-export type SPDXExpression = "MIT"
-
-export interface Player {
- cursor: string
- selection: string
-}
-
-export interface Players {
- "0": Player
- "1": Player
- "2": Player
- "3": Player
- "4": Player
- "5": Player
- "6": Player
- "7": Player
-}
-
-export interface Shadow {
- blur: number
- color: string
- offset: number[]
-}
-
-export type StyleSets = keyof Layer
-export interface Layer {
- base: StyleSet
- variant: StyleSet
- on: StyleSet
- accent: StyleSet
- positive: StyleSet
- warning: StyleSet
- negative: StyleSet
-}
-
-export interface RampSet {
- neutral: Scale
- red: Scale
- orange: Scale
- yellow: Scale
- green: Scale
- cyan: Scale
- blue: Scale
- violet: Scale
- magenta: Scale
-}
-
-export type Styles = keyof StyleSet
-export interface StyleSet {
- default: Style
- active: Style
- disabled: Style
- hovered: Style
- pressed: Style
- inverted: Style
-}
-
-export interface Style {
- background: string
- border: string
- foreground: string
-}
-
-export function createColorScheme(theme: ThemeConfig): ColorScheme {
- const {
- name,
- appearance,
- inputColor,
- override: { syntax },
- } = theme
-
- const isLight = appearance === ThemeAppearance.Light
- const colorRamps: ThemeConfigInputColors = inputColor
-
- // Chromajs scales from 0 to 1 flipped if isLight is true
- const ramps = getRamps(isLight, colorRamps)
- const lowest = lowestLayer(ramps)
- const middle = middleLayer(ramps)
- const highest = highestLayer(ramps)
-
- const popoverShadow = {
- blur: 4,
- color: ramps
- .neutral(isLight ? 7 : 0)
- .darken()
- .alpha(0.2)
- .hex(), // TODO used blend previously. Replace with something else
- offset: [1, 2],
- }
-
- const modalShadow = {
- blur: 16,
- color: ramps
- .neutral(isLight ? 7 : 0)
- .darken()
- .alpha(0.2)
- .hex(), // TODO used blend previously. Replace with something else
- offset: [0, 2],
- }
-
- const players = {
- "0": player(ramps.blue),
- "1": player(ramps.green),
- "2": player(ramps.magenta),
- "3": player(ramps.orange),
- "4": player(ramps.violet),
- "5": player(ramps.cyan),
- "6": player(ramps.red),
- "7": player(ramps.yellow),
- }
-
- return {
- name,
- isLight,
-
- ramps,
-
- lowest,
- middle,
- highest,
-
- popoverShadow,
- modalShadow,
-
- players,
- syntax,
- }
-}
-
-function player(ramp: Scale): Player {
- return {
- selection: ramp(0.5).alpha(0.24).hex(),
- cursor: ramp(0.5).hex(),
- }
-}
-
-function lowestLayer(ramps: RampSet): Layer {
- return {
- base: buildStyleSet(ramps.neutral, 0.2, 1),
- variant: buildStyleSet(ramps.neutral, 0.2, 0.7),
- on: buildStyleSet(ramps.neutral, 0.1, 1),
- accent: buildStyleSet(ramps.blue, 0.1, 0.5),
- positive: buildStyleSet(ramps.green, 0.1, 0.5),
- warning: buildStyleSet(ramps.yellow, 0.1, 0.5),
- negative: buildStyleSet(ramps.red, 0.1, 0.5),
- }
-}
-
-function middleLayer(ramps: RampSet): Layer {
- return {
- base: buildStyleSet(ramps.neutral, 0.1, 1),
- variant: buildStyleSet(ramps.neutral, 0.1, 0.7),
- on: buildStyleSet(ramps.neutral, 0, 1),
- accent: buildStyleSet(ramps.blue, 0.1, 0.5),
- positive: buildStyleSet(ramps.green, 0.1, 0.5),
- warning: buildStyleSet(ramps.yellow, 0.1, 0.5),
- negative: buildStyleSet(ramps.red, 0.1, 0.5),
- }
-}
-
-function highestLayer(ramps: RampSet): Layer {
- return {
- base: buildStyleSet(ramps.neutral, 0, 1),
- variant: buildStyleSet(ramps.neutral, 0, 0.7),
- on: buildStyleSet(ramps.neutral, 0.1, 1),
- accent: buildStyleSet(ramps.blue, 0.1, 0.5),
- positive: buildStyleSet(ramps.green, 0.1, 0.5),
- warning: buildStyleSet(ramps.yellow, 0.1, 0.5),
- negative: buildStyleSet(ramps.red, 0.1, 0.5),
- }
-}
-
-function buildStyleSet(
- ramp: Scale,
- backgroundBase: number,
- foregroundBase: number,
- step: number = 0.08
-): StyleSet {
- let styleDefinitions = buildStyleDefinition(
- backgroundBase,
- foregroundBase,
- step
- )
-
- function colorString(indexOrColor: number | Color): string {
- if (typeof indexOrColor === "number") {
- return ramp(indexOrColor).hex()
- } else {
- return indexOrColor.hex()
- }
- }
-
- function buildStyle(style: Styles): Style {
- return {
- background: colorString(styleDefinitions.background[style]),
- border: colorString(styleDefinitions.border[style]),
- foreground: colorString(styleDefinitions.foreground[style]),
- }
- }
-
- return {
- default: buildStyle("default"),
- hovered: buildStyle("hovered"),
- pressed: buildStyle("pressed"),
- active: buildStyle("active"),
- disabled: buildStyle("disabled"),
- inverted: buildStyle("inverted"),
- }
-}
-
-function buildStyleDefinition(
- bgBase: number,
- fgBase: number,
- step: number = 0.08
-) {
- return {
- background: {
- default: bgBase,
- hovered: bgBase + step,
- pressed: bgBase + step * 1.5,
- active: bgBase + step * 2.2,
- disabled: bgBase,
- inverted: fgBase + step * 6,
- },
- border: {
- default: bgBase + step * 1,
- hovered: bgBase + step,
- pressed: bgBase + step,
- active: bgBase + step * 3,
- disabled: bgBase + step * 0.5,
- inverted: bgBase - step * 3,
- },
- foreground: {
- default: fgBase,
- hovered: fgBase,
- pressed: fgBase,
- active: fgBase + step * 6,
- disabled: bgBase + step * 4,
- inverted: bgBase + step * 2,
- },
- }
-}
@@ -0,0 +1,282 @@
+import { Scale, Color } from "chroma-js"
+import { Syntax, ThemeSyntax, SyntaxHighlightStyle } from "./syntax"
+export { Syntax, ThemeSyntax, SyntaxHighlightStyle }
+import {
+ ThemeConfig,
+ ThemeAppearance,
+ ThemeConfigInputColors,
+} from "./theme_config"
+import { get_ramps } from "./ramps"
+
+export interface ColorScheme {
+ name: string
+ is_light: boolean
+
+ lowest: Layer
+ middle: Layer
+ highest: Layer
+
+ ramps: RampSet
+
+ popover_shadow: Shadow
+ modal_shadow: Shadow
+
+ players: Players
+ syntax?: Partial<ThemeSyntax>
+}
+
+export interface Meta {
+ name: string
+ author: string
+ url: string
+ license: License
+}
+
+export interface License {
+ SPDX: SPDXExpression
+}
+
+// License name -> License text
+export interface Licenses {
+ [key: string]: string
+}
+
+// FIXME: Add support for the SPDX expression syntax
+export type SPDXExpression = "MIT"
+
+export interface Player {
+ cursor: string
+ selection: string
+}
+
+export interface Players {
+ "0": Player
+ "1": Player
+ "2": Player
+ "3": Player
+ "4": Player
+ "5": Player
+ "6": Player
+ "7": Player
+}
+
+export interface Shadow {
+ blur: number
+ color: string
+ offset: number[]
+}
+
+export type StyleSets = keyof Layer
+export interface Layer {
+ base: StyleSet
+ variant: StyleSet
+ on: StyleSet
+ accent: StyleSet
+ positive: StyleSet
+ warning: StyleSet
+ negative: StyleSet
+}
+
+export interface RampSet {
+ neutral: Scale
+ red: Scale
+ orange: Scale
+ yellow: Scale
+ green: Scale
+ cyan: Scale
+ blue: Scale
+ violet: Scale
+ magenta: Scale
+}
+
+export type Styles = keyof StyleSet
+export interface StyleSet {
+ default: Style
+ active: Style
+ disabled: Style
+ hovered: Style
+ pressed: Style
+ inverted: Style
+}
+
+export interface Style {
+ background: string
+ border: string
+ foreground: string
+}
+
+export function create_color_scheme(theme: ThemeConfig): ColorScheme {
+ const {
+ name,
+ appearance,
+ input_color,
+ override: { syntax },
+ } = theme
+
+ const is_light = appearance === ThemeAppearance.Light
+ const color_ramps: ThemeConfigInputColors = input_color
+
+ // Chromajs scales from 0 to 1 flipped if is_light is true
+ const ramps = get_ramps(is_light, color_ramps)
+ const lowest = lowest_layer(ramps)
+ const middle = middle_layer(ramps)
+ const highest = highest_layer(ramps)
+
+ const popover_shadow = {
+ blur: 4,
+ color: ramps
+ .neutral(is_light ? 7 : 0)
+ .darken()
+ .alpha(0.2)
+ .hex(), // TODO used blend previously. Replace with something else
+ offset: [1, 2],
+ }
+
+ const modal_shadow = {
+ blur: 16,
+ color: ramps
+ .neutral(is_light ? 7 : 0)
+ .darken()
+ .alpha(0.2)
+ .hex(), // TODO used blend previously. Replace with something else
+ offset: [0, 2],
+ }
+
+ const players = {
+ "0": player(ramps.blue),
+ "1": player(ramps.green),
+ "2": player(ramps.magenta),
+ "3": player(ramps.orange),
+ "4": player(ramps.violet),
+ "5": player(ramps.cyan),
+ "6": player(ramps.red),
+ "7": player(ramps.yellow),
+ }
+
+ return {
+ name,
+ is_light,
+
+ ramps,
+
+ lowest,
+ middle,
+ highest,
+
+ popover_shadow,
+ modal_shadow,
+
+ players,
+ syntax,
+ }
+}
+
+function player(ramp: Scale): Player {
+ return {
+ selection: ramp(0.5).alpha(0.24).hex(),
+ cursor: ramp(0.5).hex(),
+ }
+}
+
+function lowest_layer(ramps: RampSet): Layer {
+ return {
+ base: build_style_set(ramps.neutral, 0.2, 1),
+ variant: build_style_set(ramps.neutral, 0.2, 0.7),
+ on: build_style_set(ramps.neutral, 0.1, 1),
+ accent: build_style_set(ramps.blue, 0.1, 0.5),
+ positive: build_style_set(ramps.green, 0.1, 0.5),
+ warning: build_style_set(ramps.yellow, 0.1, 0.5),
+ negative: build_style_set(ramps.red, 0.1, 0.5),
+ }
+}
+
+function middle_layer(ramps: RampSet): Layer {
+ return {
+ base: build_style_set(ramps.neutral, 0.1, 1),
+ variant: build_style_set(ramps.neutral, 0.1, 0.7),
+ on: build_style_set(ramps.neutral, 0, 1),
+ accent: build_style_set(ramps.blue, 0.1, 0.5),
+ positive: build_style_set(ramps.green, 0.1, 0.5),
+ warning: build_style_set(ramps.yellow, 0.1, 0.5),
+ negative: build_style_set(ramps.red, 0.1, 0.5),
+ }
+}
+
+function highest_layer(ramps: RampSet): Layer {
+ return {
+ base: build_style_set(ramps.neutral, 0, 1),
+ variant: build_style_set(ramps.neutral, 0, 0.7),
+ on: build_style_set(ramps.neutral, 0.1, 1),
+ accent: build_style_set(ramps.blue, 0.1, 0.5),
+ positive: build_style_set(ramps.green, 0.1, 0.5),
+ warning: build_style_set(ramps.yellow, 0.1, 0.5),
+ negative: build_style_set(ramps.red, 0.1, 0.5),
+ }
+}
+
+function build_style_set(
+ ramp: Scale,
+ background_base: number,
+ foreground_base: number,
+ step = 0.08
+): StyleSet {
+ const style_definitions = build_style_definition(
+ background_base,
+ foreground_base,
+ step
+ )
+
+ function color_string(index_or_color: number | Color): string {
+ if (typeof index_or_color === "number") {
+ return ramp(index_or_color).hex()
+ } else {
+ return index_or_color.hex()
+ }
+ }
+
+ function build_style(style: Styles): Style {
+ return {
+ background: color_string(style_definitions.background[style]),
+ border: color_string(style_definitions.border[style]),
+ foreground: color_string(style_definitions.foreground[style]),
+ }
+ }
+
+ return {
+ default: build_style("default"),
+ hovered: build_style("hovered"),
+ pressed: build_style("pressed"),
+ active: build_style("active"),
+ disabled: build_style("disabled"),
+ inverted: build_style("inverted"),
+ }
+}
+
+function build_style_definition(bg_base: number, fg_base: number, step = 0.08) {
+ return {
+ background: {
+ default: bg_base,
+ hovered: bg_base + step,
+ pressed: bg_base + step * 1.5,
+ active: bg_base + step * 2.2,
+ disabled: bg_base,
+ inverted: fg_base + step * 6,
+ },
+ border: {
+ default: bg_base + step * 1,
+ hovered: bg_base + step,
+ pressed: bg_base + step,
+ active: bg_base + step * 3,
+ disabled: bg_base + step * 0.5,
+ inverted: bg_base - step * 3,
+ },
+ foreground: {
+ default: fg_base,
+ hovered: fg_base,
+ pressed: fg_base,
+ active: fg_base + step * 6,
+ disabled: bg_base + step * 4,
+ inverted: bg_base + step * 2,
+ },
+ }
+}
@@ -1,4 +1,4 @@
-export * from "./colorScheme"
+export * from "./color_scheme"
export * from "./ramps"
export * from "./syntax"
-export * from "./themeConfig"
+export * from "./theme_config"
@@ -1,14 +1,14 @@
import chroma, { Color, Scale } from "chroma-js"
-import { RampSet } from "./colorScheme"
+import { RampSet } from "./color_scheme"
import {
ThemeConfigInputColors,
ThemeConfigInputColorsKeys,
-} from "./themeConfig"
+} from "./theme_config"
-export function colorRamp(color: Color): Scale {
- let endColor = color.desaturate(1).brighten(5)
- let startColor = color.desaturate(1).darken(4)
- return chroma.scale([startColor, color, endColor]).mode("lab")
+export function color_ramp(color: Color): Scale {
+ const end_color = color.desaturate(1).brighten(5)
+ const start_color = color.desaturate(1).darken(4)
+ return chroma.scale([start_color, color, end_color]).mode("lab")
}
/**
@@ -18,29 +18,29 @@ export function colorRamp(color: Color): Scale {
theme so that we don't modify the passed in ramps.
This combined with an error in the type definitions for chroma js means we have to cast the colors
function to any in order to get the colors back out from the original ramps.
- * @param isLight
- * @param colorRamps
- * @returns
+ * @param is_light
+ * @param color_ramps
+ * @returns
*/
-export function getRamps(
- isLight: boolean,
- colorRamps: ThemeConfigInputColors
+export function get_ramps(
+ is_light: boolean,
+ color_ramps: ThemeConfigInputColors
): RampSet {
- const ramps: RampSet = {} as any
- const colorsKeys = Object.keys(colorRamps) as ThemeConfigInputColorsKeys[]
+ const ramps: RampSet = {} as any // eslint-disable-line @typescript-eslint/no-explicit-any
+ const color_keys = Object.keys(color_ramps) as ThemeConfigInputColorsKeys[]
- if (isLight) {
- for (const rampName of colorsKeys) {
- ramps[rampName] = chroma.scale(
- colorRamps[rampName].colors(100).reverse()
+ if (is_light) {
+ for (const ramp_name of color_keys) {
+ ramps[ramp_name] = chroma.scale(
+ color_ramps[ramp_name].colors(100).reverse()
)
}
- ramps.neutral = chroma.scale(colorRamps.neutral.colors(100).reverse())
+ ramps.neutral = chroma.scale(color_ramps.neutral.colors(100).reverse())
} else {
- for (const rampName of colorsKeys) {
- ramps[rampName] = chroma.scale(colorRamps[rampName].colors(100))
+ for (const ramp_name of color_keys) {
+ ramps[ramp_name] = chroma.scale(color_ramps[ramp_name].colors(100))
}
- ramps.neutral = chroma.scale(colorRamps.neutral.colors(100))
+ ramps.neutral = chroma.scale(color_ramps.neutral.colors(100))
}
return ramps
@@ -1,6 +1,6 @@
import deepmerge from "deepmerge"
-import { FontWeight, fontWeights } from "../common"
-import { ColorScheme } from "./colorScheme"
+import { FontWeight, font_weights } from "../common"
+import { ColorScheme } from "./color_scheme"
import chroma from "chroma-js"
export interface SyntaxHighlightStyle {
@@ -17,13 +17,14 @@ export interface Syntax {
"comment.doc": SyntaxHighlightStyle
primary: SyntaxHighlightStyle
predictive: SyntaxHighlightStyle
+ hint: SyntaxHighlightStyle
// === Formatted Text ====== /
emphasis: SyntaxHighlightStyle
"emphasis.strong": SyntaxHighlightStyle
title: SyntaxHighlightStyle
- linkUri: SyntaxHighlightStyle
- linkText: SyntaxHighlightStyle
+ link_uri: SyntaxHighlightStyle
+ link_text: SyntaxHighlightStyle
/** md: indented_code_block, fenced_code_block, code_span */
"text.literal": SyntaxHighlightStyle
@@ -56,7 +57,7 @@ export interface Syntax {
// == Types ====== /
// We allow Function here because all JS objects literals have this property
- constructor: SyntaxHighlightStyle | Function
+ constructor: SyntaxHighlightStyle | Function // eslint-disable-line @typescript-eslint/ban-types
variant: SyntaxHighlightStyle
type: SyntaxHighlightStyle
// js: predefined_type
@@ -116,13 +117,13 @@ export interface Syntax {
export type ThemeSyntax = Partial<Syntax>
-const defaultSyntaxHighlightStyle: Omit<SyntaxHighlightStyle, "color"> = {
+const default_syntax_highlight_style: Omit<SyntaxHighlightStyle, "color"> = {
weight: "normal",
underline: false,
italic: false,
}
-function buildDefaultSyntax(colorScheme: ColorScheme): Syntax {
+function build_default_syntax(color_scheme: ColorScheme): Syntax {
// Make a temporary object that is allowed to be missing
// the "color" property for each style
const syntax: {
@@ -132,7 +133,7 @@ function buildDefaultSyntax(colorScheme: ColorScheme): Syntax {
// then spread the default to each style
for (const key of Object.keys({} as Syntax)) {
syntax[key as keyof Syntax] = {
- ...defaultSyntaxHighlightStyle,
+ ...default_syntax_highlight_style,
}
}
@@ -140,35 +141,46 @@ function buildDefaultSyntax(colorScheme: ColorScheme): Syntax {
// predictive color distinct from any other color in the theme
const predictive = chroma
.mix(
- colorScheme.ramps.neutral(0.4).hex(),
- colorScheme.ramps.blue(0.4).hex(),
+ color_scheme.ramps.neutral(0.4).hex(),
+ color_scheme.ramps.blue(0.4).hex(),
+ 0.45,
+ "lch"
+ )
+ .hex()
+ // Mix the neutral and green colors to get a
+ // hint color distinct from any other color in the theme
+ const hint = chroma
+ .mix(
+ color_scheme.ramps.neutral(0.6).hex(),
+ color_scheme.ramps.blue(0.4).hex(),
0.45,
"lch"
)
.hex()
const color = {
- primary: colorScheme.ramps.neutral(1).hex(),
- comment: colorScheme.ramps.neutral(0.71).hex(),
- punctuation: colorScheme.ramps.neutral(0.86).hex(),
+ primary: color_scheme.ramps.neutral(1).hex(),
+ comment: color_scheme.ramps.neutral(0.71).hex(),
+ punctuation: color_scheme.ramps.neutral(0.86).hex(),
predictive: predictive,
- emphasis: colorScheme.ramps.blue(0.5).hex(),
- string: colorScheme.ramps.orange(0.5).hex(),
- function: colorScheme.ramps.yellow(0.5).hex(),
- type: colorScheme.ramps.cyan(0.5).hex(),
- constructor: colorScheme.ramps.blue(0.5).hex(),
- variant: colorScheme.ramps.blue(0.5).hex(),
- property: colorScheme.ramps.blue(0.5).hex(),
- enum: colorScheme.ramps.orange(0.5).hex(),
- operator: colorScheme.ramps.orange(0.5).hex(),
- number: colorScheme.ramps.green(0.5).hex(),
- boolean: colorScheme.ramps.green(0.5).hex(),
- constant: colorScheme.ramps.green(0.5).hex(),
- keyword: colorScheme.ramps.blue(0.5).hex(),
+ hint: hint,
+ emphasis: color_scheme.ramps.blue(0.5).hex(),
+ string: color_scheme.ramps.orange(0.5).hex(),
+ function: color_scheme.ramps.yellow(0.5).hex(),
+ type: color_scheme.ramps.cyan(0.5).hex(),
+ constructor: color_scheme.ramps.blue(0.5).hex(),
+ variant: color_scheme.ramps.blue(0.5).hex(),
+ property: color_scheme.ramps.blue(0.5).hex(),
+ enum: color_scheme.ramps.orange(0.5).hex(),
+ operator: color_scheme.ramps.orange(0.5).hex(),
+ number: color_scheme.ramps.green(0.5).hex(),
+ boolean: color_scheme.ramps.green(0.5).hex(),
+ constant: color_scheme.ramps.green(0.5).hex(),
+ keyword: color_scheme.ramps.blue(0.5).hex(),
}
// Then assign colors and use Syntax to enforce each style getting it's own color
- const defaultSyntax: Syntax = {
+ const default_syntax: Syntax = {
...syntax,
comment: {
color: color.comment,
@@ -183,23 +195,27 @@ function buildDefaultSyntax(colorScheme: ColorScheme): Syntax {
color: color.predictive,
italic: true,
},
+ hint: {
+ color: color.hint,
+ weight: font_weights.bold,
+ },
emphasis: {
color: color.emphasis,
},
"emphasis.strong": {
color: color.emphasis,
- weight: fontWeights.bold,
+ weight: font_weights.bold,
},
title: {
color: color.primary,
- weight: fontWeights.bold,
+ weight: font_weights.bold,
},
- linkUri: {
- color: colorScheme.ramps.green(0.5).hex(),
+ link_uri: {
+ color: color_scheme.ramps.green(0.5).hex(),
underline: true,
},
- linkText: {
- color: colorScheme.ramps.orange(0.5).hex(),
+ link_text: {
+ color: color_scheme.ramps.orange(0.5).hex(),
italic: true,
},
"text.literal": {
@@ -215,7 +231,7 @@ function buildDefaultSyntax(colorScheme: ColorScheme): Syntax {
color: color.punctuation,
},
"punctuation.special": {
- color: colorScheme.ramps.neutral(0.86).hex(),
+ color: color_scheme.ramps.neutral(0.86).hex(),
},
"punctuation.list_marker": {
color: color.punctuation,
@@ -236,10 +252,10 @@ function buildDefaultSyntax(colorScheme: ColorScheme): Syntax {
color: color.string,
},
constructor: {
- color: colorScheme.ramps.blue(0.5).hex(),
+ color: color_scheme.ramps.blue(0.5).hex(),
},
variant: {
- color: colorScheme.ramps.blue(0.5).hex(),
+ color: color_scheme.ramps.blue(0.5).hex(),
},
type: {
color: color.type,
@@ -248,16 +264,16 @@ function buildDefaultSyntax(colorScheme: ColorScheme): Syntax {
color: color.primary,
},
label: {
- color: colorScheme.ramps.blue(0.5).hex(),
+ color: color_scheme.ramps.blue(0.5).hex(),
},
tag: {
- color: colorScheme.ramps.blue(0.5).hex(),
+ color: color_scheme.ramps.blue(0.5).hex(),
},
attribute: {
- color: colorScheme.ramps.blue(0.5).hex(),
+ color: color_scheme.ramps.blue(0.5).hex(),
},
property: {
- color: colorScheme.ramps.blue(0.5).hex(),
+ color: color_scheme.ramps.blue(0.5).hex(),
},
constant: {
color: color.constant,
@@ -288,17 +304,20 @@ function buildDefaultSyntax(colorScheme: ColorScheme): Syntax {
},
}
- return defaultSyntax
+ return default_syntax
}
-function mergeSyntax(defaultSyntax: Syntax, colorScheme: ColorScheme): Syntax {
- if (!colorScheme.syntax) {
- return defaultSyntax
+function merge_syntax(
+ default_syntax: Syntax,
+ color_scheme: ColorScheme
+): Syntax {
+ if (!color_scheme.syntax) {
+ return default_syntax
}
return deepmerge<Syntax, Partial<ThemeSyntax>>(
- defaultSyntax,
- colorScheme.syntax,
+ default_syntax,
+ color_scheme.syntax,
{
arrayMerge: (destinationArray, sourceArray) => [
...destinationArray,
@@ -308,10 +327,10 @@ function mergeSyntax(defaultSyntax: Syntax, colorScheme: ColorScheme): Syntax {
)
}
-export function buildSyntax(colorScheme: ColorScheme): Syntax {
- const defaultSyntax: Syntax = buildDefaultSyntax(colorScheme)
+export function build_syntax(color_scheme: ColorScheme): Syntax {
+ const default_syntax: Syntax = build_default_syntax(color_scheme)
- const syntax = mergeSyntax(defaultSyntax, colorScheme)
+ const syntax = merge_syntax(default_syntax, color_scheme)
return syntax
}
@@ -17,15 +17,15 @@ interface ThemeMeta {
*
* Example: `MIT`
*/
- licenseType?: string | ThemeLicenseType
- licenseUrl?: string
- licenseFile: string
- themeUrl?: string
+ license_type?: string | ThemeLicenseType
+ license_url?: string
+ license_file: string
+ theme_url?: string
}
export type ThemeFamilyMeta = Pick<
ThemeMeta,
- "name" | "author" | "licenseType" | "licenseUrl"
+ "name" | "author" | "license_type" | "license_url"
>
export interface ThemeConfigInputColors {
@@ -62,7 +62,7 @@ interface ThemeConfigOverrides {
}
type ThemeConfigProperties = ThemeMeta & {
- inputColor: ThemeConfigInputColors
+ input_color: ThemeConfigInputColors
override: ThemeConfigOverrides
}
@@ -1,99 +0,0 @@
-import {
- SingleBoxShadowToken,
- SingleColorToken,
- SingleOtherToken,
- TokenTypes,
-} from "@tokens-studio/types"
-import {
- ColorScheme,
- Shadow,
- SyntaxHighlightStyle,
- ThemeSyntax,
-} from "../colorScheme"
-import { LayerToken, layerToken } from "./layer"
-import { PlayersToken, playersToken } from "./players"
-import { colorToken } from "./token"
-import { Syntax } from "../syntax"
-import editor from "../../styleTree/editor"
-
-interface ColorSchemeTokens {
- name: SingleOtherToken
- appearance: SingleOtherToken
- lowest: LayerToken
- middle: LayerToken
- highest: LayerToken
- players: PlayersToken
- popoverShadow: SingleBoxShadowToken
- modalShadow: SingleBoxShadowToken
- syntax?: Partial<ThemeSyntaxColorTokens>
-}
-
-const createShadowToken = (
- shadow: Shadow,
- tokenName: string
-): SingleBoxShadowToken => {
- return {
- name: tokenName,
- type: TokenTypes.BOX_SHADOW,
- value: `${shadow.offset[0]}px ${shadow.offset[1]}px ${shadow.blur}px 0px ${shadow.color}`,
- }
-}
-
-const popoverShadowToken = (colorScheme: ColorScheme): SingleBoxShadowToken => {
- const shadow = colorScheme.popoverShadow
- return createShadowToken(shadow, "popoverShadow")
-}
-
-const modalShadowToken = (colorScheme: ColorScheme): SingleBoxShadowToken => {
- const shadow = colorScheme.modalShadow
- return createShadowToken(shadow, "modalShadow")
-}
-
-type ThemeSyntaxColorTokens = Record<keyof ThemeSyntax, SingleColorToken>
-
-function syntaxHighlightStyleColorTokens(
- syntax: Syntax
-): ThemeSyntaxColorTokens {
- const styleKeys = Object.keys(syntax) as (keyof Syntax)[]
-
- return styleKeys.reduce((acc, styleKey) => {
- // Hack: The type of a style could be "Function"
- // This can happen because we have a "constructor" property on the syntax object
- // and a "constructor" property on the prototype of the syntax object
- // To work around this just assert that the type of the style is not a function
- if (!syntax[styleKey] || typeof syntax[styleKey] === "function")
- return acc
- const { color } = syntax[styleKey] as Required<SyntaxHighlightStyle>
- return { ...acc, [styleKey]: colorToken(styleKey, color) }
- }, {} as ThemeSyntaxColorTokens)
-}
-
-const syntaxTokens = (
- colorScheme: ColorScheme
-): ColorSchemeTokens["syntax"] => {
- const syntax = editor(colorScheme).syntax
-
- return syntaxHighlightStyleColorTokens(syntax)
-}
-
-export function colorSchemeTokens(colorScheme: ColorScheme): ColorSchemeTokens {
- return {
- name: {
- name: "themeName",
- value: colorScheme.name,
- type: TokenTypes.OTHER,
- },
- appearance: {
- name: "themeAppearance",
- value: colorScheme.isLight ? "light" : "dark",
- type: TokenTypes.OTHER,
- },
- lowest: layerToken(colorScheme.lowest, "lowest"),
- middle: layerToken(colorScheme.middle, "middle"),
- highest: layerToken(colorScheme.highest, "highest"),
- popoverShadow: popoverShadowToken(colorScheme),
- modalShadow: modalShadowToken(colorScheme),
- players: playersToken(colorScheme),
- syntax: syntaxTokens(colorScheme),
- }
-}
@@ -0,0 +1,97 @@
+import {
+ SingleBoxShadowToken,
+ SingleColorToken,
+ SingleOtherToken,
+ TokenTypes,
+} from "@tokens-studio/types"
+import {
+ ColorScheme,
+ Shadow,
+ SyntaxHighlightStyle,
+ ThemeSyntax,
+} from "../color_scheme"
+import { LayerToken, layer_token } from "./layer"
+import { PlayersToken, players_token } from "./players"
+import { color_token } from "./token"
+import { Syntax } from "../syntax"
+import editor from "../../style_tree/editor"
+
+interface ColorSchemeTokens {
+ name: SingleOtherToken
+ appearance: SingleOtherToken
+ lowest: LayerToken
+ middle: LayerToken
+ highest: LayerToken
+ players: PlayersToken
+ popover_shadow: SingleBoxShadowToken
+ modal_shadow: SingleBoxShadowToken
+ syntax?: Partial<ThemeSyntaxColorTokens>
+}
+
+const create_shadow_token = (
+ shadow: Shadow,
+ token_name: string
+): SingleBoxShadowToken => {
+ return {
+ name: token_name,
+ type: TokenTypes.BOX_SHADOW,
+ value: `${shadow.offset[0]}px ${shadow.offset[1]}px ${shadow.blur}px 0px ${shadow.color}`,
+ }
+}
+
+const popover_shadow_token = (theme: ColorScheme): SingleBoxShadowToken => {
+ const shadow = theme.popover_shadow
+ return create_shadow_token(shadow, "popover_shadow")
+}
+
+const modal_shadow_token = (theme: ColorScheme): SingleBoxShadowToken => {
+ const shadow = theme.modal_shadow
+ return create_shadow_token(shadow, "modal_shadow")
+}
+
+type ThemeSyntaxColorTokens = Record<keyof ThemeSyntax, SingleColorToken>
+
+function syntax_highlight_style_color_tokens(
+ syntax: Syntax
+): ThemeSyntaxColorTokens {
+ const style_keys = Object.keys(syntax) as (keyof Syntax)[]
+
+ return style_keys.reduce((acc, style_key) => {
+ // Hack: The type of a style could be "Function"
+ // This can happen because we have a "constructor" property on the syntax object
+ // and a "constructor" property on the prototype of the syntax object
+ // To work around this just assert that the type of the style is not a function
+ if (!syntax[style_key] || typeof syntax[style_key] === "function")
+ return acc
+ const { color } = syntax[style_key] as Required<SyntaxHighlightStyle>
+ return { ...acc, [style_key]: color_token(style_key, color) }
+ }, {} as ThemeSyntaxColorTokens)
+}
+
+const syntax_tokens = (theme: ColorScheme): ColorSchemeTokens["syntax"] => {
+ const syntax = editor(theme).syntax
+
+ return syntax_highlight_style_color_tokens(syntax)
+}
+
+export function theme_tokens(theme: ColorScheme): ColorSchemeTokens {
+ return {
+ name: {
+ name: "themeName",
+ value: theme.name,
+ type: TokenTypes.OTHER,
+ },
+ appearance: {
+ name: "themeAppearance",
+ value: theme.is_light ? "light" : "dark",
+ type: TokenTypes.OTHER,
+ },
+ lowest: layer_token(theme.lowest, "lowest"),
+ middle: layer_token(theme.middle, "middle"),
+ highest: layer_token(theme.highest, "highest"),
+ popover_shadow: popover_shadow_token(theme),
+ modal_shadow: modal_shadow_token(theme),
+ players: players_token(theme),
+ syntax: syntax_tokens(theme),
+ }
+}
@@ -1,6 +1,6 @@
import { SingleColorToken } from "@tokens-studio/types"
-import { Layer, Style, StyleSet } from "../colorScheme"
-import { colorToken } from "./token"
+import { Layer, Style, StyleSet } from "../color_scheme"
+import { color_token } from "./token"
interface StyleToken {
background: SingleColorToken
@@ -27,36 +27,36 @@ export interface LayerToken {
negative: StyleSetToken
}
-export const styleToken = (style: Style, name: string): StyleToken => {
+export const style_token = (style: Style, name: string): StyleToken => {
const token = {
- background: colorToken(`${name}Background`, style.background),
- border: colorToken(`${name}Border`, style.border),
- foreground: colorToken(`${name}Foreground`, style.foreground),
+ background: color_token(`${name}Background`, style.background),
+ border: color_token(`${name}Border`, style.border),
+ foreground: color_token(`${name}Foreground`, style.foreground),
}
return token
}
-export const styleSetToken = (
- styleSet: StyleSet,
+export const style_set_token = (
+ style_set: StyleSet,
name: string
): StyleSetToken => {
const token: StyleSetToken = {} as StyleSetToken
- for (const style in styleSet) {
+ for (const style in style_set) {
const s = style as keyof StyleSet
- token[s] = styleToken(styleSet[s], `${name}${style}`)
+ token[s] = style_token(style_set[s], `${name}${style}`)
}
return token
}
-export const layerToken = (layer: Layer, name: string): LayerToken => {
+export const layer_token = (layer: Layer, name: string): LayerToken => {
const token: LayerToken = {} as LayerToken
- for (const styleSet in layer) {
- const s = styleSet as keyof Layer
- token[s] = styleSetToken(layer[s], `${name}${styleSet}`)
+ for (const style_set in layer) {
+ const s = style_set as keyof Layer
+ token[s] = style_set_token(layer[s], `${name}${style_set}`)
}
return token
@@ -1,36 +1,33 @@
import { SingleColorToken } from "@tokens-studio/types"
-import { ColorScheme, Players } from "../../common"
-import { colorToken } from "./token"
+import { color_token } from "./token"
+import { ColorScheme, Players } from "../color_scheme"
export type PlayerToken = Record<"selection" | "cursor", SingleColorToken>
export type PlayersToken = Record<keyof Players, PlayerToken>
-function buildPlayerToken(
- colorScheme: ColorScheme,
- index: number
-): PlayerToken {
- const playerNumber = index.toString() as keyof Players
+function build_player_token(theme: ColorScheme, index: number): PlayerToken {
+ const player_number = index.toString() as keyof Players
return {
- selection: colorToken(
+ selection: color_token(
`player${index}Selection`,
- colorScheme.players[playerNumber].selection
+ theme.players[player_number].selection
),
- cursor: colorToken(
+ cursor: color_token(
`player${index}Cursor`,
- colorScheme.players[playerNumber].cursor
+ theme.players[player_number].cursor
),
}
}
-export const playersToken = (colorScheme: ColorScheme): PlayersToken => ({
- "0": buildPlayerToken(colorScheme, 0),
- "1": buildPlayerToken(colorScheme, 1),
- "2": buildPlayerToken(colorScheme, 2),
- "3": buildPlayerToken(colorScheme, 3),
- "4": buildPlayerToken(colorScheme, 4),
- "5": buildPlayerToken(colorScheme, 5),
- "6": buildPlayerToken(colorScheme, 6),
- "7": buildPlayerToken(colorScheme, 7),
+export const players_token = (theme: ColorScheme): PlayersToken => ({
+ "0": build_player_token(theme, 0),
+ "1": build_player_token(theme, 1),
+ "2": build_player_token(theme, 2),
+ "3": build_player_token(theme, 3),
+ "4": build_player_token(theme, 4),
+ "5": build_player_token(theme, 5),
+ "6": build_player_token(theme, 6),
+ "7": build_player_token(theme, 7),
})
@@ -1,6 +1,6 @@
import { SingleColorToken, TokenTypes } from "@tokens-studio/types"
-export function colorToken(
+export function color_token(
name: string,
value: string,
description?: string
@@ -18,4 +18,4 @@ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
-SOFTWARE.
+SOFTWARE.
@@ -1,6 +1,6 @@
import {
chroma,
- colorRamp,
+ color_ramp,
ThemeAppearance,
ThemeLicenseType,
ThemeConfig,
@@ -10,10 +10,10 @@ export const dark: ThemeConfig = {
name: "Andromeda",
author: "EliverLara",
appearance: ThemeAppearance.Dark,
- licenseType: ThemeLicenseType.MIT,
- licenseUrl: "https://github.com/EliverLara/Andromeda",
- licenseFile: `${__dirname}/LICENSE`,
- inputColor: {
+ license_type: ThemeLicenseType.MIT,
+ license_url: "https://github.com/EliverLara/Andromeda",
+ license_file: `${__dirname}/LICENSE`,
+ input_color: {
neutral: chroma
.scale([
"#1E2025",
@@ -26,14 +26,14 @@ export const dark: ThemeConfig = {
"#F7F7F8",
])
.domain([0, 0.15, 0.25, 0.35, 0.7, 0.8, 0.9, 1]),
- red: colorRamp(chroma("#F92672")),
- orange: colorRamp(chroma("#F39C12")),
- yellow: colorRamp(chroma("#FFE66D")),
- green: colorRamp(chroma("#96E072")),
- cyan: colorRamp(chroma("#00E8C6")),
- blue: colorRamp(chroma("#0CA793")),
- violet: colorRamp(chroma("#8A3FA6")),
- magenta: colorRamp(chroma("#C74DED")),
+ red: color_ramp(chroma("#F92672")),
+ orange: color_ramp(chroma("#F39C12")),
+ yellow: color_ramp(chroma("#FFE66D")),
+ green: color_ramp(chroma("#96E072")),
+ cyan: color_ramp(chroma("#00E8C6")),
+ blue: color_ramp(chroma("#0CA793")),
+ violet: color_ramp(chroma("#8A3FA6")),
+ magenta: color_ramp(chroma("#C74DED")),
},
override: { syntax: {} },
}
@@ -18,4 +18,4 @@ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
-SOFTWARE.
+SOFTWARE.
@@ -1,5 +1,5 @@
-import { chroma, ThemeAppearance, ThemeConfig, colorRamp } from "../../common"
-import { meta, buildSyntax, Variant } from "./common"
+import { chroma, ThemeAppearance, ThemeConfig, color_ramp } from "../../common"
+import { meta, build_syntax, Variant } from "./common"
const variant: Variant = {
colors: {
@@ -22,19 +22,19 @@ const variant: Variant = {
},
}
-const syntax = buildSyntax(variant)
+const syntax = build_syntax(variant)
-const getTheme = (variant: Variant): ThemeConfig => {
+const get_theme = (variant: Variant): ThemeConfig => {
const { colors } = variant
return {
name: `${meta.name} Cave Dark`,
author: meta.author,
appearance: ThemeAppearance.Dark,
- licenseType: meta.licenseType,
- licenseUrl: meta.licenseUrl,
- licenseFile: `${__dirname}/LICENSE`,
- inputColor: {
+ license_type: meta.license_type,
+ license_url: meta.license_url,
+ license_file: `${__dirname}/LICENSE`,
+ input_color: {
neutral: chroma.scale([
colors.base00,
colors.base01,
@@ -45,17 +45,17 @@ const getTheme = (variant: Variant): ThemeConfig => {
colors.base06,
colors.base07,
]),
- red: colorRamp(chroma(colors.base08)),
- orange: colorRamp(chroma(colors.base09)),
- yellow: colorRamp(chroma(colors.base0A)),
- green: colorRamp(chroma(colors.base0B)),
- cyan: colorRamp(chroma(colors.base0C)),
- blue: colorRamp(chroma(colors.base0D)),
- violet: colorRamp(chroma(colors.base0E)),
- magenta: colorRamp(chroma(colors.base0F)),
+ red: color_ramp(chroma(colors.base08)),
+ orange: color_ramp(chroma(colors.base09)),
+ yellow: color_ramp(chroma(colors.base0A)),
+ green: color_ramp(chroma(colors.base0B)),
+ cyan: color_ramp(chroma(colors.base0C)),
+ blue: color_ramp(chroma(colors.base0D)),
+ violet: color_ramp(chroma(colors.base0E)),
+ magenta: color_ramp(chroma(colors.base0F)),
},
override: { syntax },
}
}
-export const theme = getTheme(variant)
+export const theme = get_theme(variant)
@@ -1,5 +1,5 @@
-import { chroma, ThemeAppearance, ThemeConfig, colorRamp } from "../../common"
-import { meta, buildSyntax, Variant } from "./common"
+import { chroma, ThemeAppearance, ThemeConfig, color_ramp } from "../../common"
+import { meta, build_syntax, Variant } from "./common"
const variant: Variant = {
colors: {
@@ -22,19 +22,19 @@ const variant: Variant = {
},
}
-const syntax = buildSyntax(variant)
+const syntax = build_syntax(variant)
-const getTheme = (variant: Variant): ThemeConfig => {
+const get_theme = (variant: Variant): ThemeConfig => {
const { colors } = variant
return {
name: `${meta.name} Cave Light`,
author: meta.author,
appearance: ThemeAppearance.Light,
- licenseType: meta.licenseType,
- licenseUrl: meta.licenseUrl,
- licenseFile: `${__dirname}/LICENSE`,
- inputColor: {
+ license_type: meta.license_type,
+ license_url: meta.license_url,
+ license_file: `${__dirname}/LICENSE`,
+ input_color: {
neutral: chroma.scale(
[
colors.base00,
@@ -47,17 +47,17 @@ const getTheme = (variant: Variant): ThemeConfig => {
colors.base07,
].reverse()
),
- red: colorRamp(chroma(colors.base08)),
- orange: colorRamp(chroma(colors.base09)),
- yellow: colorRamp(chroma(colors.base0A)),
- green: colorRamp(chroma(colors.base0B)),
- cyan: colorRamp(chroma(colors.base0C)),
- blue: colorRamp(chroma(colors.base0D)),
- violet: colorRamp(chroma(colors.base0E)),
- magenta: colorRamp(chroma(colors.base0F)),
+ red: color_ramp(chroma(colors.base08)),
+ orange: color_ramp(chroma(colors.base09)),
+ yellow: color_ramp(chroma(colors.base0A)),
+ green: color_ramp(chroma(colors.base0B)),
+ cyan: color_ramp(chroma(colors.base0C)),
+ blue: color_ramp(chroma(colors.base0D)),
+ violet: color_ramp(chroma(colors.base0E)),
+ magenta: color_ramp(chroma(colors.base0F)),
},
override: { syntax },
}
}
-export const theme = getTheme(variant)
+export const theme = get_theme(variant)
@@ -1,5 +1,5 @@
-import { chroma, ThemeAppearance, ThemeConfig, colorRamp } from "../../common"
-import { meta, buildSyntax, Variant } from "./common"
+import { chroma, ThemeAppearance, ThemeConfig, color_ramp } from "../../common"
+import { meta, build_syntax, Variant } from "./common"
const variant: Variant = {
colors: {
@@ -22,19 +22,19 @@ const variant: Variant = {
},
}
-const syntax = buildSyntax(variant)
+const syntax = build_syntax(variant)
-const getTheme = (variant: Variant): ThemeConfig => {
+const get_theme = (variant: Variant): ThemeConfig => {
const { colors } = variant
return {
name: `${meta.name} Dune Dark`,
author: meta.author,
appearance: ThemeAppearance.Dark,
- licenseType: meta.licenseType,
- licenseUrl: meta.licenseUrl,
- licenseFile: `${__dirname}/LICENSE`,
- inputColor: {
+ license_type: meta.license_type,
+ license_url: meta.license_url,
+ license_file: `${__dirname}/LICENSE`,
+ input_color: {
neutral: chroma.scale([
colors.base00,
colors.base01,
@@ -45,17 +45,17 @@ const getTheme = (variant: Variant): ThemeConfig => {
colors.base06,
colors.base07,
]),
- red: colorRamp(chroma(colors.base08)),
- orange: colorRamp(chroma(colors.base09)),
- yellow: colorRamp(chroma(colors.base0A)),
- green: colorRamp(chroma(colors.base0B)),
- cyan: colorRamp(chroma(colors.base0C)),
- blue: colorRamp(chroma(colors.base0D)),
- violet: colorRamp(chroma(colors.base0E)),
- magenta: colorRamp(chroma(colors.base0F)),
+ red: color_ramp(chroma(colors.base08)),
+ orange: color_ramp(chroma(colors.base09)),
+ yellow: color_ramp(chroma(colors.base0A)),
+ green: color_ramp(chroma(colors.base0B)),
+ cyan: color_ramp(chroma(colors.base0C)),
+ blue: color_ramp(chroma(colors.base0D)),
+ violet: color_ramp(chroma(colors.base0E)),
+ magenta: color_ramp(chroma(colors.base0F)),
},
override: { syntax },
}
}
-export const theme = getTheme(variant)
+export const theme = get_theme(variant)
@@ -1,5 +1,5 @@
-import { chroma, ThemeAppearance, ThemeConfig, colorRamp } from "../../common"
-import { meta, buildSyntax, Variant } from "./common"
+import { chroma, ThemeAppearance, ThemeConfig, color_ramp } from "../../common"
+import { meta, build_syntax, Variant } from "./common"
const variant: Variant = {
colors: {
@@ -22,19 +22,19 @@ const variant: Variant = {
},
}
-const syntax = buildSyntax(variant)
+const syntax = build_syntax(variant)
-const getTheme = (variant: Variant): ThemeConfig => {
+const get_theme = (variant: Variant): ThemeConfig => {
const { colors } = variant
return {
name: `${meta.name} Dune Light`,
author: meta.author,
appearance: ThemeAppearance.Light,
- licenseType: meta.licenseType,
- licenseUrl: meta.licenseUrl,
- licenseFile: `${__dirname}/LICENSE`,
- inputColor: {
+ license_type: meta.license_type,
+ license_url: meta.license_url,
+ license_file: `${__dirname}/LICENSE`,
+ input_color: {
neutral: chroma.scale(
[
colors.base00,
@@ -47,17 +47,17 @@ const getTheme = (variant: Variant): ThemeConfig => {
colors.base07,
].reverse()
),
- red: colorRamp(chroma(colors.base08)),
- orange: colorRamp(chroma(colors.base09)),
- yellow: colorRamp(chroma(colors.base0A)),
- green: colorRamp(chroma(colors.base0B)),
- cyan: colorRamp(chroma(colors.base0C)),
- blue: colorRamp(chroma(colors.base0D)),
- violet: colorRamp(chroma(colors.base0E)),
- magenta: colorRamp(chroma(colors.base0F)),
+ red: color_ramp(chroma(colors.base08)),
+ orange: color_ramp(chroma(colors.base09)),
+ yellow: color_ramp(chroma(colors.base0A)),
+ green: color_ramp(chroma(colors.base0B)),
+ cyan: color_ramp(chroma(colors.base0C)),
+ blue: color_ramp(chroma(colors.base0D)),
+ violet: color_ramp(chroma(colors.base0E)),
+ magenta: color_ramp(chroma(colors.base0F)),
},
override: { syntax },
}
}
-export const theme = getTheme(variant)
+export const theme = get_theme(variant)
@@ -1,5 +1,5 @@
-import { chroma, ThemeAppearance, ThemeConfig, colorRamp } from "../../common"
-import { meta, buildSyntax, Variant } from "./common"
+import { chroma, ThemeAppearance, ThemeConfig, color_ramp } from "../../common"
+import { meta, build_syntax, Variant } from "./common"
const variant: Variant = {
colors: {
@@ -22,19 +22,19 @@ const variant: Variant = {
},
}
-const syntax = buildSyntax(variant)
+const syntax = build_syntax(variant)
-const getTheme = (variant: Variant): ThemeConfig => {
+const get_theme = (variant: Variant): ThemeConfig => {
const { colors } = variant
return {
name: `${meta.name} Estuary Dark`,
author: meta.author,
appearance: ThemeAppearance.Dark,
- licenseType: meta.licenseType,
- licenseUrl: meta.licenseUrl,
- licenseFile: `${__dirname}/LICENSE`,
- inputColor: {
+ license_type: meta.license_type,
+ license_url: meta.license_url,
+ license_file: `${__dirname}/LICENSE`,
+ input_color: {
neutral: chroma.scale([
colors.base00,
colors.base01,
@@ -45,17 +45,17 @@ const getTheme = (variant: Variant): ThemeConfig => {
colors.base06,
colors.base07,
]),
- red: colorRamp(chroma(colors.base08)),
- orange: colorRamp(chroma(colors.base09)),
- yellow: colorRamp(chroma(colors.base0A)),
- green: colorRamp(chroma(colors.base0B)),
- cyan: colorRamp(chroma(colors.base0C)),
- blue: colorRamp(chroma(colors.base0D)),
- violet: colorRamp(chroma(colors.base0E)),
- magenta: colorRamp(chroma(colors.base0F)),
+ red: color_ramp(chroma(colors.base08)),
+ orange: color_ramp(chroma(colors.base09)),
+ yellow: color_ramp(chroma(colors.base0A)),
+ green: color_ramp(chroma(colors.base0B)),
+ cyan: color_ramp(chroma(colors.base0C)),
+ blue: color_ramp(chroma(colors.base0D)),
+ violet: color_ramp(chroma(colors.base0E)),
+ magenta: color_ramp(chroma(colors.base0F)),
},
override: { syntax },
}
}
-export const theme = getTheme(variant)
+export const theme = get_theme(variant)
@@ -1,5 +1,5 @@
-import { chroma, ThemeAppearance, ThemeConfig, colorRamp } from "../../common"
-import { meta, buildSyntax, Variant } from "./common"
+import { chroma, ThemeAppearance, ThemeConfig, color_ramp } from "../../common"
+import { meta, build_syntax, Variant } from "./common"
const variant: Variant = {
colors: {
@@ -22,19 +22,19 @@ const variant: Variant = {
},
}
-const syntax = buildSyntax(variant)
+const syntax = build_syntax(variant)
-const getTheme = (variant: Variant): ThemeConfig => {
+const get_theme = (variant: Variant): ThemeConfig => {
const { colors } = variant
return {
name: `${meta.name} Estuary Light`,
author: meta.author,
appearance: ThemeAppearance.Light,
- licenseType: meta.licenseType,
- licenseUrl: meta.licenseUrl,
- licenseFile: `${__dirname}/LICENSE`,
- inputColor: {
+ license_type: meta.license_type,
+ license_url: meta.license_url,
+ license_file: `${__dirname}/LICENSE`,
+ input_color: {
neutral: chroma.scale(
[
colors.base00,
@@ -47,17 +47,17 @@ const getTheme = (variant: Variant): ThemeConfig => {
colors.base07,
].reverse()
),
- red: colorRamp(chroma(colors.base08)),
- orange: colorRamp(chroma(colors.base09)),
- yellow: colorRamp(chroma(colors.base0A)),
- green: colorRamp(chroma(colors.base0B)),
- cyan: colorRamp(chroma(colors.base0C)),
- blue: colorRamp(chroma(colors.base0D)),
- violet: colorRamp(chroma(colors.base0E)),
- magenta: colorRamp(chroma(colors.base0F)),
+ red: color_ramp(chroma(colors.base08)),
+ orange: color_ramp(chroma(colors.base09)),
+ yellow: color_ramp(chroma(colors.base0A)),
+ green: color_ramp(chroma(colors.base0B)),
+ cyan: color_ramp(chroma(colors.base0C)),
+ blue: color_ramp(chroma(colors.base0D)),
+ violet: color_ramp(chroma(colors.base0E)),
+ magenta: color_ramp(chroma(colors.base0F)),
},
override: { syntax },
}
}
-export const theme = getTheme(variant)
+export const theme = get_theme(variant)
@@ -1,5 +1,5 @@
-import { chroma, ThemeAppearance, ThemeConfig, colorRamp } from "../../common"
-import { meta, buildSyntax, Variant } from "./common"
+import { chroma, ThemeAppearance, ThemeConfig, color_ramp } from "../../common"
+import { meta, build_syntax, Variant } from "./common"
const variant: Variant = {
colors: {
@@ -22,19 +22,19 @@ const variant: Variant = {
},
}
-const syntax = buildSyntax(variant)
+const syntax = build_syntax(variant)
-const getTheme = (variant: Variant): ThemeConfig => {
+const get_theme = (variant: Variant): ThemeConfig => {
const { colors } = variant
return {
name: `${meta.name} Forest Dark`,
author: meta.author,
appearance: ThemeAppearance.Dark,
- licenseType: meta.licenseType,
- licenseUrl: meta.licenseUrl,
- licenseFile: `${__dirname}/LICENSE`,
- inputColor: {
+ license_type: meta.license_type,
+ license_url: meta.license_url,
+ license_file: `${__dirname}/LICENSE`,
+ input_color: {
neutral: chroma.scale([
colors.base00,
colors.base01,
@@ -45,17 +45,17 @@ const getTheme = (variant: Variant): ThemeConfig => {
colors.base06,
colors.base07,
]),
- red: colorRamp(chroma(colors.base08)),
- orange: colorRamp(chroma(colors.base09)),
- yellow: colorRamp(chroma(colors.base0A)),
- green: colorRamp(chroma(colors.base0B)),
- cyan: colorRamp(chroma(colors.base0C)),
- blue: colorRamp(chroma(colors.base0D)),
- violet: colorRamp(chroma(colors.base0E)),
- magenta: colorRamp(chroma(colors.base0F)),
+ red: color_ramp(chroma(colors.base08)),
+ orange: color_ramp(chroma(colors.base09)),
+ yellow: color_ramp(chroma(colors.base0A)),
+ green: color_ramp(chroma(colors.base0B)),
+ cyan: color_ramp(chroma(colors.base0C)),
+ blue: color_ramp(chroma(colors.base0D)),
+ violet: color_ramp(chroma(colors.base0E)),
+ magenta: color_ramp(chroma(colors.base0F)),
},
override: { syntax },
}
}
-export const theme = getTheme(variant)
+export const theme = get_theme(variant)
@@ -1,5 +1,5 @@
-import { chroma, ThemeAppearance, ThemeConfig, colorRamp } from "../../common"
-import { meta, buildSyntax, Variant } from "./common"
+import { chroma, ThemeAppearance, ThemeConfig, color_ramp } from "../../common"
+import { meta, build_syntax, Variant } from "./common"
const variant: Variant = {
colors: {
@@ -22,19 +22,19 @@ const variant: Variant = {
},
}
-const syntax = buildSyntax(variant)
+const syntax = build_syntax(variant)
-const getTheme = (variant: Variant): ThemeConfig => {
+const get_theme = (variant: Variant): ThemeConfig => {
const { colors } = variant
return {
name: `${meta.name} Forest Light`,
author: meta.author,
appearance: ThemeAppearance.Light,
- licenseType: meta.licenseType,
- licenseUrl: meta.licenseUrl,
- licenseFile: `${__dirname}/LICENSE`,
- inputColor: {
+ license_type: meta.license_type,
+ license_url: meta.license_url,
+ license_file: `${__dirname}/LICENSE`,
+ input_color: {
neutral: chroma.scale(
[
colors.base00,
@@ -47,17 +47,17 @@ const getTheme = (variant: Variant): ThemeConfig => {
colors.base07,
].reverse()
),
- red: colorRamp(chroma(colors.base08)),
- orange: colorRamp(chroma(colors.base09)),
- yellow: colorRamp(chroma(colors.base0A)),
- green: colorRamp(chroma(colors.base0B)),
- cyan: colorRamp(chroma(colors.base0C)),
- blue: colorRamp(chroma(colors.base0D)),
- violet: colorRamp(chroma(colors.base0E)),
- magenta: colorRamp(chroma(colors.base0F)),
+ red: color_ramp(chroma(colors.base08)),
+ orange: color_ramp(chroma(colors.base09)),
+ yellow: color_ramp(chroma(colors.base0A)),
+ green: color_ramp(chroma(colors.base0B)),
+ cyan: color_ramp(chroma(colors.base0C)),
+ blue: color_ramp(chroma(colors.base0D)),
+ violet: color_ramp(chroma(colors.base0E)),
+ magenta: color_ramp(chroma(colors.base0F)),
},
override: { syntax },
}
}
-export const theme = getTheme(variant)
+export const theme = get_theme(variant)
@@ -1,5 +1,5 @@
-import { chroma, ThemeAppearance, ThemeConfig, colorRamp } from "../../common"
-import { meta, buildSyntax, Variant } from "./common"
+import { chroma, ThemeAppearance, ThemeConfig, color_ramp } from "../../common"
+import { meta, build_syntax, Variant } from "./common"
const variant: Variant = {
colors: {
@@ -22,19 +22,19 @@ const variant: Variant = {
},
}
-const syntax = buildSyntax(variant)
+const syntax = build_syntax(variant)
-const getTheme = (variant: Variant): ThemeConfig => {
+const get_theme = (variant: Variant): ThemeConfig => {
const { colors } = variant
return {
name: `${meta.name} Heath Dark`,
author: meta.author,
appearance: ThemeAppearance.Dark,
- licenseType: meta.licenseType,
- licenseUrl: meta.licenseUrl,
- licenseFile: `${__dirname}/LICENSE`,
- inputColor: {
+ license_type: meta.license_type,
+ license_url: meta.license_url,
+ license_file: `${__dirname}/LICENSE`,
+ input_color: {
neutral: chroma.scale([
colors.base00,
colors.base01,
@@ -45,17 +45,17 @@ const getTheme = (variant: Variant): ThemeConfig => {
colors.base06,
colors.base07,
]),
- red: colorRamp(chroma(colors.base08)),
- orange: colorRamp(chroma(colors.base09)),
- yellow: colorRamp(chroma(colors.base0A)),
- green: colorRamp(chroma(colors.base0B)),
- cyan: colorRamp(chroma(colors.base0C)),
- blue: colorRamp(chroma(colors.base0D)),
- violet: colorRamp(chroma(colors.base0E)),
- magenta: colorRamp(chroma(colors.base0F)),
+ red: color_ramp(chroma(colors.base08)),
+ orange: color_ramp(chroma(colors.base09)),
+ yellow: color_ramp(chroma(colors.base0A)),
+ green: color_ramp(chroma(colors.base0B)),
+ cyan: color_ramp(chroma(colors.base0C)),
+ blue: color_ramp(chroma(colors.base0D)),
+ violet: color_ramp(chroma(colors.base0E)),
+ magenta: color_ramp(chroma(colors.base0F)),
},
override: { syntax },
}
}
-export const theme = getTheme(variant)
+export const theme = get_theme(variant)
@@ -1,5 +1,5 @@
-import { chroma, ThemeAppearance, ThemeConfig, colorRamp } from "../../common"
-import { meta, buildSyntax, Variant } from "./common"
+import { chroma, ThemeAppearance, ThemeConfig, color_ramp } from "../../common"
+import { meta, build_syntax, Variant } from "./common"
const variant: Variant = {
colors: {
@@ -22,19 +22,19 @@ const variant: Variant = {
},
}
-const syntax = buildSyntax(variant)
+const syntax = build_syntax(variant)
-const getTheme = (variant: Variant): ThemeConfig => {
+const get_theme = (variant: Variant): ThemeConfig => {
const { colors } = variant
return {
name: `${meta.name} Heath Light`,
author: meta.author,
appearance: ThemeAppearance.Light,
- licenseType: meta.licenseType,
- licenseUrl: meta.licenseUrl,
- licenseFile: `${__dirname}/LICENSE`,
- inputColor: {
+ license_type: meta.license_type,
+ license_url: meta.license_url,
+ license_file: `${__dirname}/LICENSE`,
+ input_color: {
neutral: chroma.scale(
[
colors.base00,
@@ -47,17 +47,17 @@ const getTheme = (variant: Variant): ThemeConfig => {
colors.base07,
].reverse()
),
- red: colorRamp(chroma(colors.base08)),
- orange: colorRamp(chroma(colors.base09)),
- yellow: colorRamp(chroma(colors.base0A)),
- green: colorRamp(chroma(colors.base0B)),
- cyan: colorRamp(chroma(colors.base0C)),
- blue: colorRamp(chroma(colors.base0D)),
- violet: colorRamp(chroma(colors.base0E)),
- magenta: colorRamp(chroma(colors.base0F)),
+ red: color_ramp(chroma(colors.base08)),
+ orange: color_ramp(chroma(colors.base09)),
+ yellow: color_ramp(chroma(colors.base0A)),
+ green: color_ramp(chroma(colors.base0B)),
+ cyan: color_ramp(chroma(colors.base0C)),
+ blue: color_ramp(chroma(colors.base0D)),
+ violet: color_ramp(chroma(colors.base0E)),
+ magenta: color_ramp(chroma(colors.base0F)),
},
override: { syntax },
}
}
-export const theme = getTheme(variant)
+export const theme = get_theme(variant)
@@ -1,5 +1,5 @@
-import { chroma, ThemeAppearance, ThemeConfig, colorRamp } from "../../common"
-import { meta, buildSyntax, Variant } from "./common"
+import { chroma, ThemeAppearance, ThemeConfig, color_ramp } from "../../common"
+import { meta, build_syntax, Variant } from "./common"
const variant: Variant = {
colors: {
@@ -22,19 +22,19 @@ const variant: Variant = {
},
}
-const syntax = buildSyntax(variant)
+const syntax = build_syntax(variant)
-const getTheme = (variant: Variant): ThemeConfig => {
+const get_theme = (variant: Variant): ThemeConfig => {
const { colors } = variant
return {
name: `${meta.name} Lakeside Dark`,
author: meta.author,
appearance: ThemeAppearance.Dark,
- licenseType: meta.licenseType,
- licenseUrl: meta.licenseUrl,
- licenseFile: `${__dirname}/LICENSE`,
- inputColor: {
+ license_type: meta.license_type,
+ license_url: meta.license_url,
+ license_file: `${__dirname}/LICENSE`,
+ input_color: {
neutral: chroma.scale([
colors.base00,
colors.base01,
@@ -45,17 +45,17 @@ const getTheme = (variant: Variant): ThemeConfig => {
colors.base06,
colors.base07,
]),
- red: colorRamp(chroma(colors.base08)),
- orange: colorRamp(chroma(colors.base09)),
- yellow: colorRamp(chroma(colors.base0A)),
- green: colorRamp(chroma(colors.base0B)),
- cyan: colorRamp(chroma(colors.base0C)),
- blue: colorRamp(chroma(colors.base0D)),
- violet: colorRamp(chroma(colors.base0E)),
- magenta: colorRamp(chroma(colors.base0F)),
+ red: color_ramp(chroma(colors.base08)),
+ orange: color_ramp(chroma(colors.base09)),
+ yellow: color_ramp(chroma(colors.base0A)),
+ green: color_ramp(chroma(colors.base0B)),
+ cyan: color_ramp(chroma(colors.base0C)),
+ blue: color_ramp(chroma(colors.base0D)),
+ violet: color_ramp(chroma(colors.base0E)),
+ magenta: color_ramp(chroma(colors.base0F)),
},
override: { syntax },
}
}
-export const theme = getTheme(variant)
+export const theme = get_theme(variant)
@@ -1,5 +1,5 @@
-import { chroma, ThemeAppearance, ThemeConfig, colorRamp } from "../../common"
-import { meta, buildSyntax, Variant } from "./common"
+import { chroma, ThemeAppearance, ThemeConfig, color_ramp } from "../../common"
+import { meta, build_syntax, Variant } from "./common"
const variant: Variant = {
colors: {
@@ -22,19 +22,19 @@ const variant: Variant = {
},
}
-const syntax = buildSyntax(variant)
+const syntax = build_syntax(variant)
-const getTheme = (variant: Variant): ThemeConfig => {
+const get_theme = (variant: Variant): ThemeConfig => {
const { colors } = variant
return {
name: `${meta.name} Lakeside Light`,
author: meta.author,
appearance: ThemeAppearance.Light,
- licenseType: meta.licenseType,
- licenseUrl: meta.licenseUrl,
- licenseFile: `${__dirname}/LICENSE`,
- inputColor: {
+ license_type: meta.license_type,
+ license_url: meta.license_url,
+ license_file: `${__dirname}/LICENSE`,
+ input_color: {
neutral: chroma.scale(
[
colors.base00,
@@ -47,17 +47,17 @@ const getTheme = (variant: Variant): ThemeConfig => {
colors.base07,
].reverse()
),
- red: colorRamp(chroma(colors.base08)),
- orange: colorRamp(chroma(colors.base09)),
- yellow: colorRamp(chroma(colors.base0A)),
- green: colorRamp(chroma(colors.base0B)),
- cyan: colorRamp(chroma(colors.base0C)),
- blue: colorRamp(chroma(colors.base0D)),
- violet: colorRamp(chroma(colors.base0E)),
- magenta: colorRamp(chroma(colors.base0F)),
+ red: color_ramp(chroma(colors.base08)),
+ orange: color_ramp(chroma(colors.base09)),
+ yellow: color_ramp(chroma(colors.base0A)),
+ green: color_ramp(chroma(colors.base0B)),
+ cyan: color_ramp(chroma(colors.base0C)),
+ blue: color_ramp(chroma(colors.base0D)),
+ violet: color_ramp(chroma(colors.base0E)),
+ magenta: color_ramp(chroma(colors.base0F)),
},
override: { syntax },
}
}
-export const theme = getTheme(variant)
+export const theme = get_theme(variant)
@@ -1,5 +1,5 @@
-import { chroma, ThemeAppearance, ThemeConfig, colorRamp } from "../../common"
-import { meta, buildSyntax, Variant } from "./common"
+import { chroma, ThemeAppearance, ThemeConfig, color_ramp } from "../../common"
+import { meta, build_syntax, Variant } from "./common"
const variant: Variant = {
colors: {
@@ -22,19 +22,19 @@ const variant: Variant = {
},
}
-const syntax = buildSyntax(variant)
+const syntax = build_syntax(variant)
-const getTheme = (variant: Variant): ThemeConfig => {
+const get_theme = (variant: Variant): ThemeConfig => {
const { colors } = variant
return {
name: `${meta.name} Plateau Dark`,
author: meta.author,
appearance: ThemeAppearance.Dark,
- licenseType: meta.licenseType,
- licenseUrl: meta.licenseUrl,
- licenseFile: `${__dirname}/LICENSE`,
- inputColor: {
+ license_type: meta.license_type,
+ license_url: meta.license_url,
+ license_file: `${__dirname}/LICENSE`,
+ input_color: {
neutral: chroma.scale([
colors.base00,
colors.base01,
@@ -45,17 +45,17 @@ const getTheme = (variant: Variant): ThemeConfig => {
colors.base06,
colors.base07,
]),
- red: colorRamp(chroma(colors.base08)),
- orange: colorRamp(chroma(colors.base09)),
- yellow: colorRamp(chroma(colors.base0A)),
- green: colorRamp(chroma(colors.base0B)),
- cyan: colorRamp(chroma(colors.base0C)),
- blue: colorRamp(chroma(colors.base0D)),
- violet: colorRamp(chroma(colors.base0E)),
- magenta: colorRamp(chroma(colors.base0F)),
+ red: color_ramp(chroma(colors.base08)),
+ orange: color_ramp(chroma(colors.base09)),
+ yellow: color_ramp(chroma(colors.base0A)),
+ green: color_ramp(chroma(colors.base0B)),
+ cyan: color_ramp(chroma(colors.base0C)),
+ blue: color_ramp(chroma(colors.base0D)),
+ violet: color_ramp(chroma(colors.base0E)),
+ magenta: color_ramp(chroma(colors.base0F)),
},
override: { syntax },
}
}
-export const theme = getTheme(variant)
+export const theme = get_theme(variant)
@@ -1,5 +1,5 @@
-import { chroma, ThemeAppearance, ThemeConfig, colorRamp } from "../../common"
-import { meta, buildSyntax, Variant } from "./common"
+import { chroma, ThemeAppearance, ThemeConfig, color_ramp } from "../../common"
+import { meta, build_syntax, Variant } from "./common"
const variant: Variant = {
colors: {
@@ -22,19 +22,19 @@ const variant: Variant = {
},
}
-const syntax = buildSyntax(variant)
+const syntax = build_syntax(variant)
-const getTheme = (variant: Variant): ThemeConfig => {
+const get_theme = (variant: Variant): ThemeConfig => {
const { colors } = variant
return {
name: `${meta.name} Plateau Light`,
author: meta.author,
appearance: ThemeAppearance.Light,
- licenseType: meta.licenseType,
- licenseUrl: meta.licenseUrl,
- licenseFile: `${__dirname}/LICENSE`,
- inputColor: {
+ license_type: meta.license_type,
+ license_url: meta.license_url,
+ license_file: `${__dirname}/LICENSE`,
+ input_color: {
neutral: chroma.scale(
[
colors.base00,
@@ -47,17 +47,17 @@ const getTheme = (variant: Variant): ThemeConfig => {
colors.base07,
].reverse()
),
- red: colorRamp(chroma(colors.base08)),
- orange: colorRamp(chroma(colors.base09)),
- yellow: colorRamp(chroma(colors.base0A)),
- green: colorRamp(chroma(colors.base0B)),
- cyan: colorRamp(chroma(colors.base0C)),
- blue: colorRamp(chroma(colors.base0D)),
- violet: colorRamp(chroma(colors.base0E)),
- magenta: colorRamp(chroma(colors.base0F)),
+ red: color_ramp(chroma(colors.base08)),
+ orange: color_ramp(chroma(colors.base09)),
+ yellow: color_ramp(chroma(colors.base0A)),
+ green: color_ramp(chroma(colors.base0B)),
+ cyan: color_ramp(chroma(colors.base0C)),
+ blue: color_ramp(chroma(colors.base0D)),
+ violet: color_ramp(chroma(colors.base0E)),
+ magenta: color_ramp(chroma(colors.base0F)),
},
override: { syntax },
}
}
-export const theme = getTheme(variant)
+export const theme = get_theme(variant)
@@ -1,5 +1,5 @@
-import { chroma, ThemeAppearance, ThemeConfig, colorRamp } from "../../common"
-import { meta, buildSyntax, Variant } from "./common"
+import { chroma, ThemeAppearance, ThemeConfig, color_ramp } from "../../common"
+import { meta, build_syntax, Variant } from "./common"
const variant: Variant = {
colors: {
@@ -22,19 +22,19 @@ const variant: Variant = {
},
}
-const syntax = buildSyntax(variant)
+const syntax = build_syntax(variant)
-const getTheme = (variant: Variant): ThemeConfig => {
+const get_theme = (variant: Variant): ThemeConfig => {
const { colors } = variant
return {
name: `${meta.name} Savanna Dark`,
author: meta.author,
appearance: ThemeAppearance.Dark,
- licenseType: meta.licenseType,
- licenseUrl: meta.licenseUrl,
- licenseFile: `${__dirname}/LICENSE`,
- inputColor: {
+ license_type: meta.license_type,
+ license_url: meta.license_url,
+ license_file: `${__dirname}/LICENSE`,
+ input_color: {
neutral: chroma.scale([
colors.base00,
colors.base01,
@@ -45,17 +45,17 @@ const getTheme = (variant: Variant): ThemeConfig => {
colors.base06,
colors.base07,
]),
- red: colorRamp(chroma(colors.base08)),
- orange: colorRamp(chroma(colors.base09)),
- yellow: colorRamp(chroma(colors.base0A)),
- green: colorRamp(chroma(colors.base0B)),
- cyan: colorRamp(chroma(colors.base0C)),
- blue: colorRamp(chroma(colors.base0D)),
- violet: colorRamp(chroma(colors.base0E)),
- magenta: colorRamp(chroma(colors.base0F)),
+ red: color_ramp(chroma(colors.base08)),
+ orange: color_ramp(chroma(colors.base09)),
+ yellow: color_ramp(chroma(colors.base0A)),
+ green: color_ramp(chroma(colors.base0B)),
+ cyan: color_ramp(chroma(colors.base0C)),
+ blue: color_ramp(chroma(colors.base0D)),
+ violet: color_ramp(chroma(colors.base0E)),
+ magenta: color_ramp(chroma(colors.base0F)),
},
override: { syntax },
}
}
-export const theme = getTheme(variant)
+export const theme = get_theme(variant)
@@ -1,5 +1,5 @@
-import { chroma, ThemeAppearance, ThemeConfig, colorRamp } from "../../common"
-import { meta, buildSyntax, Variant } from "./common"
+import { chroma, ThemeAppearance, ThemeConfig, color_ramp } from "../../common"
+import { meta, build_syntax, Variant } from "./common"
const variant: Variant = {
colors: {
@@ -22,19 +22,19 @@ const variant: Variant = {
},
}
-const syntax = buildSyntax(variant)
+const syntax = build_syntax(variant)
-const getTheme = (variant: Variant): ThemeConfig => {
+const get_theme = (variant: Variant): ThemeConfig => {
const { colors } = variant
return {
name: `${meta.name} Savanna Light`,
author: meta.author,
appearance: ThemeAppearance.Light,
- licenseType: meta.licenseType,
- licenseUrl: meta.licenseUrl,
- licenseFile: `${__dirname}/LICENSE`,
- inputColor: {
+ license_type: meta.license_type,
+ license_url: meta.license_url,
+ license_file: `${__dirname}/LICENSE`,
+ input_color: {
neutral: chroma.scale(
[
colors.base00,
@@ -47,17 +47,17 @@ const getTheme = (variant: Variant): ThemeConfig => {
colors.base07,
].reverse()
),
- red: colorRamp(chroma(colors.base08)),
- orange: colorRamp(chroma(colors.base09)),
- yellow: colorRamp(chroma(colors.base0A)),
- green: colorRamp(chroma(colors.base0B)),
- cyan: colorRamp(chroma(colors.base0C)),
- blue: colorRamp(chroma(colors.base0D)),
- violet: colorRamp(chroma(colors.base0E)),
- magenta: colorRamp(chroma(colors.base0F)),
+ red: color_ramp(chroma(colors.base08)),
+ orange: color_ramp(chroma(colors.base09)),
+ yellow: color_ramp(chroma(colors.base0A)),
+ green: color_ramp(chroma(colors.base0B)),
+ cyan: color_ramp(chroma(colors.base0C)),
+ blue: color_ramp(chroma(colors.base0D)),
+ violet: color_ramp(chroma(colors.base0E)),
+ magenta: color_ramp(chroma(colors.base0F)),
},
override: { syntax },
}
}
-export const theme = getTheme(variant)
+export const theme = get_theme(variant)
@@ -1,5 +1,5 @@
-import { chroma, ThemeAppearance, ThemeConfig, colorRamp } from "../../common"
-import { meta, buildSyntax, Variant } from "./common"
+import { chroma, ThemeAppearance, ThemeConfig, color_ramp } from "../../common"
+import { meta, build_syntax, Variant } from "./common"
const variant: Variant = {
colors: {
@@ -22,19 +22,19 @@ const variant: Variant = {
},
}
-const syntax = buildSyntax(variant)
+const syntax = build_syntax(variant)
-const getTheme = (variant: Variant): ThemeConfig => {
+const get_theme = (variant: Variant): ThemeConfig => {
const { colors } = variant
return {
name: `${meta.name} Seaside Dark`,
author: meta.author,
appearance: ThemeAppearance.Dark,
- licenseType: meta.licenseType,
- licenseUrl: meta.licenseUrl,
- licenseFile: `${__dirname}/LICENSE`,
- inputColor: {
+ license_type: meta.license_type,
+ license_url: meta.license_url,
+ license_file: `${__dirname}/LICENSE`,
+ input_color: {
neutral: chroma.scale([
colors.base00,
colors.base01,
@@ -45,17 +45,17 @@ const getTheme = (variant: Variant): ThemeConfig => {
colors.base06,
colors.base07,
]),
- red: colorRamp(chroma(colors.base08)),
- orange: colorRamp(chroma(colors.base09)),
- yellow: colorRamp(chroma(colors.base0A)),
- green: colorRamp(chroma(colors.base0B)),
- cyan: colorRamp(chroma(colors.base0C)),
- blue: colorRamp(chroma(colors.base0D)),
- violet: colorRamp(chroma(colors.base0E)),
- magenta: colorRamp(chroma(colors.base0F)),
+ red: color_ramp(chroma(colors.base08)),
+ orange: color_ramp(chroma(colors.base09)),
+ yellow: color_ramp(chroma(colors.base0A)),
+ green: color_ramp(chroma(colors.base0B)),
+ cyan: color_ramp(chroma(colors.base0C)),
+ blue: color_ramp(chroma(colors.base0D)),
+ violet: color_ramp(chroma(colors.base0E)),
+ magenta: color_ramp(chroma(colors.base0F)),
},
override: { syntax },
}
}
-export const theme = getTheme(variant)
+export const theme = get_theme(variant)
@@ -1,5 +1,5 @@
-import { chroma, ThemeAppearance, ThemeConfig, colorRamp } from "../../common"
-import { meta, buildSyntax, Variant } from "./common"
+import { chroma, ThemeAppearance, ThemeConfig, color_ramp } from "../../common"
+import { meta, build_syntax, Variant } from "./common"
const variant: Variant = {
colors: {
@@ -22,19 +22,19 @@ const variant: Variant = {
},
}
-const syntax = buildSyntax(variant)
+const syntax = build_syntax(variant)
-const getTheme = (variant: Variant): ThemeConfig => {
+const get_theme = (variant: Variant): ThemeConfig => {
const { colors } = variant
return {
name: `${meta.name} Seaside Light`,
author: meta.author,
appearance: ThemeAppearance.Light,
- licenseType: meta.licenseType,
- licenseUrl: meta.licenseUrl,
- licenseFile: `${__dirname}/LICENSE`,
- inputColor: {
+ license_type: meta.license_type,
+ license_url: meta.license_url,
+ license_file: `${__dirname}/LICENSE`,
+ input_color: {
neutral: chroma.scale(
[
colors.base00,
@@ -47,17 +47,17 @@ const getTheme = (variant: Variant): ThemeConfig => {
colors.base07,
].reverse()
),
- red: colorRamp(chroma(colors.base08)),
- orange: colorRamp(chroma(colors.base09)),
- yellow: colorRamp(chroma(colors.base0A)),
- green: colorRamp(chroma(colors.base0B)),
- cyan: colorRamp(chroma(colors.base0C)),
- blue: colorRamp(chroma(colors.base0D)),
- violet: colorRamp(chroma(colors.base0E)),
- magenta: colorRamp(chroma(colors.base0F)),
+ red: color_ramp(chroma(colors.base08)),
+ orange: color_ramp(chroma(colors.base09)),
+ yellow: color_ramp(chroma(colors.base0A)),
+ green: color_ramp(chroma(colors.base0B)),
+ cyan: color_ramp(chroma(colors.base0C)),
+ blue: color_ramp(chroma(colors.base0D)),
+ violet: color_ramp(chroma(colors.base0E)),
+ magenta: color_ramp(chroma(colors.base0F)),
},
override: { syntax },
}
}
-export const theme = getTheme(variant)
+export const theme = get_theme(variant)
@@ -1,5 +1,5 @@
-import { chroma, ThemeAppearance, ThemeConfig, colorRamp } from "../../common"
-import { meta, buildSyntax, Variant } from "./common"
+import { chroma, ThemeAppearance, ThemeConfig, color_ramp } from "../../common"
+import { meta, build_syntax, Variant } from "./common"
const variant: Variant = {
colors: {
@@ -22,19 +22,19 @@ const variant: Variant = {
},
}
-const syntax = buildSyntax(variant)
+const syntax = build_syntax(variant)
-const getTheme = (variant: Variant): ThemeConfig => {
+const get_theme = (variant: Variant): ThemeConfig => {
const { colors } = variant
return {
name: `${meta.name} Sulphurpool Dark`,
author: meta.author,
appearance: ThemeAppearance.Dark,
- licenseType: meta.licenseType,
- licenseUrl: meta.licenseUrl,
- licenseFile: `${__dirname}/LICENSE`,
- inputColor: {
+ license_type: meta.license_type,
+ license_url: meta.license_url,
+ license_file: `${__dirname}/LICENSE`,
+ input_color: {
neutral: chroma.scale([
colors.base00,
colors.base01,
@@ -45,17 +45,17 @@ const getTheme = (variant: Variant): ThemeConfig => {
colors.base06,
colors.base07,
]),
- red: colorRamp(chroma(colors.base08)),
- orange: colorRamp(chroma(colors.base09)),
- yellow: colorRamp(chroma(colors.base0A)),
- green: colorRamp(chroma(colors.base0B)),
- cyan: colorRamp(chroma(colors.base0C)),
- blue: colorRamp(chroma(colors.base0D)),
- violet: colorRamp(chroma(colors.base0E)),
- magenta: colorRamp(chroma(colors.base0F)),
+ red: color_ramp(chroma(colors.base08)),
+ orange: color_ramp(chroma(colors.base09)),
+ yellow: color_ramp(chroma(colors.base0A)),
+ green: color_ramp(chroma(colors.base0B)),
+ cyan: color_ramp(chroma(colors.base0C)),
+ blue: color_ramp(chroma(colors.base0D)),
+ violet: color_ramp(chroma(colors.base0E)),
+ magenta: color_ramp(chroma(colors.base0F)),
},
override: { syntax },
}
}
-export const theme = getTheme(variant)
+export const theme = get_theme(variant)
@@ -1,5 +1,5 @@
-import { chroma, ThemeAppearance, ThemeConfig, colorRamp } from "../../common"
-import { meta, buildSyntax, Variant } from "./common"
+import { chroma, ThemeAppearance, ThemeConfig, color_ramp } from "../../common"
+import { meta, build_syntax, Variant } from "./common"
const variant: Variant = {
colors: {
@@ -22,19 +22,19 @@ const variant: Variant = {
},
}
-const syntax = buildSyntax(variant)
+const syntax = build_syntax(variant)
-const getTheme = (variant: Variant): ThemeConfig => {
+const get_theme = (variant: Variant): ThemeConfig => {
const { colors } = variant
return {
name: `${meta.name} Sulphurpool Light`,
author: meta.author,
appearance: ThemeAppearance.Light,
- licenseType: meta.licenseType,
- licenseUrl: meta.licenseUrl,
- licenseFile: `${__dirname}/LICENSE`,
- inputColor: {
+ license_type: meta.license_type,
+ license_url: meta.license_url,
+ license_file: `${__dirname}/LICENSE`,
+ input_color: {
neutral: chroma.scale(
[
colors.base00,
@@ -47,17 +47,17 @@ const getTheme = (variant: Variant): ThemeConfig => {
colors.base07,
].reverse()
),
- red: colorRamp(chroma(colors.base08)),
- orange: colorRamp(chroma(colors.base09)),
- yellow: colorRamp(chroma(colors.base0A)),
- green: colorRamp(chroma(colors.base0B)),
- cyan: colorRamp(chroma(colors.base0C)),
- blue: colorRamp(chroma(colors.base0D)),
- violet: colorRamp(chroma(colors.base0E)),
- magenta: colorRamp(chroma(colors.base0F)),
+ red: color_ramp(chroma(colors.base08)),
+ orange: color_ramp(chroma(colors.base09)),
+ yellow: color_ramp(chroma(colors.base0A)),
+ green: color_ramp(chroma(colors.base0B)),
+ cyan: color_ramp(chroma(colors.base0C)),
+ blue: color_ramp(chroma(colors.base0D)),
+ violet: color_ramp(chroma(colors.base0E)),
+ magenta: color_ramp(chroma(colors.base0F)),
},
override: { syntax },
}
}
-export const theme = getTheme(variant)
+export const theme = get_theme(variant)
@@ -24,12 +24,12 @@ export interface Variant {
export const meta: ThemeFamilyMeta = {
name: "Atelier",
author: "Bram de Haan (http://atelierbramdehaan.nl)",
- licenseType: ThemeLicenseType.MIT,
- licenseUrl:
+ license_type: ThemeLicenseType.MIT,
+ license_url:
"https://atelierbram.github.io/syntax-highlighting/atelier-schemes/cave/",
}
-export const buildSyntax = (variant: Variant): ThemeSyntax => {
+export const build_syntax = (variant: Variant): ThemeSyntax => {
const { colors } = variant
return {
primary: { color: colors.base06 },
@@ -18,4 +18,4 @@ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
-SOFTWARE.
+SOFTWARE.
@@ -1,16 +1,16 @@
import { ThemeAppearance, ThemeConfig } from "../../common"
-import { ayu, meta, buildTheme } from "./common"
+import { ayu, meta, build_theme } from "./common"
const variant = ayu.dark
-const { ramps, syntax } = buildTheme(variant, false)
+const { ramps, syntax } = build_theme(variant, false)
export const theme: ThemeConfig = {
name: `${meta.name} Dark`,
author: meta.author,
appearance: ThemeAppearance.Dark,
- licenseType: meta.licenseType,
- licenseUrl: meta.licenseUrl,
- licenseFile: `${__dirname}/LICENSE`,
- inputColor: ramps,
+ license_type: meta.license_type,
+ license_url: meta.license_url,
+ license_file: `${__dirname}/LICENSE`,
+ input_color: ramps,
override: { syntax },
}
@@ -1,16 +1,16 @@
import { ThemeAppearance, ThemeConfig } from "../../common"
-import { ayu, meta, buildTheme } from "./common"
+import { ayu, meta, build_theme } from "./common"
const variant = ayu.light
-const { ramps, syntax } = buildTheme(variant, true)
+const { ramps, syntax } = build_theme(variant, true)
export const theme: ThemeConfig = {
name: `${meta.name} Light`,
author: meta.author,
appearance: ThemeAppearance.Light,
- licenseType: meta.licenseType,
- licenseUrl: meta.licenseUrl,
- licenseFile: `${__dirname}/LICENSE`,
- inputColor: ramps,
+ license_type: meta.license_type,
+ license_url: meta.license_url,
+ license_file: `${__dirname}/LICENSE`,
+ input_color: ramps,
override: { syntax },
}
@@ -1,16 +1,16 @@
import { ThemeAppearance, ThemeConfig } from "../../common"
-import { ayu, meta, buildTheme } from "./common"
+import { ayu, meta, build_theme } from "./common"
const variant = ayu.mirage
-const { ramps, syntax } = buildTheme(variant, false)
+const { ramps, syntax } = build_theme(variant, false)
export const theme: ThemeConfig = {
name: `${meta.name} Mirage`,
author: meta.author,
appearance: ThemeAppearance.Dark,
- licenseType: meta.licenseType,
- licenseUrl: meta.licenseUrl,
- licenseFile: `${__dirname}/LICENSE`,
- inputColor: ramps,
+ license_type: meta.license_type,
+ license_url: meta.license_url,
+ license_file: `${__dirname}/LICENSE`,
+ input_color: ramps,
override: { syntax },
}
@@ -1,7 +1,7 @@
import { dark, light, mirage } from "ayu"
import {
chroma,
- colorRamp,
+ color_ramp,
ThemeLicenseType,
ThemeSyntax,
ThemeFamilyMeta,
@@ -13,16 +13,16 @@ export const ayu = {
mirage,
}
-export const buildTheme = (t: typeof dark, light: boolean) => {
+export const build_theme = (t: typeof dark, light: boolean) => {
const color = {
- lightBlue: t.syntax.tag.hex(),
+ light_blue: t.syntax.tag.hex(),
yellow: t.syntax.func.hex(),
blue: t.syntax.entity.hex(),
green: t.syntax.string.hex(),
teal: t.syntax.regexp.hex(),
red: t.syntax.markup.hex(),
orange: t.syntax.keyword.hex(),
- lightYellow: t.syntax.special.hex(),
+ light_yellow: t.syntax.special.hex(),
gray: t.syntax.comment.hex(),
purple: t.syntax.constant.hex(),
}
@@ -48,20 +48,20 @@ export const buildTheme = (t: typeof dark, light: boolean) => {
light ? t.editor.fg.hex() : t.editor.bg.hex(),
light ? t.editor.bg.hex() : t.editor.fg.hex(),
]),
- red: colorRamp(chroma(color.red)),
- orange: colorRamp(chroma(color.orange)),
- yellow: colorRamp(chroma(color.yellow)),
- green: colorRamp(chroma(color.green)),
- cyan: colorRamp(chroma(color.teal)),
- blue: colorRamp(chroma(color.blue)),
- violet: colorRamp(chroma(color.purple)),
- magenta: colorRamp(chroma(color.lightBlue)),
+ red: color_ramp(chroma(color.red)),
+ orange: color_ramp(chroma(color.orange)),
+ yellow: color_ramp(chroma(color.yellow)),
+ green: color_ramp(chroma(color.green)),
+ cyan: color_ramp(chroma(color.teal)),
+ blue: color_ramp(chroma(color.blue)),
+ violet: color_ramp(chroma(color.purple)),
+ magenta: color_ramp(chroma(color.light_blue)),
},
syntax,
}
}
-export const buildSyntax = (t: typeof dark): ThemeSyntax => {
+export const build_syntax = (t: typeof dark): ThemeSyntax => {
return {
constant: { color: t.syntax.constant.hex() },
"string.regex": { color: t.syntax.regexp.hex() },
@@ -80,6 +80,6 @@ export const buildSyntax = (t: typeof dark): ThemeSyntax => {
export const meta: ThemeFamilyMeta = {
name: "Ayu",
author: "dempfi",
- licenseType: ThemeLicenseType.MIT,
- licenseUrl: "https://github.com/dempfi/ayu",
+ license_type: ThemeLicenseType.MIT,
+ license_url: "https://github.com/dempfi/ayu",
}
@@ -18,4 +18,4 @@ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
-SOFTWARE.
+SOFTWARE.
@@ -1,6 +1,6 @@
import {
chroma,
- colorRamp,
+ color_ramp,
ThemeAppearance,
ThemeLicenseType,
ThemeConfig,
@@ -11,8 +11,8 @@ import {
const meta: ThemeFamilyMeta = {
name: "Gruvbox",
author: "morhetz <morhetz@gmail.com>",
- licenseType: ThemeLicenseType.MIT,
- licenseUrl: "https://github.com/morhetz/gruvbox",
+ license_type: ThemeLicenseType.MIT,
+ license_url: "https://github.com/morhetz/gruvbox",
}
const color = {
@@ -73,7 +73,7 @@ interface ThemeColors {
gray: string
}
-const darkNeutrals = [
+const dark_neutrals = [
color.dark1,
color.dark2,
color.dark3,
@@ -96,7 +96,7 @@ const dark: ThemeColors = {
gray: color.light4,
}
-const lightNeutrals = [
+const light_neutrals = [
color.light1,
color.light2,
color.light3,
@@ -119,14 +119,6 @@ const light: ThemeColors = {
gray: color.dark4,
}
-const darkHardNeutral = [color.dark0_hard, ...darkNeutrals]
-const darkNeutral = [color.dark0, ...darkNeutrals]
-const darkSoftNeutral = [color.dark0_soft, ...darkNeutrals]
-
-const lightHardNeutral = [color.light0_hard, ...lightNeutrals]
-const lightNeutral = [color.light0, ...lightNeutrals]
-const lightSoftNeutral = [color.light0_soft, ...lightNeutrals]
-
interface Variant {
name: string
appearance: "light" | "dark"
@@ -167,61 +159,68 @@ const variant: Variant[] = [
},
]
-const buildVariant = (variant: Variant): ThemeConfig => {
+const dark_hard_neutral = [color.dark0_hard, ...dark_neutrals]
+const dark_neutral = [color.dark0, ...dark_neutrals]
+const dark_soft_neutral = [color.dark0_soft, ...dark_neutrals]
+
+const light_hard_neutral = [color.light0_hard, ...light_neutrals]
+const light_neutral = [color.light0, ...light_neutrals]
+const light_soft_neutral = [color.light0_soft, ...light_neutrals]
+
+const build_variant = (variant: Variant): ThemeConfig => {
const { colors } = variant
const name = `Gruvbox ${variant.name}`
- const isLight = variant.appearance === "light"
+ const is_light = variant.appearance === "light"
let neutral: string[] = []
switch (variant.name) {
- case "Dark Hard": {
- neutral = darkHardNeutral
+ case "Dark Hard":
+ neutral = dark_hard_neutral
break
- }
- case "Dark": {
- neutral = darkNeutral
+
+ case "Dark":
+ neutral = dark_neutral
break
- }
- case "Dark Soft": {
- neutral = darkSoftNeutral
+
+ case "Dark Soft":
+ neutral = dark_soft_neutral
break
- }
- case "Light Hard": {
- neutral = lightHardNeutral
+
+ case "Light Hard":
+ neutral = light_hard_neutral
break
- }
- case "Light": {
- neutral = lightNeutral
+
+ case "Light":
+ neutral = light_neutral
break
- }
- case "Light Soft": {
- neutral = lightSoftNeutral
+
+ case "Light Soft":
+ neutral = light_soft_neutral
break
- }
}
const ramps = {
- neutral: chroma.scale(isLight ? neutral.reverse() : neutral),
- red: colorRamp(chroma(variant.colors.red)),
- orange: colorRamp(chroma(variant.colors.orange)),
- yellow: colorRamp(chroma(variant.colors.yellow)),
- green: colorRamp(chroma(variant.colors.green)),
- cyan: colorRamp(chroma(variant.colors.aqua)),
- blue: colorRamp(chroma(variant.colors.blue)),
- violet: colorRamp(chroma(variant.colors.purple)),
- magenta: colorRamp(chroma(variant.colors.gray)),
+ neutral: chroma.scale(is_light ? neutral.reverse() : neutral),
+ red: color_ramp(chroma(variant.colors.red)),
+ orange: color_ramp(chroma(variant.colors.orange)),
+ yellow: color_ramp(chroma(variant.colors.yellow)),
+ green: color_ramp(chroma(variant.colors.green)),
+ cyan: color_ramp(chroma(variant.colors.aqua)),
+ blue: color_ramp(chroma(variant.colors.blue)),
+ violet: color_ramp(chroma(variant.colors.purple)),
+ magenta: color_ramp(chroma(variant.colors.gray)),
}
const syntax: ThemeSyntax = {
- primary: { color: neutral[isLight ? 0 : 8] },
+ primary: { color: neutral[is_light ? 0 : 8] },
"text.literal": { color: colors.blue },
comment: { color: colors.gray },
- punctuation: { color: neutral[isLight ? 1 : 7] },
- "punctuation.bracket": { color: neutral[isLight ? 3 : 5] },
- "punctuation.list_marker": { color: neutral[isLight ? 0 : 8] },
+ punctuation: { color: neutral[is_light ? 1 : 7] },
+ "punctuation.bracket": { color: neutral[is_light ? 3 : 5] },
+ "punctuation.list_marker": { color: neutral[is_light ? 0 : 8] },
operator: { color: colors.aqua },
boolean: { color: colors.purple },
number: { color: colors.purple },
@@ -237,10 +236,10 @@ const buildVariant = (variant: Variant): ThemeConfig => {
function: { color: colors.green },
"function.builtin": { color: colors.red },
variable: { color: colors.blue },
- property: { color: neutral[isLight ? 0 : 8] },
+ property: { color: neutral[is_light ? 0 : 8] },
embedded: { color: colors.aqua },
- linkText: { color: colors.aqua },
- linkUri: { color: colors.purple },
+ link_text: { color: colors.aqua },
+ link_uri: { color: colors.purple },
title: { color: colors.green },
}
@@ -248,18 +247,18 @@ const buildVariant = (variant: Variant): ThemeConfig => {
name,
author: meta.author,
appearance: variant.appearance as ThemeAppearance,
- licenseType: meta.licenseType,
- licenseUrl: meta.licenseUrl,
- licenseFile: `${__dirname}/LICENSE`,
- inputColor: ramps,
+ license_type: meta.license_type,
+ license_url: meta.license_url,
+ license_file: `${__dirname}/LICENSE`,
+ input_color: ramps,
override: { syntax },
}
}
// Variants
-export const darkHard = buildVariant(variant[0])
-export const darkDefault = buildVariant(variant[1])
-export const darkSoft = buildVariant(variant[2])
-export const lightHard = buildVariant(variant[3])
-export const lightDefault = buildVariant(variant[4])
-export const lightSoft = buildVariant(variant[5])
+export const dark_hard = build_variant(variant[0])
+export const dark_default = build_variant(variant[1])
+export const dark_soft = build_variant(variant[2])
+export const light_hard = build_variant(variant[3])
+export const light_default = build_variant(variant[4])
+export const light_soft = build_variant(variant[5])
@@ -1 +1 @@
-export { darkHard } from "./gruvbox-common"
+export { dark_hard } from "./gruvbox-common"
@@ -1 +1 @@
-export { darkSoft } from "./gruvbox-common"
+export { dark_soft } from "./gruvbox-common"
@@ -1 +1 @@
-export { darkDefault } from "./gruvbox-common"
+export { dark_default } from "./gruvbox-common"
@@ -1 +1 @@
-export { lightHard } from "./gruvbox-common"
+export { light_hard } from "./gruvbox-common"
@@ -1 +1 @@
-export { lightSoft } from "./gruvbox-common"
+export { light_soft } from "./gruvbox-common"
@@ -1 +1 @@
-export { lightDefault } from "./gruvbox-common"
+export { light_default } from "./gruvbox-common"
@@ -1,82 +1,82 @@
import { ThemeConfig } from "../theme"
-import { darkDefault as gruvboxDark } from "./gruvbox/gruvbox-dark"
-import { darkHard as gruvboxDarkHard } from "./gruvbox/gruvbox-dark-hard"
-import { darkSoft as gruvboxDarkSoft } from "./gruvbox/gruvbox-dark-soft"
-import { lightDefault as gruvboxLight } from "./gruvbox/gruvbox-light"
-import { lightHard as gruvboxLightHard } from "./gruvbox/gruvbox-light-hard"
-import { lightSoft as gruvboxLightSoft } from "./gruvbox/gruvbox-light-soft"
-import { dark as solarizedDark } from "./solarized/solarized"
-import { light as solarizedLight } from "./solarized/solarized"
-import { dark as andromedaDark } from "./andromeda/andromeda"
-import { theme as oneDark } from "./one/one-dark"
-import { theme as oneLight } from "./one/one-light"
-import { theme as ayuLight } from "./ayu/ayu-light"
-import { theme as ayuDark } from "./ayu/ayu-dark"
-import { theme as ayuMirage } from "./ayu/ayu-mirage"
-import { theme as rosePine } from "./rose-pine/rose-pine"
-import { theme as rosePineDawn } from "./rose-pine/rose-pine-dawn"
-import { theme as rosePineMoon } from "./rose-pine/rose-pine-moon"
+import { dark_default as gruvbox_dark } from "./gruvbox/gruvbox-dark"
+import { dark_hard as gruvbox_dark_hard } from "./gruvbox/gruvbox-dark-hard"
+import { dark_soft as gruvbox_dark_soft } from "./gruvbox/gruvbox-dark-soft"
+import { light_default as gruvbox_light } from "./gruvbox/gruvbox-light"
+import { light_hard as gruvbox_light_hard } from "./gruvbox/gruvbox-light-hard"
+import { light_soft as gruvbox_light_soft } from "./gruvbox/gruvbox-light-soft"
+import { dark as solarized_dark } from "./solarized/solarized"
+import { light as solarized_light } from "./solarized/solarized"
+import { dark as andromeda_dark } from "./andromeda/andromeda"
+import { theme as one_dark } from "./one/one-dark"
+import { theme as one_light } from "./one/one-light"
+import { theme as ayu_light } from "./ayu/ayu-light"
+import { theme as ayu_dark } from "./ayu/ayu-dark"
+import { theme as ayu_mirage } from "./ayu/ayu-mirage"
+import { theme as rose_pine } from "./rose-pine/rose-pine"
+import { theme as rose_pine_dawn } from "./rose-pine/rose-pine-dawn"
+import { theme as rose_pine_moon } from "./rose-pine/rose-pine-moon"
import { theme as sandcastle } from "./sandcastle/sandcastle"
import { theme as summercamp } from "./summercamp/summercamp"
-import { theme as atelierCaveDark } from "./atelier/atelier-cave-dark"
-import { theme as atelierCaveLight } from "./atelier/atelier-cave-light"
-import { theme as atelierDuneDark } from "./atelier/atelier-dune-dark"
-import { theme as atelierDuneLight } from "./atelier/atelier-dune-light"
-import { theme as atelierEstuaryDark } from "./atelier/atelier-estuary-dark"
-import { theme as atelierEstuaryLight } from "./atelier/atelier-estuary-light"
-import { theme as atelierForestDark } from "./atelier/atelier-forest-dark"
-import { theme as atelierForestLight } from "./atelier/atelier-forest-light"
-import { theme as atelierHeathDark } from "./atelier/atelier-heath-dark"
-import { theme as atelierHeathLight } from "./atelier/atelier-heath-light"
-import { theme as atelierLakesideDark } from "./atelier/atelier-lakeside-dark"
-import { theme as atelierLakesideLight } from "./atelier/atelier-lakeside-light"
-import { theme as atelierPlateauDark } from "./atelier/atelier-plateau-dark"
-import { theme as atelierPlateauLight } from "./atelier/atelier-plateau-light"
-import { theme as atelierSavannaDark } from "./atelier/atelier-savanna-dark"
-import { theme as atelierSavannaLight } from "./atelier/atelier-savanna-light"
-import { theme as atelierSeasideDark } from "./atelier/atelier-seaside-dark"
-import { theme as atelierSeasideLight } from "./atelier/atelier-seaside-light"
-import { theme as atelierSulphurpoolDark } from "./atelier/atelier-sulphurpool-dark"
-import { theme as atelierSulphurpoolLight } from "./atelier/atelier-sulphurpool-light"
+import { theme as atelier_cave_dark } from "./atelier/atelier-cave-dark"
+import { theme as atelier_cave_light } from "./atelier/atelier-cave-light"
+import { theme as atelier_dune_dark } from "./atelier/atelier-dune-dark"
+import { theme as atelier_dune_light } from "./atelier/atelier-dune-light"
+import { theme as atelier_estuary_dark } from "./atelier/atelier-estuary-dark"
+import { theme as atelier_estuary_light } from "./atelier/atelier-estuary-light"
+import { theme as atelier_forest_dark } from "./atelier/atelier-forest-dark"
+import { theme as atelier_forest_light } from "./atelier/atelier-forest-light"
+import { theme as atelier_heath_dark } from "./atelier/atelier-heath-dark"
+import { theme as atelier_heath_light } from "./atelier/atelier-heath-light"
+import { theme as atelier_lakeside_dark } from "./atelier/atelier-lakeside-dark"
+import { theme as atelier_lakeside_light } from "./atelier/atelier-lakeside-light"
+import { theme as atelier_plateau_dark } from "./atelier/atelier-plateau-dark"
+import { theme as atelier_plateau_light } from "./atelier/atelier-plateau-light"
+import { theme as atelier_savanna_dark } from "./atelier/atelier-savanna-dark"
+import { theme as atelier_savanna_light } from "./atelier/atelier-savanna-light"
+import { theme as atelier_seaside_dark } from "./atelier/atelier-seaside-dark"
+import { theme as atelier_seaside_light } from "./atelier/atelier-seaside-light"
+import { theme as atelier_sulphurpool_dark } from "./atelier/atelier-sulphurpool-dark"
+import { theme as atelier_sulphurpool_light } from "./atelier/atelier-sulphurpool-light"
export const themes: ThemeConfig[] = [
- oneDark,
- oneLight,
- ayuLight,
- ayuDark,
- ayuMirage,
- gruvboxDark,
- gruvboxDarkHard,
- gruvboxDarkSoft,
- gruvboxLight,
- gruvboxLightHard,
- gruvboxLightSoft,
- rosePine,
- rosePineDawn,
- rosePineMoon,
+ one_dark,
+ one_light,
+ ayu_light,
+ ayu_dark,
+ ayu_mirage,
+ gruvbox_dark,
+ gruvbox_dark_hard,
+ gruvbox_dark_soft,
+ gruvbox_light,
+ gruvbox_light_hard,
+ gruvbox_light_soft,
+ rose_pine,
+ rose_pine_dawn,
+ rose_pine_moon,
sandcastle,
- solarizedDark,
- solarizedLight,
- andromedaDark,
+ solarized_dark,
+ solarized_light,
+ andromeda_dark,
summercamp,
- atelierCaveDark,
- atelierCaveLight,
- atelierDuneDark,
- atelierDuneLight,
- atelierEstuaryDark,
- atelierEstuaryLight,
- atelierForestDark,
- atelierForestLight,
- atelierHeathDark,
- atelierHeathLight,
- atelierLakesideDark,
- atelierLakesideLight,
- atelierPlateauDark,
- atelierPlateauLight,
- atelierSavannaDark,
- atelierSavannaLight,
- atelierSeasideDark,
- atelierSeasideLight,
- atelierSulphurpoolDark,
- atelierSulphurpoolLight,
+ atelier_cave_dark,
+ atelier_cave_light,
+ atelier_dune_dark,
+ atelier_dune_light,
+ atelier_estuary_dark,
+ atelier_estuary_light,
+ atelier_forest_dark,
+ atelier_forest_light,
+ atelier_heath_dark,
+ atelier_heath_light,
+ atelier_lakeside_dark,
+ atelier_lakeside_light,
+ atelier_plateau_dark,
+ atelier_plateau_light,
+ atelier_savanna_dark,
+ atelier_savanna_light,
+ atelier_seaside_dark,
+ atelier_seaside_light,
+ atelier_sulphurpool_dark,
+ atelier_sulphurpool_light,
]
@@ -18,4 +18,4 @@ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
-SOFTWARE.
+SOFTWARE.
@@ -1,7 +1,7 @@
import {
chroma,
- fontWeights,
- colorRamp,
+ font_weights,
+ color_ramp,
ThemeAppearance,
ThemeLicenseType,
ThemeConfig,
@@ -11,7 +11,7 @@ const color = {
white: "#ACB2BE",
grey: "#5D636F",
red: "#D07277",
- darkRed: "#B1574B",
+ dark_red: "#B1574B",
orange: "#C0966B",
yellow: "#DFC184",
green: "#A1C181",
@@ -24,10 +24,11 @@ export const theme: ThemeConfig = {
name: "One Dark",
author: "simurai",
appearance: ThemeAppearance.Dark,
- licenseType: ThemeLicenseType.MIT,
- licenseUrl: "https://github.com/atom/atom/tree/master/packages/one-dark-ui",
- licenseFile: `${__dirname}/LICENSE`,
- inputColor: {
+ license_type: ThemeLicenseType.MIT,
+ license_url:
+ "https://github.com/atom/atom/tree/master/packages/one-dark-ui",
+ license_file: `${__dirname}/LICENSE`,
+ input_color: {
neutral: chroma
.scale([
"#282c34",
@@ -40,14 +41,14 @@ export const theme: ThemeConfig = {
"#c8ccd4",
])
.domain([0.05, 0.22, 0.25, 0.45, 0.62, 0.8, 0.9, 1]),
- red: colorRamp(chroma(color.red)),
- orange: colorRamp(chroma(color.orange)),
- yellow: colorRamp(chroma(color.yellow)),
- green: colorRamp(chroma(color.green)),
- cyan: colorRamp(chroma(color.teal)),
- blue: colorRamp(chroma(color.blue)),
- violet: colorRamp(chroma(color.purple)),
- magenta: colorRamp(chroma("#be5046")),
+ red: color_ramp(chroma(color.red)),
+ orange: color_ramp(chroma(color.orange)),
+ yellow: color_ramp(chroma(color.yellow)),
+ green: color_ramp(chroma(color.green)),
+ cyan: color_ramp(chroma(color.teal)),
+ blue: color_ramp(chroma(color.blue)),
+ violet: color_ramp(chroma(color.purple)),
+ magenta: color_ramp(chroma("#be5046")),
},
override: {
syntax: {
@@ -57,8 +58,8 @@ export const theme: ThemeConfig = {
"emphasis.strong": { color: color.orange },
function: { color: color.blue },
keyword: { color: color.purple },
- linkText: { color: color.blue, italic: false },
- linkUri: { color: color.teal },
+ link_text: { color: color.blue, italic: false },
+ link_uri: { color: color.teal },
number: { color: color.orange },
constant: { color: color.yellow },
operator: { color: color.teal },
@@ -66,9 +67,9 @@ export const theme: ThemeConfig = {
property: { color: color.red },
punctuation: { color: color.white },
"punctuation.list_marker": { color: color.red },
- "punctuation.special": { color: color.darkRed },
+ "punctuation.special": { color: color.dark_red },
string: { color: color.green },
- title: { color: color.red, weight: fontWeights.normal },
+ title: { color: color.red, weight: font_weights.normal },
"text.literal": { color: color.green },
type: { color: color.teal },
"variable.special": { color: color.orange },
@@ -1,7 +1,7 @@
import {
chroma,
- fontWeights,
- colorRamp,
+ font_weights,
+ color_ramp,
ThemeAppearance,
ThemeLicenseType,
ThemeConfig,
@@ -11,7 +11,7 @@ const color = {
black: "#383A41",
grey: "#A2A3A7",
red: "#D36050",
- darkRed: "#B92C46",
+ dark_red: "#B92C46",
orange: "#AD6F26",
yellow: "#DFC184",
green: "#659F58",
@@ -25,11 +25,11 @@ export const theme: ThemeConfig = {
name: "One Light",
author: "simurai",
appearance: ThemeAppearance.Light,
- licenseType: ThemeLicenseType.MIT,
- licenseUrl:
+ license_type: ThemeLicenseType.MIT,
+ license_url:
"https://github.com/atom/atom/tree/master/packages/one-light-ui",
- licenseFile: `${__dirname}/LICENSE`,
- inputColor: {
+ license_file: `${__dirname}/LICENSE`,
+ input_color: {
neutral: chroma
.scale([
"#383A41",
@@ -42,14 +42,14 @@ export const theme: ThemeConfig = {
"#FAFAFA",
])
.domain([0.05, 0.22, 0.25, 0.45, 0.62, 0.8, 0.9, 1]),
- red: colorRamp(chroma(color.red)),
- orange: colorRamp(chroma(color.orange)),
- yellow: colorRamp(chroma(color.yellow)),
- green: colorRamp(chroma(color.green)),
- cyan: colorRamp(chroma(color.teal)),
- blue: colorRamp(chroma(color.blue)),
- violet: colorRamp(chroma(color.purple)),
- magenta: colorRamp(chroma(color.magenta)),
+ red: color_ramp(chroma(color.red)),
+ orange: color_ramp(chroma(color.orange)),
+ yellow: color_ramp(chroma(color.yellow)),
+ green: color_ramp(chroma(color.green)),
+ cyan: color_ramp(chroma(color.teal)),
+ blue: color_ramp(chroma(color.blue)),
+ violet: color_ramp(chroma(color.purple)),
+ magenta: color_ramp(chroma(color.magenta)),
},
override: {
syntax: {
@@ -59,17 +59,17 @@ export const theme: ThemeConfig = {
"emphasis.strong": { color: color.orange },
function: { color: color.blue },
keyword: { color: color.purple },
- linkText: { color: color.blue },
- linkUri: { color: color.teal },
+ link_text: { color: color.blue },
+ link_uri: { color: color.teal },
number: { color: color.orange },
operator: { color: color.teal },
primary: { color: color.black },
property: { color: color.red },
punctuation: { color: color.black },
"punctuation.list_marker": { color: color.red },
- "punctuation.special": { color: color.darkRed },
+ "punctuation.special": { color: color.dark_red },
string: { color: color.green },
- title: { color: color.red, weight: fontWeights.normal },
+ title: { color: color.red, weight: font_weights.normal },
"text.literal": { color: color.green },
type: { color: color.teal },
"variable.special": { color: color.orange },
@@ -18,4 +18,4 @@ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
-SOFTWARE.
+SOFTWARE.
@@ -14,9 +14,9 @@ export const color = {
pine: "#31748f",
foam: "#9ccfd8",
iris: "#c4a7e7",
- highlightLow: "#21202e",
- highlightMed: "#403d52",
- highlightHigh: "#524f67",
+ highlight_low: "#21202e",
+ highlight_med: "#403d52",
+ highlight_high: "#524f67",
},
moon: {
base: "#232136",
@@ -31,9 +31,9 @@ export const color = {
pine: "#3e8fb0",
foam: "#9ccfd8",
iris: "#c4a7e7",
- highlightLow: "#2a283e",
- highlightMed: "#44415a",
- highlightHigh: "#56526e",
+ highlight_low: "#2a283e",
+ highlight_med: "#44415a",
+ highlight_high: "#56526e",
},
dawn: {
base: "#faf4ed",
@@ -48,9 +48,9 @@ export const color = {
pine: "#286983",
foam: "#56949f",
iris: "#907aa9",
- highlightLow: "#f4ede8",
- highlightMed: "#dfdad9",
- highlightHigh: "#cecacd",
+ highlight_low: "#f4ede8",
+ highlight_med: "#dfdad9",
+ highlight_high: "#cecacd",
},
}
@@ -69,7 +69,7 @@ export const syntax = (c: typeof color.default): Partial<ThemeSyntax> => {
tag: { color: c.foam },
"function.method": { color: c.rose },
title: { color: c.gold },
- linkText: { color: c.foam, italic: false },
- linkUri: { color: c.rose },
+ link_text: { color: c.foam, italic: false },
+ link_uri: { color: c.rose },
}
}
@@ -1,6 +1,6 @@
import {
chroma,
- colorRamp,
+ color_ramp,
ThemeAppearance,
ThemeLicenseType,
ThemeConfig,
@@ -17,16 +17,16 @@ export const theme: ThemeConfig = {
name: "RosΓ© Pine Dawn",
author: "edunfelt",
appearance: ThemeAppearance.Light,
- licenseType: ThemeLicenseType.MIT,
- licenseUrl: "https://github.com/edunfelt/base16-rose-pine-scheme",
- licenseFile: `${__dirname}/LICENSE`,
- inputColor: {
+ license_type: ThemeLicenseType.MIT,
+ license_url: "https://github.com/edunfelt/base16-rose-pine-scheme",
+ license_file: `${__dirname}/LICENSE`,
+ input_color: {
neutral: chroma
.scale(
[
color.base,
color.surface,
- color.highlightHigh,
+ color.highlight_high,
color.overlay,
color.muted,
color.subtle,
@@ -34,14 +34,14 @@ export const theme: ThemeConfig = {
].reverse()
)
.domain([0, 0.35, 0.45, 0.65, 0.7, 0.8, 0.9, 1]),
- red: colorRamp(chroma(color.love)),
- orange: colorRamp(chroma(color.iris)),
- yellow: colorRamp(chroma(color.gold)),
- green: colorRamp(chroma(green)),
- cyan: colorRamp(chroma(color.pine)),
- blue: colorRamp(chroma(color.foam)),
- violet: colorRamp(chroma(color.iris)),
- magenta: colorRamp(chroma(magenta)),
+ red: color_ramp(chroma(color.love)),
+ orange: color_ramp(chroma(color.iris)),
+ yellow: color_ramp(chroma(color.gold)),
+ green: color_ramp(chroma(green)),
+ cyan: color_ramp(chroma(color.pine)),
+ blue: color_ramp(chroma(color.foam)),
+ violet: color_ramp(chroma(color.iris)),
+ magenta: color_ramp(chroma(magenta)),
},
override: {
syntax: syntax(color),
@@ -1,6 +1,6 @@
import {
chroma,
- colorRamp,
+ color_ramp,
ThemeAppearance,
ThemeLicenseType,
ThemeConfig,
@@ -17,29 +17,29 @@ export const theme: ThemeConfig = {
name: "RosΓ© Pine Moon",
author: "edunfelt",
appearance: ThemeAppearance.Dark,
- licenseType: ThemeLicenseType.MIT,
- licenseUrl: "https://github.com/edunfelt/base16-rose-pine-scheme",
- licenseFile: `${__dirname}/LICENSE`,
- inputColor: {
+ license_type: ThemeLicenseType.MIT,
+ license_url: "https://github.com/edunfelt/base16-rose-pine-scheme",
+ license_file: `${__dirname}/LICENSE`,
+ input_color: {
neutral: chroma
.scale([
color.base,
color.surface,
- color.highlightHigh,
+ color.highlight_high,
color.overlay,
color.muted,
color.subtle,
color.text,
])
.domain([0, 0.3, 0.55, 1]),
- red: colorRamp(chroma(color.love)),
- orange: colorRamp(chroma(color.iris)),
- yellow: colorRamp(chroma(color.gold)),
- green: colorRamp(chroma(green)),
- cyan: colorRamp(chroma(color.pine)),
- blue: colorRamp(chroma(color.foam)),
- violet: colorRamp(chroma(color.iris)),
- magenta: colorRamp(chroma(magenta)),
+ red: color_ramp(chroma(color.love)),
+ orange: color_ramp(chroma(color.iris)),
+ yellow: color_ramp(chroma(color.gold)),
+ green: color_ramp(chroma(green)),
+ cyan: color_ramp(chroma(color.pine)),
+ blue: color_ramp(chroma(color.foam)),
+ violet: color_ramp(chroma(color.iris)),
+ magenta: color_ramp(chroma(magenta)),
},
override: {
syntax: syntax(color),
@@ -1,6 +1,6 @@
import {
chroma,
- colorRamp,
+ color_ramp,
ThemeAppearance,
ThemeLicenseType,
ThemeConfig,
@@ -16,27 +16,27 @@ export const theme: ThemeConfig = {
name: "RosΓ© Pine",
author: "edunfelt",
appearance: ThemeAppearance.Dark,
- licenseType: ThemeLicenseType.MIT,
- licenseUrl: "https://github.com/edunfelt/base16-rose-pine-scheme",
- licenseFile: `${__dirname}/LICENSE`,
- inputColor: {
+ license_type: ThemeLicenseType.MIT,
+ license_url: "https://github.com/edunfelt/base16-rose-pine-scheme",
+ license_file: `${__dirname}/LICENSE`,
+ input_color: {
neutral: chroma.scale([
color.base,
color.surface,
- color.highlightHigh,
+ color.highlight_high,
color.overlay,
color.muted,
color.subtle,
color.text,
]),
- red: colorRamp(chroma(color.love)),
- orange: colorRamp(chroma(color.iris)),
- yellow: colorRamp(chroma(color.gold)),
- green: colorRamp(chroma(green)),
- cyan: colorRamp(chroma(color.pine)),
- blue: colorRamp(chroma(color.foam)),
- violet: colorRamp(chroma(color.iris)),
- magenta: colorRamp(chroma(magenta)),
+ red: color_ramp(chroma(color.love)),
+ orange: color_ramp(chroma(color.iris)),
+ yellow: color_ramp(chroma(color.gold)),
+ green: color_ramp(chroma(green)),
+ cyan: color_ramp(chroma(color.pine)),
+ blue: color_ramp(chroma(color.foam)),
+ violet: color_ramp(chroma(color.iris)),
+ magenta: color_ramp(chroma(magenta)),
},
override: {
syntax: syntax(color),
@@ -18,4 +18,4 @@ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
-SOFTWARE.
+SOFTWARE.
@@ -1,6 +1,6 @@
import {
chroma,
- colorRamp,
+ color_ramp,
ThemeAppearance,
ThemeLicenseType,
ThemeConfig,
@@ -10,10 +10,10 @@ export const theme: ThemeConfig = {
name: "Sandcastle",
author: "gessig",
appearance: ThemeAppearance.Dark,
- licenseType: ThemeLicenseType.MIT,
- licenseUrl: "https://github.com/gessig/base16-sandcastle-scheme",
- licenseFile: `${__dirname}/LICENSE`,
- inputColor: {
+ license_type: ThemeLicenseType.MIT,
+ license_url: "https://github.com/gessig/base16-sandcastle-scheme",
+ license_file: `${__dirname}/LICENSE`,
+ input_color: {
neutral: chroma.scale([
"#282c34",
"#2c323b",
@@ -24,14 +24,14 @@ export const theme: ThemeConfig = {
"#d5c4a1",
"#fdf4c1",
]),
- red: colorRamp(chroma("#B4637A")),
- orange: colorRamp(chroma("#a07e3b")),
- yellow: colorRamp(chroma("#a07e3b")),
- green: colorRamp(chroma("#83a598")),
- cyan: colorRamp(chroma("#83a598")),
- blue: colorRamp(chroma("#528b8b")),
- violet: colorRamp(chroma("#d75f5f")),
- magenta: colorRamp(chroma("#a87322")),
+ red: color_ramp(chroma("#B4637A")),
+ orange: color_ramp(chroma("#a07e3b")),
+ yellow: color_ramp(chroma("#a07e3b")),
+ green: color_ramp(chroma("#83a598")),
+ cyan: color_ramp(chroma("#83a598")),
+ blue: color_ramp(chroma("#528b8b")),
+ violet: color_ramp(chroma("#d75f5f")),
+ magenta: color_ramp(chroma("#a87322")),
},
override: { syntax: {} },
}
@@ -18,4 +18,4 @@ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
-SOFTWARE.
+SOFTWARE.
@@ -1,6 +1,6 @@
import {
chroma,
- colorRamp,
+ color_ramp,
ThemeAppearance,
ThemeLicenseType,
ThemeConfig,
@@ -19,24 +19,24 @@ const ramps = {
"#fdf6e3",
])
.domain([0, 0.2, 0.38, 0.45, 0.65, 0.7, 0.85, 1]),
- red: colorRamp(chroma("#dc322f")),
- orange: colorRamp(chroma("#cb4b16")),
- yellow: colorRamp(chroma("#b58900")),
- green: colorRamp(chroma("#859900")),
- cyan: colorRamp(chroma("#2aa198")),
- blue: colorRamp(chroma("#268bd2")),
- violet: colorRamp(chroma("#6c71c4")),
- magenta: colorRamp(chroma("#d33682")),
+ red: color_ramp(chroma("#dc322f")),
+ orange: color_ramp(chroma("#cb4b16")),
+ yellow: color_ramp(chroma("#b58900")),
+ green: color_ramp(chroma("#859900")),
+ cyan: color_ramp(chroma("#2aa198")),
+ blue: color_ramp(chroma("#268bd2")),
+ violet: color_ramp(chroma("#6c71c4")),
+ magenta: color_ramp(chroma("#d33682")),
}
export const dark: ThemeConfig = {
name: "Solarized Dark",
author: "Ethan Schoonover",
appearance: ThemeAppearance.Dark,
- licenseType: ThemeLicenseType.MIT,
- licenseUrl: "https://github.com/altercation/solarized",
- licenseFile: `${__dirname}/LICENSE`,
- inputColor: ramps,
+ license_type: ThemeLicenseType.MIT,
+ license_url: "https://github.com/altercation/solarized",
+ license_file: `${__dirname}/LICENSE`,
+ input_color: ramps,
override: { syntax: {} },
}
@@ -44,9 +44,9 @@ export const light: ThemeConfig = {
name: "Solarized Light",
author: "Ethan Schoonover",
appearance: ThemeAppearance.Light,
- licenseType: ThemeLicenseType.MIT,
- licenseUrl: "https://github.com/altercation/solarized",
- licenseFile: `${__dirname}/LICENSE`,
- inputColor: ramps,
+ license_type: ThemeLicenseType.MIT,
+ license_url: "https://github.com/altercation/solarized",
+ license_file: `${__dirname}/LICENSE`,
+ input_color: ramps,
override: { syntax: {} },
}
@@ -18,4 +18,4 @@ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
-SOFTWARE.
+SOFTWARE.
@@ -1,6 +1,6 @@
import {
chroma,
- colorRamp,
+ color_ramp,
ThemeAppearance,
ThemeLicenseType,
ThemeConfig,
@@ -10,10 +10,10 @@ export const theme: ThemeConfig = {
name: "Summercamp",
author: "zoefiri",
appearance: ThemeAppearance.Dark,
- licenseType: ThemeLicenseType.MIT,
- licenseUrl: "https://github.com/zoefiri/base16-sc",
- licenseFile: `${__dirname}/LICENSE`,
- inputColor: {
+ license_type: ThemeLicenseType.MIT,
+ license_url: "https://github.com/zoefiri/base16-sc",
+ license_file: `${__dirname}/LICENSE`,
+ input_color: {
neutral: chroma
.scale([
"#1c1810",
@@ -26,14 +26,14 @@ export const theme: ThemeConfig = {
"#f8f5de",
])
.domain([0, 0.2, 0.38, 0.4, 0.65, 0.7, 0.85, 1]),
- red: colorRamp(chroma("#e35142")),
- orange: colorRamp(chroma("#fba11b")),
- yellow: colorRamp(chroma("#f2ff27")),
- green: colorRamp(chroma("#5ceb5a")),
- cyan: colorRamp(chroma("#5aebbc")),
- blue: colorRamp(chroma("#489bf0")),
- violet: colorRamp(chroma("#FF8080")),
- magenta: colorRamp(chroma("#F69BE7")),
+ red: color_ramp(chroma("#e35142")),
+ orange: color_ramp(chroma("#fba11b")),
+ yellow: color_ramp(chroma("#f2ff27")),
+ green: color_ramp(chroma("#5ceb5a")),
+ cyan: color_ramp(chroma("#5aebbc")),
+ blue: color_ramp(chroma("#489bf0")),
+ violet: color_ramp(chroma("#FF8080")),
+ magenta: color_ramp(chroma("#F69BE7")),
},
override: { syntax: {} },
}
@@ -3,8 +3,8 @@ export function slugify(t: string): string {
.toString()
.toLowerCase()
.replace(/\s+/g, "-")
- .replace(/[^\w\-]+/g, "")
- .replace(/\-\-+/g, "-")
+ .replace(/[^\w-]+/g, "")
+ .replace(/--+/g, "-")
.replace(/^-+/, "")
.replace(/-+$/, "")
}
@@ -1,35 +0,0 @@
-import { snakeCase } from "case-anything"
-
-// https://stackoverflow.com/questions/60269936/typescript-convert-generic-object-from-snake-to-camel-case
-
-// Typescript magic to convert any string from camelCase to snake_case at compile time
-type SnakeCase<S> = S extends string
- ? S extends `${infer T}${infer U}`
- ? `${T extends Capitalize<T> ? "_" : ""}${Lowercase<T>}${SnakeCase<U>}`
- : S
- : S
-
-type SnakeCased<Type> = {
- [Property in keyof Type as SnakeCase<Property>]: SnakeCased<Type[Property]>
-}
-
-export default function snakeCaseTree<T>(object: T): SnakeCased<T> {
- const snakeObject: any = {}
- for (const key in object) {
- snakeObject[snakeCase(key, { keepSpecialCharacters: true })] =
- snakeCaseValue(object[key])
- }
- return snakeObject
-}
-
-function snakeCaseValue(value: any): any {
- if (typeof value === "object") {
- if (Array.isArray(value)) {
- return value.map(snakeCaseValue)
- } else {
- return snakeCaseTree(value)
- }
- } else {
- return value
- }
-}
@@ -21,6 +21,7 @@
"experimentalDecorators": true,
"strictPropertyInitialization": false,
"skipLibCheck": true,
+ "useUnknownInCatchVariables": false,
"baseUrl": ".",
"paths": {
"@/*": ["./*"],
@@ -28,6 +29,7 @@
"@component/*": ["./src/component/*"],
"@styleTree/*": ["./src/styleTree/*"],
"@theme/*": ["./src/theme/*"],
+ "@types/*": ["./src/util/*"],
"@themes/*": ["./src/themes/*"],
"@util/*": ["./src/util/*"]
}