diff --git a/Cargo.lock b/Cargo.lock index 693d27666967891fba73dff467a5fbb2760af4ed..8bd63c644cc90fb3293fa4645d3a3bb42b6c42ad 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1188,7 +1188,7 @@ dependencies = [ [[package]] name = "collab" -version = "0.6.1" +version = "0.7.1" dependencies = [ "anyhow", "async-tungstenite", @@ -1259,6 +1259,7 @@ dependencies = [ "collections", "context_menu", "editor", + "feedback", "futures 0.3.25", "fuzzy", "gpui", @@ -3018,6 +3019,17 @@ version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "adab1eaa3408fb7f0c777a73e7465fd5656136fc93b670eb6df3c88c2c1344e3" +[[package]] +name = "install_cli" +version = "0.1.0" +dependencies = [ + "anyhow", + "gpui", + "log", + "smol", + "util", +] + [[package]] name = "instant" version = "0.1.12" @@ -3157,6 +3169,7 @@ dependencies = [ name = "journal" version = "0.1.0" dependencies = [ + "anyhow", "chrono", "dirs 4.0.0", "editor", @@ -3285,6 +3298,22 @@ dependencies = [ "util", ] +[[package]] +name = "language_selector" +version = "0.1.0" +dependencies = [ + "anyhow", + "editor", + "fuzzy", + "gpui", + "language", + "picker", + "project", + "settings", + "theme", + "workspace", +] + [[package]] name = "lazy_static" version = "1.4.0" @@ -8013,6 +8042,26 @@ version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9193164d4de03a926d909d3bc7c30543cecb35400c02114792c2cae20d5e2dbb" +[[package]] +name = "welcome" +version = "0.1.0" +dependencies = [ + "anyhow", + "db", + "editor", + "fuzzy", + "gpui", + "install_cli", + "log", + "picker", + "project", + "settings", + "theme", + "theme_selector", + "util", + "workspace", +] + [[package]] name = "wepoll-ffi" version = "0.1.2" @@ -8288,6 +8337,7 @@ dependencies = [ "futures 0.3.25", "gpui", "indoc", + "install_cli", "language", "lazy_static", "log", @@ -8359,7 +8409,7 @@ checksum = "09041cd90cf85f7f8b2df60c646f853b7f535ce68f85244eb6731cf89fa498ec" [[package]] name = "zed" -version = "0.76.0" +version = "0.77.0" dependencies = [ "activity_indicator", "anyhow", @@ -8381,6 +8431,7 @@ dependencies = [ "command_palette", "context_menu", "ctor", + "db", "diagnostics", "easy-parallel", "editor", @@ -8396,9 +8447,11 @@ dependencies = [ "ignore", "image", "indexmap", + "install_cli", "isahc", "journal", "language", + "language_selector", "lazy_static", "libc", "log", @@ -8460,6 +8513,7 @@ dependencies = [ "util", "uuid 1.2.2", "vim", + "welcome", "workspace", ] diff --git a/Cargo.toml b/Cargo.toml index c74a76cccefe6c6de610b30c264a81e74b2654df..ab8bcd6de83d8f96853e4f4599bd3c51eced63d3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -26,8 +26,10 @@ members = [ "crates/go_to_line", "crates/gpui", "crates/gpui_macros", + "crates/install_cli", "crates/journal", "crates/language", + "crates/language_selector", "crates/live_kit_client", "crates/live_kit_server", "crates/lsp", @@ -58,6 +60,7 @@ members = [ "crates/util", "crates/vim", "crates/workspace", + "crates/welcome", "crates/zed", ] default-members = ["crates/zed"] diff --git a/assets/icons/logo_96.svg b/assets/icons/logo_96.svg new file mode 100644 index 0000000000000000000000000000000000000000..dc98bb8bc249bfb1decb1771b33470b324dde96f --- /dev/null +++ b/assets/icons/logo_96.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/icons/speech_bubble_12.svg b/assets/icons/speech_bubble_12.svg new file mode 100644 index 0000000000000000000000000000000000000000..f5f330056a34f1261d31416b688bcc86dcdf8bf1 --- /dev/null +++ b/assets/icons/speech_bubble_12.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/keymaps/atom.json b/assets/keymaps/atom.json new file mode 100644 index 0000000000000000000000000000000000000000..766c46c133df9b617061694b9eb23ad8fecaceb8 --- /dev/null +++ b/assets/keymaps/atom.json @@ -0,0 +1,68 @@ +[ + { + "bindings": { + "cmd-k cmd-p": "workspace::ActivatePreviousPane", + "cmd-k cmd-n": "workspace::ActivateNextPane" + } + }, + { + "context": "Editor", + "bindings": { + "cmd-b": "editor::GoToDefinition", + "cmd-<": "editor::ScrollCursorCenter", + "cmd-g": [ + "editor::SelectNext", + { + "replace_newest": true + } + ], + "ctrl-shift-down": "editor::AddSelectionBelow", + "ctrl-shift-up": "editor::AddSelectionAbove", + "cmd-shift-backspace": "editor::DeleteToBeginningOfLine" + } + }, + { + "context": "Editor && mode == full", + "bindings": { + "cmd-r": "outline::Toggle" + } + }, + { + "context": "BufferSearchBar", + "bindings": { + "cmd-f3": "search::SelectNextMatch", + "cmd-shift-f3": "search::SelectPrevMatch" + } + }, + { + "context": "Workspace", + "bindings": { + "cmd-\\": "workspace::ToggleLeftSidebar", + "cmd-k cmd-b": "workspace::ToggleLeftSidebar", + "cmd-t": "file_finder::Toggle", + "cmd-shift-r": "project_symbols::Toggle" + } + }, + { + "context": "Pane", + "bindings": { + "alt-cmd-/": "search::ToggleRegex", + "ctrl-0": "project_panel::ToggleFocus" + } + }, + { + "context": "ProjectPanel", + "bindings": { + "ctrl-[": "project_panel::CollapseSelectedEntry", + "ctrl-b": "project_panel::CollapseSelectedEntry", + "h": "project_panel::CollapseSelectedEntry", + "ctrl-]": "project_panel::ExpandSelectedEntry", + "ctrl-f": "project_panel::ExpandSelectedEntry", + "ctrl-shift-c": "project_panel::CopyPath" + } + }, + { + "context": "Dock", + "bindings": {} + } +] \ No newline at end of file diff --git a/assets/keymaps/default.json b/assets/keymaps/default.json index cce65eda8abcadcb632d1b496c10b2dc283dc548..57f5075aca9a6678bb6a7231d8fdd5a3036c5aef 100644 --- a/assets/keymaps/default.json +++ b/assets/keymaps/default.json @@ -353,7 +353,8 @@ "cmd-shift-p": "command_palette::Toggle", "cmd-shift-m": "diagnostics::Deploy", "cmd-shift-e": "project_panel::ToggleFocus", - "cmd-alt-s": "workspace::SaveAll" + "cmd-alt-s": "workspace::SaveAll", + "cmd-k m": "language_selector::Toggle" } }, // Bindings from Sublime Text @@ -537,4 +538,4 @@ ] } } -] \ No newline at end of file +] diff --git a/assets/keymaps/jetbrains.json b/assets/keymaps/jetbrains.json new file mode 100644 index 0000000000000000000000000000000000000000..2e6e5e77e6abebd3d6c464895af5cdcaa9dc2075 --- /dev/null +++ b/assets/keymaps/jetbrains.json @@ -0,0 +1,78 @@ +[ + { + "bindings": { + "cmd-shift-[": "pane::ActivatePrevItem", + "cmd-shift-]": "pane::ActivateNextItem" + } + }, + { + "context": "Editor", + "bindings": { + "ctrl->": "zed::IncreaseBufferFontSize", + "ctrl-<": "zed::DecreaseBufferFontSize", + "cmd-d": "editor::DuplicateLine", + "cmd-pagedown": "editor::MovePageDown", + "cmd-pageup": "editor::MovePageUp", + "ctrl-alt-shift-b": "editor::SelectToPreviousWordStart", + "shift-enter": "editor::NewlineBelow", + "cmd--": "editor::Fold", + "cmd-=": "editor::UnfoldLines", + "alt-shift-g": "editor::SplitSelectionIntoLines", + "ctrl-g": [ + "editor::SelectNext", + { + "replace_newest": false + } + ], + "cmd-/": [ + "editor::ToggleComments", + { + "advance_downwards": true + } + ], + "shift-alt-up": "editor::MoveLineUp", + "shift-alt-down": "editor::MoveLineDown", + "cmd-[": "pane::GoBack", + "cmd-]": "pane::GoForward", + "alt-f7": "editor::FindAllReferences", + "cmd-alt-f7": "editor::FindAllReferences", + "cmd-b": "editor::GoToDefinition", + "cmd-alt-b": "editor::GoToDefinition", + "cmd-shift-b": "editor::GoToTypeDefinition", + "alt-enter": "editor::ToggleCodeActions", + "f2": "editor::GoToDiagnostic", + "cmd-f2": "editor::GoToPrevDiagnostic", + "ctrl-alt-shift-down": "editor::GoToHunk", + "ctrl-alt-shift-up": "editor::GoToPrevHunk", + "cmd-home": "editor::MoveToBeginning", + "cmd-end": "editor::MoveToEnd", + "cmd-shift-home": "editor::SelectToBeginning", + "cmd-shift-end": "editor::SelectToEnd" + } + }, + { + "context": "Editor && mode == full", + "bindings": { + "cmd-f12": "outline::Toggle", + "cmd-7": "outline::Toggle", + "cmd-shift-o": "file_finder::Toggle", + "cmd-l": "go_to_line::Toggle" + } + }, + { + "context": "Workspace", + "bindings": { + "cmd-shift-a": "command_palette::Toggle", + "cmd-alt-o": "project_symbols::Toggle", + "cmd-1": "workspace::ToggleLeftSidebar", + "cmd-6": "diagnostics::Deploy", + "alt-f12": "dock::FocusDock" + } + }, + { + "context": "Dock", + "bindings": { + "alt-f12": "dock::HideDock" + } + } +] \ No newline at end of file diff --git a/assets/keymaps/sublime_text.json b/assets/keymaps/sublime_text.json new file mode 100644 index 0000000000000000000000000000000000000000..1d3dd887d7b4a57add7d5be95d34700e1be1143a --- /dev/null +++ b/assets/keymaps/sublime_text.json @@ -0,0 +1,60 @@ +[ + { + "bindings": { + "cmd-shift-[": "pane::ActivatePrevItem", + "cmd-shift-]": "pane::ActivateNextItem", + "ctrl-pagedown": "pane::ActivatePrevItem", + "ctrl-pageup": "pane::ActivateNextItem", + "ctrl-shift-tab": "pane::ActivateNextItem", + "ctrl-tab": "pane::ActivatePrevItem", + "cmd-+": "zed::IncreaseBufferFontSize" + } + }, + { + "context": "Editor", + "bindings": { + "ctrl-shift-up": "editor::AddSelectionAbove", + "ctrl-shift-down": "editor::AddSelectionBelow", + "cmd-shift-space": "editor::SelectAll", + "ctrl-shift-m": "editor::SelectLargerSyntaxNode", + "cmd-shift-a": "editor::SelectLargerSyntaxNode", + "shift-f12": "editor::FindAllReferences", + "alt-cmd-down": "editor::GoToDefinition", + "alt-shift-cmd-down": "editor::FindAllReferences", + "ctrl-.": "editor::GoToHunk", + "ctrl-,": "editor::GoToPrevHunk", + "ctrl-backspace": "editor::DeleteToPreviousWordStart", + "ctrl-delete": "editor::DeleteToNextWordEnd" + } + }, + { + "context": "Editor && mode == full", + "bindings": { + "cmd-r": "outline::Toggle" + } + }, + { + "context": "Pane", + "bindings": { + "f4": "search::SelectNextMatch", + "shift-f4": "search::SelectPrevMatch" + } + }, + { + "context": "Workspace", + "bindings": { + "ctrl-`": "dock::FocusDock", + "cmd-k cmd-b": "workspace::ToggleLeftSidebar", + "cmd-t": "file_finder::Toggle", + "shift-cmd-r": "project_symbols::Toggle", + // Currently busted: https://github.com/zed-industries/feedback/issues/898 + "ctrl-0": "project_panel::ToggleFocus" + } + }, + { + "context": "Dock", + "bindings": { + "ctrl-`": "dock::HideDock" + } + } +] \ No newline at end of file diff --git a/assets/settings/default.json b/assets/settings/default.json index 2a5e05b40138053862461f721231e042b2a1cb38..90c47478f3f7c3a97b50cea061907c122bf513c9 100644 --- a/assets/settings/default.json +++ b/assets/settings/default.json @@ -50,7 +50,7 @@ // "default_dock_anchor": "right" // 3. Position the dock full screen over the entire workspace" // "default_dock_anchor": "expanded" - "default_dock_anchor": "right", + "default_dock_anchor": "bottom", // Whether or not to remove any trailing whitespace from lines of a buffer // before saving it. "remove_trailing_whitespace_on_save": true, diff --git a/crates/call/src/room.rs b/crates/call/src/room.rs index 06680c1dcb86335c2e120df3043fd413d3f26af1..901ae08fc048cf43c98131566855373d38686225 100644 --- a/crates/call/src/room.rs +++ b/crates/call/src/room.rs @@ -20,7 +20,7 @@ use project::Project; use std::{mem, sync::Arc, time::Duration}; use util::{post_inc, ResultExt, TryFutureExt}; -pub const RECONNECT_TIMEOUT: Duration = client::RECEIVE_TIMEOUT; +pub const RECONNECT_TIMEOUT: Duration = Duration::from_secs(30); #[derive(Clone, Debug, PartialEq, Eq)] pub enum Event { diff --git a/crates/client/src/telemetry.rs b/crates/client/src/telemetry.rs index 748eb48f7e27368588534266fe34e9183cb01b78..9d486619d255dd7ab214a145f7f31162dd4a9c36 100644 --- a/crates/client/src/telemetry.rs +++ b/crates/client/src/telemetry.rs @@ -224,7 +224,7 @@ impl Telemetry { .header("Content-Type", "application/json") .body(json_bytes.into())?; this.http_client.send(request).await?; - Ok(()) + anyhow::Ok(()) } .log_err(), ) @@ -320,7 +320,7 @@ impl Telemetry { .header("Content-Type", "application/json") .body(json_bytes.into())?; this.http_client.send(request).await?; - Ok(()) + anyhow::Ok(()) } .log_err(), ) diff --git a/crates/collab/.env.toml b/crates/collab/.env.toml index b4a6694e5e6578e771406d6947c2c4ad8d2efb0a..01866012ea8a14c94835d0662318d3f8c4df67f5 100644 --- a/crates/collab/.env.toml +++ b/crates/collab/.env.toml @@ -1,4 +1,5 @@ DATABASE_URL = "postgres://postgres@localhost/zed" +DATABASE_MAX_CONNECTIONS = 5 HTTP_PORT = 8080 API_TOKEN = "secret" INVITE_LINK_PREFIX = "http://localhost:3000/invites/" diff --git a/crates/collab/Cargo.toml b/crates/collab/Cargo.toml index 86fe9174bf0bfec0aeb5b90daab8d7e09c898bd7..0f06a14b68c1b0e1998554d249fa03c509f2ef2f 100644 --- a/crates/collab/Cargo.toml +++ b/crates/collab/Cargo.toml @@ -3,7 +3,7 @@ authors = ["Nathan Sobo "] default-run = "collab" edition = "2021" name = "collab" -version = "0.6.1" +version = "0.7.1" publish = false [[bin]] diff --git a/crates/collab/k8s/environments/preview.sh b/crates/collab/k8s/environments/preview.sh index 4d9dd849e96f9bb5fc672beb0b7547c6246358de..132a1ef53c2b84bb97659b15800888b195b9de65 100644 --- a/crates/collab/k8s/environments/preview.sh +++ b/crates/collab/k8s/environments/preview.sh @@ -1,3 +1,4 @@ ZED_ENVIRONMENT=preview RUST_LOG=info INVITE_LINK_PREFIX=https://zed.dev/invites/ +DATABASE_MAX_CONNECTIONS=10 diff --git a/crates/collab/k8s/environments/production.sh b/crates/collab/k8s/environments/production.sh index 83af6630c2e0a52b915c2a1ddb07778e60e16a4e..cb1d4b4de712864003c711dd8b46123b31fa5d5f 100644 --- a/crates/collab/k8s/environments/production.sh +++ b/crates/collab/k8s/environments/production.sh @@ -1,3 +1,4 @@ ZED_ENVIRONMENT=production RUST_LOG=info INVITE_LINK_PREFIX=https://zed.dev/invites/ +DATABASE_MAX_CONNECTIONS=85 diff --git a/crates/collab/k8s/environments/staging.sh b/crates/collab/k8s/environments/staging.sh index 82d799e2bc200c44a27a5ce7712fc1106b15970f..b9689ccb19390b412f4a7df9e8fee2e744b0479c 100644 --- a/crates/collab/k8s/environments/staging.sh +++ b/crates/collab/k8s/environments/staging.sh @@ -1,3 +1,4 @@ ZED_ENVIRONMENT=staging RUST_LOG=info INVITE_LINK_PREFIX=https://staging.zed.dev/invites/ +DATABASE_MAX_CONNECTIONS=5 diff --git a/crates/collab/k8s/manifest.template.yml b/crates/collab/k8s/manifest.template.yml index 339d02892ef2cf24f40815c3fd32dc0fb72c317a..0662a287d4b4541f4285e9b4ba7c3c0a2bdafcbf 100644 --- a/crates/collab/k8s/manifest.template.yml +++ b/crates/collab/k8s/manifest.template.yml @@ -59,6 +59,13 @@ spec: ports: - containerPort: 8080 protocol: TCP + livenessProbe: + httpGet: + path: /healthz + port: 8080 + initialDelaySeconds: 5 + periodSeconds: 5 + timeoutSeconds: 5 readinessProbe: httpGet: path: / @@ -73,6 +80,8 @@ spec: secretKeyRef: name: database key: url + - name: DATABASE_MAX_CONNECTIONS + value: "${DATABASE_MAX_CONNECTIONS}" - name: API_TOKEN valueFrom: secretKeyRef: diff --git a/crates/collab/src/db.rs b/crates/collab/src/db.rs index c4ff2e39188a1133cc086b00ab5194d242ef0cc8..1d0fc377ab67e96b43265a21f7886ac1d7b6f4b7 100644 --- a/crates/collab/src/db.rs +++ b/crates/collab/src/db.rs @@ -1757,17 +1757,14 @@ impl Database { .add(follower::Column::ProjectId.eq(project_id)) .add( follower::Column::LeaderConnectionServerId - .eq(leader_connection.owner_id) - .and(follower::Column::LeaderConnectionId.eq(leader_connection.id)), + .eq(leader_connection.owner_id), ) + .add(follower::Column::LeaderConnectionId.eq(leader_connection.id)) .add( follower::Column::FollowerConnectionServerId - .eq(follower_connection.owner_id) - .and( - follower::Column::FollowerConnectionId - .eq(follower_connection.id), - ), - ), + .eq(follower_connection.owner_id), + ) + .add(follower::Column::FollowerConnectionId.eq(follower_connection.id)), ) .exec(&*tx) .await?; @@ -2560,7 +2557,7 @@ impl Database { &self, project_id: ProjectId, connection: ConnectionId, - ) -> Result> { + ) -> Result> { let room_id = self.room_id_for_project(project_id).await?; self.room_transaction(room_id, |tx| async move { let result = project_collaborator::Entity::delete_many() @@ -2592,13 +2589,39 @@ impl Database { .map(|collaborator| collaborator.connection()) .collect(); + follower::Entity::delete_many() + .filter( + Condition::any() + .add( + Condition::all() + .add(follower::Column::ProjectId.eq(project_id)) + .add( + follower::Column::LeaderConnectionServerId + .eq(connection.owner_id), + ) + .add(follower::Column::LeaderConnectionId.eq(connection.id)), + ) + .add( + Condition::all() + .add(follower::Column::ProjectId.eq(project_id)) + .add( + follower::Column::FollowerConnectionServerId + .eq(connection.owner_id), + ) + .add(follower::Column::FollowerConnectionId.eq(connection.id)), + ), + ) + .exec(&*tx) + .await?; + + let room = self.get_room(project.room_id, &tx).await?; let left_project = LeftProject { id: project_id, host_user_id: project.host_user_id, host_connection_id: project.host_connection()?, connection_ids, }; - Ok(left_project) + Ok((room, left_project)) }) .await } diff --git a/crates/collab/src/lib.rs b/crates/collab/src/lib.rs index 1a83193bdfc3455fe23791b19e1b18b3e5673aaa..8c99a5ea0f5623ce5e86938587427de0d616f77c 100644 --- a/crates/collab/src/lib.rs +++ b/crates/collab/src/lib.rs @@ -91,6 +91,7 @@ impl std::error::Error for Error {} pub struct Config { pub http_port: u16, pub database_url: String, + pub database_max_connections: u32, pub api_token: String, pub invite_link_prefix: String, pub live_kit_server: Option, @@ -116,7 +117,7 @@ pub struct AppState { impl AppState { pub async fn new(config: Config) -> Result> { let mut db_options = db::ConnectOptions::new(config.database_url.clone()); - db_options.max_connections(5); + db_options.max_connections(config.database_max_connections); let db = Database::new(db_options).await?; let live_kit_client = if let Some(((server, key), secret)) = config .live_kit_server diff --git a/crates/collab/src/main.rs b/crates/collab/src/main.rs index 0f783c13e58d3b64c0e034c18ea6173d63be4036..30ed35bc354ec74ea4e323f6ad85cc8d353c922d 100644 --- a/crates/collab/src/main.rs +++ b/crates/collab/src/main.rs @@ -1,11 +1,12 @@ use anyhow::anyhow; -use axum::{routing::get, Router}; +use axum::{routing::get, Extension, Router}; use collab::{db, env, executor::Executor, AppState, Config, MigrateConfig, Result}; use db::Database; use std::{ env::args, net::{SocketAddr, TcpListener}, path::Path, + sync::Arc, }; use tokio::signal::unix::SignalKind; use tracing_log::LogTracer; @@ -66,7 +67,12 @@ async fn main() -> Result<()> { let app = collab::api::routes(rpc_server.clone(), state.clone()) .merge(collab::rpc::routes(rpc_server.clone())) - .merge(Router::new().route("/", get(handle_root))); + .merge( + Router::new() + .route("/", get(handle_root)) + .route("/healthz", get(handle_liveness_probe)) + .layer(Extension(state.clone())), + ); axum::Server::from_tcp(listener)? .serve(app.into_make_service_with_connect_info::()) @@ -95,6 +101,11 @@ async fn handle_root() -> String { format!("collab v{VERSION}") } +async fn handle_liveness_probe(Extension(state): Extension>) -> Result { + state.db.get_all_users(0, 1).await?; + Ok("ok".to_string()) +} + pub fn init_tracing(config: &Config) -> Option<()> { use std::str::FromStr; use tracing_subscriber::layer::SubscriberExt; diff --git a/crates/collab/src/rpc.rs b/crates/collab/src/rpc.rs index 01d24024eb7516e87e4c0477d069cae8feb42da6..1deaefec1a446857748e311a34ffdd741385c944 100644 --- a/crates/collab/src/rpc.rs +++ b/crates/collab/src/rpc.rs @@ -53,11 +53,11 @@ use std::{ }, time::Duration, }; -use tokio::sync::watch; +use tokio::sync::{watch, Semaphore}; use tower::ServiceBuilder; use tracing::{info_span, instrument, Instrument}; -pub const RECONNECT_TIMEOUT: Duration = Duration::from_secs(5); +pub const RECONNECT_TIMEOUT: Duration = Duration::from_secs(30); pub const CLEANUP_TIMEOUT: Duration = Duration::from_secs(10); lazy_static! { @@ -542,8 +542,13 @@ impl Server { // This arrangement ensures we will attempt to process earlier messages first, but fall // back to processing messages arrived later in the spirit of making progress. let mut foreground_message_handlers = FuturesUnordered::new(); + let concurrent_handlers = Arc::new(Semaphore::new(256)); loop { - let next_message = incoming_rx.next().fuse(); + let next_message = async { + let permit = concurrent_handlers.clone().acquire_owned().await.unwrap(); + let message = incoming_rx.next().await; + (permit, message) + }.fuse(); futures::pin_mut!(next_message); futures::select_biased! { _ = teardown.changed().fuse() => return Ok(()), @@ -554,7 +559,8 @@ impl Server { break; } _ = foreground_message_handlers.next() => {} - message = next_message => { + next_message = next_message => { + let (permit, message) = next_message; if let Some(message) = message { let type_name = message.payload_type_name(); let span = tracing::info_span!("receive message", %user_id, %login, %connection_id, %address, type_name); @@ -564,7 +570,10 @@ impl Server { let handle_message = (handler)(message, session.clone()); drop(span_enter); - let handle_message = handle_message.instrument(span); + let handle_message = async move { + handle_message.await; + drop(permit); + }.instrument(span); if is_background { executor.spawn_detached(handle_message); } else { @@ -1408,7 +1417,7 @@ async fn leave_project(request: proto::LeaveProject, session: Session) -> Result let sender_id = session.connection_id; let project_id = ProjectId::from_proto(request.project_id); - let project = session + let (room, project) = &*session .db() .await .leave_project(project_id, sender_id) @@ -1419,7 +1428,9 @@ async fn leave_project(request: proto::LeaveProject, session: Session) -> Result host_connection_id = %project.host_connection_id, "leave project" ); + project_left(&project, &session); + room_updated(&room, &session.peer); Ok(()) } diff --git a/crates/collab/src/tests.rs b/crates/collab/src/tests.rs index e9ffecf246d295934e15a2a3375af60f78a18c3e..80df0ed6dfb59136ad6183b0a0f42e34a5b63264 100644 --- a/crates/collab/src/tests.rs +++ b/crates/collab/src/tests.rs @@ -197,7 +197,8 @@ impl TestServer { fs: fs.clone(), build_window_options: |_, _, _| Default::default(), initialize_workspace: |_, _, _| unimplemented!(), - dock_default_item_factory: |_, _| unimplemented!(), + dock_default_item_factory: |_, _| None, + background_actions: || &[], }); Project::init(&client); @@ -434,15 +435,7 @@ impl TestClient { cx: &mut TestAppContext, ) -> ViewHandle { let (_, root_view) = cx.add_window(|_| EmptyView); - cx.add_view(&root_view, |cx| { - Workspace::new( - Default::default(), - 0, - project.clone(), - |_, _| unimplemented!(), - cx, - ) - }) + cx.add_view(&root_view, |cx| Workspace::test_new(project.clone(), cx)) } fn create_new_root_dir(&mut self) -> PathBuf { diff --git a/crates/collab/src/tests/integration_tests.rs b/crates/collab/src/tests/integration_tests.rs index 1ab78ac310b0d23b10f08af62fbe8490f626161b..44a2839f27c750ac795ca6a0aaa936aedbe9ebcf 100644 --- a/crates/collab/src/tests/integration_tests.rs +++ b/crates/collab/src/tests/integration_tests.rs @@ -829,7 +829,7 @@ async fn test_server_restarts( // Users A and B reconnect to the call. User C has troubles reconnecting, so it leaves the room. client_c.override_establish_connection(|_, cx| cx.spawn(|_| future::pending())); - deterministic.advance_clock(RECEIVE_TIMEOUT); + deterministic.advance_clock(RECONNECT_TIMEOUT); assert_eq!( room_participants(&room_a, cx_a), RoomParticipants { @@ -1001,7 +1001,7 @@ async fn test_server_restarts( client_a.override_establish_connection(|_, cx| cx.spawn(|_| future::pending())); client_b.override_establish_connection(|_, cx| cx.spawn(|_| future::pending())); client_c.override_establish_connection(|_, cx| cx.spawn(|_| future::pending())); - deterministic.advance_clock(RECEIVE_TIMEOUT); + deterministic.advance_clock(RECONNECT_TIMEOUT); assert_eq!( room_participants(&room_a, cx_a), RoomParticipants { @@ -1449,15 +1449,7 @@ async fn test_host_disconnect( deterministic.run_until_parked(); assert!(worktree_a.read_with(cx_a, |tree, _| tree.as_local().unwrap().is_shared())); - let (_, workspace_b) = cx_b.add_window(|cx| { - Workspace::new( - Default::default(), - 0, - project_b.clone(), - |_, _| unimplemented!(), - cx, - ) - }); + let (_, workspace_b) = cx_b.add_window(|cx| Workspace::test_new(project_b.clone(), cx)); let editor_b = workspace_b .update(cx_b, |workspace, cx| { workspace.open_path((worktree_id, "b.txt"), None, true, cx) @@ -4706,15 +4698,7 @@ async fn test_collaborating_with_code_actions( // Join the project as client B. let project_b = client_b.build_remote_project(project_id, cx_b).await; - let (_window_b, workspace_b) = cx_b.add_window(|cx| { - Workspace::new( - Default::default(), - 0, - project_b.clone(), - |_, _| unimplemented!(), - cx, - ) - }); + let (_window_b, workspace_b) = cx_b.add_window(|cx| Workspace::test_new(project_b.clone(), cx)); let editor_b = workspace_b .update(cx_b, |workspace, cx| { workspace.open_path((worktree_id, "main.rs"), None, true, cx) @@ -4937,15 +4921,7 @@ async fn test_collaborating_with_renames( .unwrap(); let project_b = client_b.build_remote_project(project_id, cx_b).await; - let (_window_b, workspace_b) = cx_b.add_window(|cx| { - Workspace::new( - Default::default(), - 0, - project_b.clone(), - |_, _| unimplemented!(), - cx, - ) - }); + let (_window_b, workspace_b) = cx_b.add_window(|cx| Workspace::test_new(project_b.clone(), cx)); let editor_b = workspace_b .update(cx_b, |workspace, cx| { workspace.open_path((worktree_id, "one.rs"), None, true, cx) @@ -5792,11 +5768,12 @@ async fn test_contact_requests( } #[gpui::test(iterations = 10)] -async fn test_following( +async fn test_basic_following( deterministic: Arc, cx_a: &mut TestAppContext, cx_b: &mut TestAppContext, cx_c: &mut TestAppContext, + cx_d: &mut TestAppContext, ) { deterministic.forbid_parking(); cx_a.update(editor::init); @@ -5806,11 +5783,14 @@ async fn test_following( let client_a = server.create_client(cx_a, "user_a").await; let client_b = server.create_client(cx_b, "user_b").await; let client_c = server.create_client(cx_c, "user_c").await; + let client_d = server.create_client(cx_d, "user_d").await; server - .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)]) - .await; - server - .make_contacts(&mut [(&client_a, cx_a), (&client_c, cx_c)]) + .create_room(&mut [ + (&client_a, cx_a), + (&client_b, cx_b), + (&client_c, cx_c), + (&client_d, cx_d), + ]) .await; let active_call_a = cx_a.read(ActiveCall::global); let active_call_b = cx_b.read(ActiveCall::global); @@ -5877,6 +5857,7 @@ async fn test_following( let peer_id_a = client_a.peer_id().unwrap(); let peer_id_b = client_b.peer_id().unwrap(); let peer_id_c = client_c.peer_id().unwrap(); + let peer_id_d = client_d.peer_id().unwrap(); // Client A updates their selections in those editors editor_a1.update(cx_a, |editor, cx| { @@ -5896,25 +5877,15 @@ async fn test_following( .await .unwrap(); - // Client A invites client C to the call. - active_call_a - .update(cx_a, |call, cx| { - call.invite(client_c.current_user_id(cx_c).to_proto(), None, cx) - }) - .await - .unwrap(); cx_c.foreground().run_until_parked(); let active_call_c = cx_c.read(ActiveCall::global); - active_call_c - .update(cx_c, |call, cx| call.accept_incoming(cx)) - .await - .unwrap(); let project_c = client_c.build_remote_project(project_id, cx_c).await; let workspace_c = client_c.build_workspace(&project_c, cx_c); active_call_c .update(cx_c, |call, cx| call.set_location(Some(&project_c), cx)) .await .unwrap(); + drop(project_c); // Client C also follows client A. workspace_c @@ -5926,12 +5897,23 @@ async fn test_following( .await .unwrap(); + cx_d.foreground().run_until_parked(); + let active_call_d = cx_d.read(ActiveCall::global); + let project_d = client_d.build_remote_project(project_id, cx_d).await; + let workspace_d = client_d.build_workspace(&project_d, cx_d); + active_call_d + .update(cx_d, |call, cx| call.set_location(Some(&project_d), cx)) + .await + .unwrap(); + drop(project_d); + // All clients see that clients B and C are following client A. cx_c.foreground().run_until_parked(); for (name, active_call, cx) in [ ("A", &active_call_a, &cx_a), ("B", &active_call_b, &cx_b), ("C", &active_call_c, &cx_c), + ("D", &active_call_d, &cx_d), ] { active_call.read_with(*cx, |call, cx| { let room = call.room().unwrap().read(cx); @@ -5954,6 +5936,7 @@ async fn test_following( ("A", &active_call_a, &cx_a), ("B", &active_call_b, &cx_b), ("C", &active_call_c, &cx_c), + ("D", &active_call_d, &cx_d), ] { active_call.read_with(*cx, |call, cx| { let room = call.room().unwrap().read(cx); @@ -5965,6 +5948,90 @@ async fn test_following( }); } + // Client C re-follows client A. + workspace_c.update(cx_c, |workspace, cx| { + workspace.toggle_follow(&ToggleFollow(peer_id_a), cx); + }); + + // All clients see that clients B and C are following client A. + cx_c.foreground().run_until_parked(); + for (name, active_call, cx) in [ + ("A", &active_call_a, &cx_a), + ("B", &active_call_b, &cx_b), + ("C", &active_call_c, &cx_c), + ("D", &active_call_d, &cx_d), + ] { + active_call.read_with(*cx, |call, cx| { + let room = call.room().unwrap().read(cx); + assert_eq!( + room.followers_for(peer_id_a, project_id), + &[peer_id_b, peer_id_c], + "checking followers for A as {name}" + ); + }); + } + + // Client D follows client C. + workspace_d + .update(cx_d, |workspace, cx| { + workspace + .toggle_follow(&ToggleFollow(peer_id_c), cx) + .unwrap() + }) + .await + .unwrap(); + + // All clients see that D is following C + cx_d.foreground().run_until_parked(); + for (name, active_call, cx) in [ + ("A", &active_call_a, &cx_a), + ("B", &active_call_b, &cx_b), + ("C", &active_call_c, &cx_c), + ("D", &active_call_d, &cx_d), + ] { + active_call.read_with(*cx, |call, cx| { + let room = call.room().unwrap().read(cx); + assert_eq!( + room.followers_for(peer_id_c, project_id), + &[peer_id_d], + "checking followers for C as {name}" + ); + }); + } + + // Client C closes the project. + cx_c.drop_last(workspace_c); + + // Clients A and B see that client B is following A, and client C is not present in the followers. + cx_c.foreground().run_until_parked(); + for (name, active_call, cx) in [("A", &active_call_a, &cx_a), ("B", &active_call_b, &cx_b)] { + active_call.read_with(*cx, |call, cx| { + let room = call.room().unwrap().read(cx); + assert_eq!( + room.followers_for(peer_id_a, project_id), + &[peer_id_b], + "checking followers for A as {name}" + ); + }); + } + + // All clients see that no-one is following C + for (name, active_call, cx) in [ + ("A", &active_call_a, &cx_a), + ("B", &active_call_b, &cx_b), + ("C", &active_call_c, &cx_c), + ("D", &active_call_d, &cx_d), + ] { + active_call.read_with(*cx, |call, cx| { + let room = call.room().unwrap().read(cx); + assert_eq!( + room.followers_for(peer_id_c, project_id), + &[], + "checking followers for C as {name}" + ); + }); + } + let editor_b2 = workspace_b.read_with(cx_b, |workspace, cx| { workspace .active_item(cx) diff --git a/crates/collab_ui/Cargo.toml b/crates/collab_ui/Cargo.toml index 899f8cc8b4204dc0c2b6d0c060aafb1a42e7e907..2afeb8ad8ae755b1f8b554d8e4a9bc15732767f3 100644 --- a/crates/collab_ui/Cargo.toml +++ b/crates/collab_ui/Cargo.toml @@ -29,6 +29,7 @@ clock = { path = "../clock" } collections = { path = "../collections" } context_menu = { path = "../context_menu" } editor = { path = "../editor" } +feedback = { path = "../feedback" } fuzzy = { path = "../fuzzy" } gpui = { path = "../gpui" } menu = { path = "../menu" } diff --git a/crates/collab_ui/src/collab_titlebar_item.rs b/crates/collab_ui/src/collab_titlebar_item.rs index f9f5738ad26762dd9e4fc5c08c6cf48439b90e67..28db4f6aa3d058766c7544b34dc5c23f347b1948 100644 --- a/crates/collab_ui/src/collab_titlebar_item.rs +++ b/crates/collab_ui/src/collab_titlebar_item.rs @@ -304,12 +304,22 @@ impl CollabTitlebarItem { label: "Sign out".into(), action: Box::new(SignOut), }, + ContextMenuItem::Item { + label: "Give Feedback".into(), + action: Box::new(feedback::feedback_editor::GiveFeedback), + }, ] } else { - vec![ContextMenuItem::Item { - label: "Sign in".into(), - action: Box::new(Authenticate), - }] + vec![ + ContextMenuItem::Item { + label: "Sign in".into(), + action: Box::new(Authenticate), + }, + ContextMenuItem::Item { + label: "Give Feedback".into(), + action: Box::new(feedback::feedback_editor::GiveFeedback), + }, + ] }; user_menu.show( @@ -572,15 +582,13 @@ impl CollabTitlebarItem { room: &ModelHandle, cx: &mut RenderContext, ) -> Vec { - let project = workspace.read(cx).project().read(cx); - let mut participants = room .read(cx) .remote_participants() .values() .cloned() .collect::>(); - participants.sort_by_key(|p| Some(project.collaborators().get(&p.peer_id)?.replica_id)); + participants.sort_by_cached_key(|p| p.user.github_login.clone()); participants .into_iter() @@ -823,7 +831,7 @@ impl CollabTitlebarItem { avatar_style: AvatarStyle, background_color: Color, ) -> ElementBox { - Image::new(avatar) + Image::from_data(avatar) .with_style(avatar_style.image) .aligned() .contained() diff --git a/crates/collab_ui/src/collab_ui.rs b/crates/collab_ui/src/collab_ui.rs index 6abfec21f74f0918e6338bc77a2e8aa8acfe783b..2dd2b0e6b47e9bbea6e2c916d201b70282517841 100644 --- a/crates/collab_ui/src/collab_ui.rs +++ b/crates/collab_ui/src/collab_ui.rs @@ -86,6 +86,7 @@ fn join_project(action: &JoinProject, app_state: Arc, cx: &mut Mutable 0, project, app_state.dock_default_item_factory, + app_state.background_actions, cx, ); (app_state.initialize_workspace)(&mut workspace, &app_state, cx); diff --git a/crates/collab_ui/src/contact_finder.rs b/crates/collab_ui/src/contact_finder.rs index 98f70e83f0ecf1eb3c312429a8461a35b028199e..83f094ebadf0765b03ade8d7d7e18e651e0b85d6 100644 --- a/crates/collab_ui/src/contact_finder.rs +++ b/crates/collab_ui/src/contact_finder.rs @@ -1,7 +1,7 @@ use client::{ContactRequestStatus, User, UserStore}; use gpui::{ - elements::*, AnyViewHandle, Entity, ModelHandle, MouseState, MutableAppContext, RenderContext, - Task, View, ViewContext, ViewHandle, + elements::*, AnyViewHandle, AppContext, Entity, ModelHandle, MouseState, MutableAppContext, + RenderContext, Task, View, ViewContext, ViewHandle, }; use picker::{Picker, PickerDelegate}; use settings::Settings; @@ -68,7 +68,7 @@ impl PickerDelegate for ContactFinder { this.potential_contacts = potential_contacts.into(); cx.notify(); }); - Ok(()) + anyhow::Ok(()) } .log_err() .await; @@ -128,7 +128,7 @@ impl PickerDelegate for ContactFinder { .style_for(mouse_state, selected); Flex::row() .with_children(user.avatar.clone().map(|avatar| { - Image::new(avatar) + Image::from_data(avatar) .with_style(theme.contact_finder.contact_avatar) .aligned() .left() @@ -178,4 +178,14 @@ impl ContactFinder { selected_index: 0, } } + + pub fn editor_text(&self, cx: &AppContext) -> String { + self.picker.read(cx).query(cx) + } + + pub fn with_editor_text(self, editor_text: String, cx: &mut ViewContext) -> Self { + self.picker + .update(cx, |picker, cx| picker.set_query(editor_text, cx)); + self + } } diff --git a/crates/collab_ui/src/contact_list.rs b/crates/collab_ui/src/contact_list.rs index 3bb036d3366a4de8f4bb8e8253ae4470ce86edbc..38444f6f12b52249f9f87705a671a828e7458c99 100644 --- a/crates/collab_ui/src/contact_list.rs +++ b/crates/collab_ui/src/contact_list.rs @@ -294,6 +294,16 @@ impl ContactList { this } + pub fn editor_text(&self, cx: &AppContext) -> String { + self.filter_editor.read(cx).text(cx) + } + + pub fn with_editor_text(self, editor_text: String, cx: &mut ViewContext) -> Self { + self.filter_editor + .update(cx, |picker, cx| picker.set_text(editor_text, cx)); + self + } + fn remove_contact(&mut self, request: &RemoveContact, cx: &mut ViewContext) { let user_id = request.0; let user_store = self.user_store.clone(); @@ -726,7 +736,7 @@ impl ContactList { ) -> ElementBox { Flex::row() .with_children(user.avatar.clone().map(|avatar| { - Image::new(avatar) + Image::from_data(avatar) .with_style(theme.contact_avatar) .aligned() .left() @@ -1080,7 +1090,7 @@ impl ContactList { }; Stack::new() .with_child( - Image::new(avatar) + Image::from_data(avatar) .with_style(theme.contact_avatar) .aligned() .left() @@ -1173,7 +1183,7 @@ impl ContactList { let mut row = Flex::row() .with_children(user.avatar.clone().map(|avatar| { - Image::new(avatar) + Image::from_data(avatar) .with_style(theme.contact_avatar) .aligned() .left() diff --git a/crates/collab_ui/src/contacts_popover.rs b/crates/collab_ui/src/contacts_popover.rs index 0c67ef4c7c094a17758284bc5b2e8aeac8d30586..017950e6cc777543d7df8fdd98d40f0da8ed1e59 100644 --- a/crates/collab_ui/src/contacts_popover.rs +++ b/crates/collab_ui/src/contacts_popover.rs @@ -43,19 +43,23 @@ impl ContactsPopover { user_store, _subscription: None, }; - this.show_contact_list(cx); + this.show_contact_list(String::new(), cx); this } fn toggle_contact_finder(&mut self, _: &ToggleContactFinder, cx: &mut ViewContext) { match &self.child { - Child::ContactList(_) => self.show_contact_finder(cx), - Child::ContactFinder(_) => self.show_contact_list(cx), + Child::ContactList(list) => self.show_contact_finder(list.read(cx).editor_text(cx), cx), + Child::ContactFinder(finder) => { + self.show_contact_list(finder.read(cx).editor_text(cx), cx) + } } } - fn show_contact_finder(&mut self, cx: &mut ViewContext) { - let child = cx.add_view(|cx| ContactFinder::new(self.user_store.clone(), cx)); + fn show_contact_finder(&mut self, editor_text: String, cx: &mut ViewContext) { + let child = cx.add_view(|cx| { + ContactFinder::new(self.user_store.clone(), cx).with_editor_text(editor_text, cx) + }); cx.focus(&child); self._subscription = Some(cx.subscribe(&child, |_, _, event, cx| match event { crate::contact_finder::Event::Dismissed => cx.emit(Event::Dismissed), @@ -64,9 +68,11 @@ impl ContactsPopover { cx.notify(); } - fn show_contact_list(&mut self, cx: &mut ViewContext) { - let child = - cx.add_view(|cx| ContactList::new(self.project.clone(), self.user_store.clone(), cx)); + fn show_contact_list(&mut self, editor_text: String, cx: &mut ViewContext) { + let child = cx.add_view(|cx| { + ContactList::new(self.project.clone(), self.user_store.clone(), cx) + .with_editor_text(editor_text, cx) + }); cx.focus(&child); self._subscription = Some(cx.subscribe(&child, |_, _, event, cx| match event { crate::contact_list::Event::Dismissed => cx.emit(Event::Dismissed), diff --git a/crates/collab_ui/src/incoming_call_notification.rs b/crates/collab_ui/src/incoming_call_notification.rs index a0f54abe386ca56ef416f051dc2474e1736cc332..6fb0278218987d0e50ea7acc8792a24258426702 100644 --- a/crates/collab_ui/src/incoming_call_notification.rs +++ b/crates/collab_ui/src/incoming_call_notification.rs @@ -108,7 +108,7 @@ impl IncomingCallNotification { .unwrap_or(&default_project); Flex::row() .with_children(self.call.calling_user.avatar.clone().map(|avatar| { - Image::new(avatar) + Image::from_data(avatar) .with_style(theme.caller_avatar) .aligned() .boxed() diff --git a/crates/collab_ui/src/notifications.rs b/crates/collab_ui/src/notifications.rs index 06b6cf2a90984fe57b932892f94fda83bb06b369..21c2d2c2185ed69d6c19ad85be51cff1231d5bcb 100644 --- a/crates/collab_ui/src/notifications.rs +++ b/crates/collab_ui/src/notifications.rs @@ -24,7 +24,7 @@ pub fn render_user_notification( .with_child( Flex::row() .with_children(user.avatar.clone().map(|avatar| { - Image::new(avatar) + Image::from_data(avatar) .with_style(theme.header_avatar) .aligned() .constrained() diff --git a/crates/collab_ui/src/project_shared_notification.rs b/crates/collab_ui/src/project_shared_notification.rs index edf0354eec0d53be306e613697962a72cc34a683..b24f3492da933f4a868be65cfb2f3344679fc6c6 100644 --- a/crates/collab_ui/src/project_shared_notification.rs +++ b/crates/collab_ui/src/project_shared_notification.rs @@ -108,7 +108,7 @@ impl ProjectSharedNotification { let theme = &cx.global::().theme.project_shared_notification; Flex::row() .with_children(self.owner.avatar.clone().map(|avatar| { - Image::new(avatar) + Image::from_data(avatar) .with_style(theme.owner_avatar) .aligned() .boxed() diff --git a/crates/command_palette/src/command_palette.rs b/crates/command_palette/src/command_palette.rs index 55522966fa48f1f25f402d999917144c0d884be1..52a0e1cdc0b15f62ef2a66da56f39ac0e2169a03 100644 --- a/crates/command_palette/src/command_palette.rs +++ b/crates/command_palette/src/command_palette.rs @@ -352,9 +352,7 @@ mod tests { }); let project = Project::test(app_state.fs.clone(), [], cx).await; - let (_, workspace) = cx.add_window(|cx| { - Workspace::new(Default::default(), 0, project, |_, _| unimplemented!(), cx) - }); + let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx)); let editor = cx.add_view(&workspace, |cx| { let mut editor = Editor::single_line(None, cx); editor.set_text("abc", cx); diff --git a/crates/db/src/db.rs b/crates/db/src/db.rs index 20f2300d89b591230dcc41df4008e8c1fc65ec7a..ae9325ea2ea381d89b7e0cfddb48bc0cb22a0e16 100644 --- a/crates/db/src/db.rs +++ b/crates/db/src/db.rs @@ -4,6 +4,7 @@ pub mod query; // Re-export pub use anyhow; use anyhow::Context; +use gpui::MutableAppContext; pub use indoc::indoc; pub use lazy_static; use parking_lot::{Mutex, RwLock}; @@ -17,6 +18,7 @@ use sqlez::domain::Migrator; use sqlez::thread_safe_connection::ThreadSafeConnection; use sqlez_macros::sql; use std::fs::create_dir_all; +use std::future::Future; use std::path::{Path, PathBuf}; use std::sync::atomic::{AtomicBool, Ordering}; use std::time::{SystemTime, UNIX_EPOCH}; @@ -39,6 +41,7 @@ const FALLBACK_DB_NAME: &'static str = "FALLBACK_MEMORY_DB"; const DB_FILE_NAME: &'static str = "db.sqlite"; lazy_static::lazy_static! { + // !!!!!!! CHANGE BACK TO DEFAULT FALSE BEFORE SHIPPING static ref ZED_STATELESS: bool = std::env::var("ZED_STATELESS").map_or(false, |v| !v.is_empty()); static ref DB_FILE_OPERATIONS: Mutex<()> = Mutex::new(()); pub static ref BACKUP_DB_PATH: RwLock> = RwLock::new(None); @@ -63,11 +66,11 @@ pub async fn open_db( let connection = async_iife!({ // Note: This still has a race condition where 1 set of migrations succeeds // (e.g. (Workspace, Editor)) and another fails (e.g. (Workspace, Terminal)) - // This will cause the first connection to have the database taken out + // This will cause the first connection to have the database taken out // from under it. This *should* be fine though. The second dabatase failure will // cause errors in the log and so should be observed by developers while writing // soon-to-be good migrations. If user databases are corrupted, we toss them out - // and try again from a blank. As long as running all migrations from start to end + // and try again from a blank. As long as running all migrations from start to end // on a blank database is ok, this race condition will never be triggered. // // Basically: Don't ever push invalid migrations to stable or everyone will have @@ -85,7 +88,7 @@ pub async fn open_db( }; } - // Take a lock in the failure case so that we move the db once per process instead + // Take a lock in the failure case so that we move the db once per process instead // of potentially multiple times from different threads. This shouldn't happen in the // normal path let _lock = DB_FILE_OPERATIONS.lock(); @@ -236,6 +239,15 @@ macro_rules! define_connection { }; } +pub fn write_and_log(cx: &mut MutableAppContext, db_write: impl FnOnce() -> F + Send + 'static) +where + F: Future> + Send, +{ + cx.background() + .spawn(async move { db_write().await.log_err() }) + .detach() +} + #[cfg(test)] mod tests { use std::{fs, thread}; diff --git a/crates/diagnostics/src/diagnostics.rs b/crates/diagnostics/src/diagnostics.rs index e447a26c6b64159689dd6fc27b37d537507d7f21..223255544290ba93523a27ea5085f7871801e2a6 100644 --- a/crates/diagnostics/src/diagnostics.rs +++ b/crates/diagnostics/src/diagnostics.rs @@ -805,15 +805,7 @@ mod tests { .await; let project = Project::test(app_state.fs.clone(), ["/test".as_ref()], cx).await; - let (_, workspace) = cx.add_window(|cx| { - Workspace::new( - Default::default(), - 0, - project.clone(), - |_, _| unimplemented!(), - cx, - ) - }); + let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx)); // Create some diagnostics project.update(cx, |project, cx| { diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 3e7a14d2aea29312bb6b6853f18957ca116eb5f6..7f60309f6ab39ad1839598fc612d465e2f5cf19d 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -1254,6 +1254,15 @@ impl Editor { self.buffer.read(cx).language_at(point, cx) } + pub fn active_excerpt( + &self, + cx: &AppContext, + ) -> Option<(ExcerptId, ModelHandle, Range)> { + self.buffer + .read(cx) + .excerpt_containing(self.selections.newest_anchor().head(), cx) + } + fn style(&self, cx: &AppContext) -> EditorStyle { build_style( cx.global::(), diff --git a/crates/editor/src/editor_tests.rs b/crates/editor/src/editor_tests.rs index 269390f72f14f7722230234579826a3ffaea7301..4f61bde39877bb0fb7ba40c0000077cf75c9fe82 100644 --- a/crates/editor/src/editor_tests.rs +++ b/crates/editor/src/editor_tests.rs @@ -484,7 +484,7 @@ fn test_navigation_history(cx: &mut gpui::MutableAppContext) { cx.set_global(Settings::test(cx)); cx.set_global(DragAndDrop::::default()); use workspace::item::Item; - let (_, pane) = cx.add_window(Default::default(), |cx| Pane::new(None, cx)); + let (_, pane) = cx.add_window(Default::default(), |cx| Pane::new(0, None, || &[], cx)); let buffer = MultiBuffer::build_simple(&sample_text(300, 5, 'a'), cx); cx.add_view(&pane, |cx| { @@ -2353,12 +2353,16 @@ async fn test_clipboard(cx: &mut gpui::TestAppContext) { e.paste(&Paste, cx); e.handle_input(") ", cx); }); - cx.assert_editor_state(indoc! {" - ( one✅ - three - five ) ˇtwo one✅ four three six five ( one✅ - three - five ) ˇ"}); + cx.assert_editor_state( + &([ + "( one✅ ", + "three ", + "five ) ˇtwo one✅ four three six five ( one✅ ", + "three ", + "five ) ˇ", + ] + .join("\n")), + ); // Cut with three selections, one of which is full-line. cx.set_state(indoc! {" @@ -5562,7 +5566,7 @@ async fn test_following_with_multiple_excerpts(cx: &mut gpui::TestAppContext) { Settings::test_async(cx); let fs = FakeFs::new(cx.background()); let project = Project::test(fs, ["/file.rs".as_ref()], cx).await; - let (_, pane) = cx.add_window(|cx| Pane::new(None, cx)); + let (_, pane) = cx.add_window(|cx| Pane::new(0, None, || &[], cx)); let leader = pane.update(cx, |_, cx| { let multibuffer = cx.add_model(|_| MultiBuffer::new(0)); @@ -5831,11 +5835,11 @@ async fn go_to_hunk(deterministic: Arc, cx: &mut gpui::TestAppCon cx.assert_editor_state( &r#" ˇuse some::modified; - - + + fn main() { println!("hello there"); - + println!("around the"); println!("world"); } diff --git a/crates/editor/src/hover_popover.rs b/crates/editor/src/hover_popover.rs index f92b07da1dd49ef4de90e7bb9fc9a00ee4993fd4..1a23a02efc3fd93cd6c73571f84f93449f588de4 100644 --- a/crates/editor/src/hover_popover.rs +++ b/crates/editor/src/hover_popover.rs @@ -1,3 +1,4 @@ +use futures::FutureExt; use gpui::{ actions, elements::{Flex, MouseEventHandler, Padding, Text}, @@ -327,12 +328,10 @@ impl InfoPopover { MouseEventHandler::::new(0, cx, |_, cx| { let mut flex = Flex::new(Axis::Vertical).scrollable::(1, None, cx); flex.extend(self.contents.iter().map(|content| { - let project = self.project.read(cx); - if let Some(language) = content - .language - .clone() - .and_then(|language| project.languages().language_for_name(&language)) - { + let languages = self.project.read(cx).languages(); + if let Some(language) = content.language.clone().and_then(|language| { + languages.language_for_name(&language).now_or_never()?.ok() + }) { let runs = language .highlight_text(&content.text.as_str().into(), 0..content.text.len()); diff --git a/crates/editor/src/items.rs b/crates/editor/src/items.rs index 0d594a66ef354ab49152a2e0e3d318592f6c3233..cda702de00f6411e10bda7ad890d8136688ebf42 100644 --- a/crates/editor/src/items.rs +++ b/crates/editor/src/items.rs @@ -612,9 +612,34 @@ impl Item for Editor { let buffers = self.buffer().clone().read(cx).all_buffers(); cx.as_mut().spawn(|mut cx| async move { format.await?; - project - .update(&mut cx, |project, cx| project.save_buffers(buffers, cx)) - .await?; + + if buffers.len() == 1 { + project + .update(&mut cx, |project, cx| project.save_buffers(buffers, cx)) + .await?; + } else { + // For multi-buffers, only save those ones that contain changes. For clean buffers + // we simulate saving by calling `Buffer::did_save`, so that language servers or + // other downstream listeners of save events get notified. + let (dirty_buffers, clean_buffers) = buffers.into_iter().partition(|buffer| { + buffer.read_with(&cx, |buffer, _| buffer.is_dirty() || buffer.has_conflict()) + }); + + project + .update(&mut cx, |project, cx| { + project.save_buffers(dirty_buffers, cx) + }) + .await?; + for buffer in clean_buffers { + buffer.update(&mut cx, |buffer, cx| { + let version = buffer.saved_version().clone(); + let fingerprint = buffer.saved_version_fingerprint(); + let mtime = buffer.saved_mtime(); + buffer.did_save(version, fingerprint, mtime, cx); + }); + } + } + Ok(()) }) } diff --git a/crates/editor/src/multi_buffer.rs b/crates/editor/src/multi_buffer.rs index da3c6bc4bd44ad76a01c407f41be7e6429a1bbd1..e1e82859317929404182b7766d34b143ff5c01dc 100644 --- a/crates/editor/src/multi_buffer.rs +++ b/crates/editor/src/multi_buffer.rs @@ -1082,18 +1082,21 @@ impl MultiBuffer { let mut cursor = snapshot.excerpts.cursor::(); cursor.seek(&position, Bias::Right, &()); - cursor.item().map(|excerpt| { - ( - excerpt.id.clone(), - self.buffers - .borrow() - .get(&excerpt.buffer_id) - .unwrap() - .buffer - .clone(), - excerpt.range.context.clone(), - ) - }) + cursor + .item() + .or_else(|| snapshot.excerpts.last()) + .map(|excerpt| { + ( + excerpt.id.clone(), + self.buffers + .borrow() + .get(&excerpt.buffer_id) + .unwrap() + .buffer + .clone(), + excerpt.range.context.clone(), + ) + }) } // If point is at the end of the buffer, the last excerpt is returned diff --git a/crates/editor/src/test/editor_lsp_test_context.rs b/crates/editor/src/test/editor_lsp_test_context.rs index 1b6d846e710f4ebd6f84c8053ab88d4c36294ef5..fe9a7909b8538bd7df4d91a97ff058d19cd1b3da 100644 --- a/crates/editor/src/test/editor_lsp_test_context.rs +++ b/crates/editor/src/test/editor_lsp_test_context.rs @@ -65,15 +65,7 @@ impl<'a> EditorLspTestContext<'a> { .insert_tree("/root", json!({ "dir": { file_name.clone(): "" }})) .await; - let (window_id, workspace) = cx.add_window(|cx| { - Workspace::new( - Default::default(), - 0, - project.clone(), - |_, _| unimplemented!(), - cx, - ) - }); + let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx)); project .update(cx, |project, cx| { project.find_or_create_local_worktree("/root", true, cx) @@ -134,7 +126,7 @@ impl<'a> EditorLspTestContext<'a> { (let_chain) (await_expression) ] @indent - + (_ "[" "]" @end) @indent (_ "<" ">" @end) @indent (_ "{" "}" @end) @indent diff --git a/crates/feedback/src/deploy_feedback_button.rs b/crates/feedback/src/deploy_feedback_button.rs index 8fcafdfedee101458f0513d145d504831bfbdb34..222c542eed4dba57e31a1b61d1099b4a72f02e24 100644 --- a/crates/feedback/src/deploy_feedback_button.rs +++ b/crates/feedback/src/deploy_feedback_button.rs @@ -1,34 +1,59 @@ -use gpui::{ - elements::{MouseEventHandler, ParentElement, Stack, Text}, - CursorStyle, Element, ElementBox, Entity, MouseButton, RenderContext, View, ViewContext, -}; +use gpui::{elements::*, CursorStyle, Entity, MouseButton, RenderContext, View, ViewContext}; use settings::Settings; use workspace::{item::ItemHandle, StatusItemView}; -use crate::feedback_editor::GiveFeedback; +use crate::feedback_editor::{FeedbackEditor, GiveFeedback}; -pub struct DeployFeedbackButton; +pub struct DeployFeedbackButton { + active: bool, +} impl Entity for DeployFeedbackButton { type Event = (); } +impl DeployFeedbackButton { + pub fn new() -> Self { + DeployFeedbackButton { active: false } + } +} + impl View for DeployFeedbackButton { fn ui_name() -> &'static str { "DeployFeedbackButton" } fn render(&mut self, cx: &mut RenderContext<'_, Self>) -> ElementBox { + let active = self.active; Stack::new() .with_child( MouseEventHandler::::new(0, cx, |state, cx| { let theme = &cx.global::().theme; - let theme = &theme.workspace.status_bar.feedback; + let style = &theme + .workspace + .status_bar + .sidebar_buttons + .item + .style_for(state, active); - Text::new("Give Feedback", theme.style_for(state, true).clone()).boxed() + Svg::new("icons/speech_bubble_12.svg") + .with_color(style.icon_color) + .constrained() + .with_width(style.icon_size) + .aligned() + .constrained() + .with_width(style.icon_size) + .with_height(style.icon_size) + .contained() + .with_style(style.container) + .boxed() }) .with_cursor_style(CursorStyle::PointingHand) - .on_click(MouseButton::Left, |_, cx| cx.dispatch_action(GiveFeedback)) + .on_click(MouseButton::Left, move |_, cx| { + if !active { + cx.dispatch_action(GiveFeedback) + } + }) .boxed(), ) .boxed() @@ -36,5 +61,15 @@ impl View for DeployFeedbackButton { } impl StatusItemView for DeployFeedbackButton { - fn set_active_pane_item(&mut self, _: Option<&dyn ItemHandle>, _: &mut ViewContext) {} + fn set_active_pane_item(&mut self, item: Option<&dyn ItemHandle>, cx: &mut ViewContext) { + if let Some(item) = item { + if let Some(_) = item.downcast::() { + self.active = true; + cx.notify(); + return; + } + } + self.active = false; + cx.notify(); + } } diff --git a/crates/feedback/src/feedback_editor.rs b/crates/feedback/src/feedback_editor.rs index e30b85170c5fd7f128948d7622c166b03b30522d..8b12e859e64eb93e070fc1cfa196f87431c73244 100644 --- a/crates/feedback/src/feedback_editor.rs +++ b/crates/feedback/src/feedback_editor.rs @@ -20,6 +20,7 @@ use postage::prelude::Stream; use project::Project; use serde::Serialize; +use util::ResultExt; use workspace::{ item::{Item, ItemHandle}, searchable::{SearchableItem, SearchableItemHandle}, @@ -200,24 +201,28 @@ impl FeedbackEditor { impl FeedbackEditor { pub fn deploy( system_specs: SystemSpecs, - workspace: &mut Workspace, + _: &mut Workspace, app_state: Arc, cx: &mut ViewContext, ) { - workspace - .with_local_workspace(&app_state, cx, |workspace, cx| { - let project = workspace.project().clone(); - let markdown_language = project.read(cx).languages().language_for_name("Markdown"); - let buffer = project - .update(cx, |project, cx| { - project.create_buffer("", markdown_language, cx) + let markdown = app_state.languages.language_for_name("Markdown"); + cx.spawn(|workspace, mut cx| async move { + let markdown = markdown.await.log_err(); + workspace + .update(&mut cx, |workspace, cx| { + workspace.with_local_workspace(&app_state, cx, |workspace, cx| { + let project = workspace.project().clone(); + let buffer = project + .update(cx, |project, cx| project.create_buffer("", markdown, cx)) + .expect("creating buffers on a local workspace always succeeds"); + let feedback_editor = cx + .add_view(|cx| FeedbackEditor::new(system_specs, project, buffer, cx)); + workspace.add_item(Box::new(feedback_editor), cx); }) - .expect("creating buffers on a local workspace always succeeds"); - let feedback_editor = - cx.add_view(|cx| FeedbackEditor::new(system_specs, project, buffer, cx)); - workspace.add_item(Box::new(feedback_editor), cx); - }) - .detach(); + }) + .await; + }) + .detach(); } } diff --git a/crates/file_finder/src/file_finder.rs b/crates/file_finder/src/file_finder.rs index 273440dce253f54e8ccf5cb520bff321d403b1be..51d4df45f58fe59e32b4cc1eefbd10f9d49ddeaf 100644 --- a/crates/file_finder/src/file_finder.rs +++ b/crates/file_finder/src/file_finder.rs @@ -329,9 +329,7 @@ mod tests { .await; let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await; - let (window_id, workspace) = cx.add_window(|cx| { - Workspace::new(Default::default(), 0, project, |_, _| unimplemented!(), cx) - }); + let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx)); cx.dispatch_action(window_id, Toggle); let finder = cx.read(|cx| workspace.read(cx).modal::().unwrap()); @@ -385,9 +383,7 @@ mod tests { .await; let project = Project::test(app_state.fs.clone(), ["/dir".as_ref()], cx).await; - let (_, workspace) = cx.add_window(|cx| { - Workspace::new(Default::default(), 0, project, |_, _| unimplemented!(), cx) - }); + let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx)); let (_, finder) = cx.add_window(|cx| FileFinder::new(workspace.read(cx).project().clone(), None, cx)); @@ -461,9 +457,7 @@ mod tests { cx, ) .await; - let (_, workspace) = cx.add_window(|cx| { - Workspace::new(Default::default(), 0, project, |_, _| unimplemented!(), cx) - }); + let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx)); let (_, finder) = cx.add_window(|cx| FileFinder::new(workspace.read(cx).project().clone(), None, cx)); finder @@ -487,9 +481,7 @@ mod tests { cx, ) .await; - let (_, workspace) = cx.add_window(|cx| { - Workspace::new(Default::default(), 0, project, |_, _| unimplemented!(), cx) - }); + let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx)); let (_, finder) = cx.add_window(|cx| FileFinder::new(workspace.read(cx).project().clone(), None, cx)); @@ -541,9 +533,7 @@ mod tests { cx, ) .await; - let (_, workspace) = cx.add_window(|cx| { - Workspace::new(Default::default(), 0, project, |_, _| unimplemented!(), cx) - }); + let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx)); let (_, finder) = cx.add_window(|cx| FileFinder::new(workspace.read(cx).project().clone(), None, cx)); @@ -585,9 +575,7 @@ mod tests { .await; let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await; - let (_, workspace) = cx.add_window(|cx| { - Workspace::new(Default::default(), 0, project, |_, _| unimplemented!(), cx) - }); + let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx)); // When workspace has an active item, sort items which are closer to that item // first when they have the same name. In this case, b.txt is closer to dir2's a.txt @@ -624,9 +612,7 @@ mod tests { .await; let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await; - let (_, workspace) = cx.add_window(|cx| { - Workspace::new(Default::default(), 0, project, |_, _| unimplemented!(), cx) - }); + let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx)); let (_, finder) = cx.add_window(|cx| FileFinder::new(workspace.read(cx).project().clone(), None, cx)); finder diff --git a/crates/gpui/src/app.rs b/crates/gpui/src/app.rs index 31563010b723d88b4f834084fba91cb3ecd9a15c..7fc1de83fdc69257e8a0384b41db839dcfc21d13 100644 --- a/crates/gpui/src/app.rs +++ b/crates/gpui/src/app.rs @@ -485,7 +485,9 @@ pub struct MutableAppContext { cx: AppContext, action_deserializers: HashMap<&'static str, (TypeId, DeserializeActionCallback)>, capture_actions: HashMap>>>, + // Entity Types -> { Action Types -> Action Handlers } actions: HashMap>>>, + // Action Types -> Action Handlers global_actions: HashMap>, keystroke_matcher: KeymapMatcher, next_entity_id: usize, @@ -1239,20 +1241,34 @@ impl MutableAppContext { action: &dyn Action, ) -> Option> { let mut contexts = Vec::new(); - for view_id in self.ancestors(window_id, view_id) { + let mut handler_depth = None; + for (i, view_id) in self.ancestors(window_id, view_id).enumerate() { if let Some(view) = self.views.get(&(window_id, view_id)) { + if let Some(actions) = self.actions.get(&view.as_any().type_id()) { + if actions.contains_key(&action.as_any().type_id()) { + handler_depth = Some(i); + } + } contexts.push(view.keymap_context(self)); } } + if self.global_actions.contains_key(&action.as_any().type_id()) { + handler_depth = Some(contexts.len()) + } + self.keystroke_matcher .bindings_for_action_type(action.as_any().type_id()) .find_map(|b| { - if b.match_context(&contexts) { - Some(b.keystrokes().into()) - } else { - None - } + handler_depth + .map(|highest_handler| { + if (0..=highest_handler).any(|depth| b.match_context(&contexts[depth..])) { + Some(b.keystrokes().into()) + } else { + None + } + }) + .flatten() }) } @@ -1261,29 +1277,42 @@ impl MutableAppContext { window_id: usize, view_id: usize, ) -> impl Iterator, SmallVec<[&Binding; 1]>)> { - let mut action_types: HashSet<_> = self.global_actions.keys().copied().collect(); - let mut contexts = Vec::new(); - for view_id in self.ancestors(window_id, view_id) { + let mut handler_depths_by_action_type = HashMap::::default(); + for (depth, view_id) in self.ancestors(window_id, view_id).enumerate() { if let Some(view) = self.views.get(&(window_id, view_id)) { contexts.push(view.keymap_context(self)); let view_type = view.as_any().type_id(); if let Some(actions) = self.actions.get(&view_type) { - action_types.extend(actions.keys().copied()); + handler_depths_by_action_type.extend( + actions + .keys() + .copied() + .map(|action_type| (action_type, depth)), + ); } } } + handler_depths_by_action_type.extend( + self.global_actions + .keys() + .copied() + .map(|action_type| (action_type, contexts.len())), + ); + self.action_deserializers .iter() .filter_map(move |(name, (type_id, deserialize))| { - if action_types.contains(type_id) { + if let Some(action_depth) = handler_depths_by_action_type.get(type_id).copied() { Some(( *name, deserialize("{}").ok()?, self.keystroke_matcher .bindings_for_action_type(*type_id) - .filter(|b| b.match_context(&contexts)) + .filter(|b| { + (0..=action_depth).any(|depth| b.match_context(&contexts[depth..])) + }) .collect(), )) } else { @@ -1294,7 +1323,7 @@ impl MutableAppContext { pub fn is_action_available(&self, action: &dyn Action) -> bool { let action_type = action.as_any().type_id(); - if let Some(window_id) = self.cx.platform.key_window_id() { + if let Some(window_id) = self.cx.platform.main_window_id() { if let Some(focused_view_id) = self.focused_view_id(window_id) { for view_id in self.ancestors(window_id, focused_view_id) { if let Some(view) = self.views.get(&(window_id, view_id)) { @@ -5086,7 +5115,7 @@ impl From> for AnyWeakModelHandle { } } -#[derive(Debug)] +#[derive(Debug, Copy)] pub struct WeakViewHandle { window_id: usize, view_id: usize, @@ -5287,6 +5316,7 @@ impl Subscription { mod tests { use super::*; use crate::{actions, elements::*, impl_actions, MouseButton, MouseButtonEvent}; + use itertools::Itertools; use postage::{sink::Sink, stream::Stream}; use serde::Deserialize; use smol::future::poll_once; @@ -6717,6 +6747,128 @@ mod tests { actions.borrow_mut().clear(); } + #[crate::test(self)] + fn test_keystrokes_for_action(cx: &mut MutableAppContext) { + actions!(test, [Action1, Action2, GlobalAction]); + + struct View1 {} + struct View2 {} + + impl Entity for View1 { + type Event = (); + } + impl Entity for View2 { + type Event = (); + } + + impl super::View for View1 { + fn render(&mut self, _: &mut RenderContext) -> ElementBox { + Empty::new().boxed() + } + fn ui_name() -> &'static str { + "View1" + } + } + impl super::View for View2 { + fn render(&mut self, _: &mut RenderContext) -> ElementBox { + Empty::new().boxed() + } + fn ui_name() -> &'static str { + "View2" + } + } + + let (window_id, view_1) = cx.add_window(Default::default(), |_| View1 {}); + let view_2 = cx.add_view(&view_1, |cx| { + cx.focus_self(); + View2 {} + }); + + cx.add_action(|_: &mut View1, _: &Action1, _cx| {}); + cx.add_action(|_: &mut View2, _: &Action2, _cx| {}); + cx.add_global_action(|_: &GlobalAction, _| {}); + + cx.add_bindings(vec![ + Binding::new("a", Action1, Some("View1")), + Binding::new("b", Action2, Some("View1 > View2")), + Binding::new("c", GlobalAction, Some("View3")), // View 3 does not exist + ]); + + // Sanity check + assert_eq!( + cx.keystrokes_for_action(window_id, view_1.id(), &Action1) + .unwrap() + .as_slice(), + &[Keystroke::parse("a").unwrap()] + ); + assert_eq!( + cx.keystrokes_for_action(window_id, view_2.id(), &Action2) + .unwrap() + .as_slice(), + &[Keystroke::parse("b").unwrap()] + ); + + // The 'a' keystroke propagates up the view tree from view_2 + // to view_1. The action, Action1, is handled by view_1. + assert_eq!( + cx.keystrokes_for_action(window_id, view_2.id(), &Action1) + .unwrap() + .as_slice(), + &[Keystroke::parse("a").unwrap()] + ); + + // Actions that are handled below the current view don't have bindings + assert_eq!( + cx.keystrokes_for_action(window_id, view_1.id(), &Action2), + None + ); + + // Actions that are handled in other branches of the tree should not have a binding + assert_eq!( + cx.keystrokes_for_action(window_id, view_2.id(), &GlobalAction), + None + ); + + // Produces a list of actions and keybindings + fn available_actions( + window_id: usize, + view_id: usize, + cx: &mut MutableAppContext, + ) -> Vec<(&'static str, Vec)> { + cx.available_actions(window_id, view_id) + .map(|(action_name, _, bindings)| { + ( + action_name, + bindings + .iter() + .map(|binding| binding.keystrokes()[0].clone()) + .collect::>(), + ) + }) + .sorted_by(|(name1, _), (name2, _)| name1.cmp(name2)) + .collect() + } + + // Check that global actions do not have a binding, even if a binding does exist in another view + assert_eq!( + &available_actions(window_id, view_1.id(), cx), + &[ + ("test::Action1", vec![Keystroke::parse("a").unwrap()]), + ("test::GlobalAction", vec![]) + ], + ); + + // Check that view 1 actions and bindings are available even when called from view 2 + assert_eq!( + &available_actions(window_id, view_2.id(), cx), + &[ + ("test::Action1", vec![Keystroke::parse("a").unwrap()]), + ("test::Action2", vec![Keystroke::parse("b").unwrap()]), + ("test::GlobalAction", vec![]), + ], + ); + } + #[crate::test(self)] async fn test_model_condition(cx: &mut TestAppContext) { struct Counter(usize); diff --git a/crates/gpui/src/app/menu.rs b/crates/gpui/src/app/menu.rs index 5fe307a7c10f0bc3a6a9f64c5aa52a4aa69d48a0..b0965539a515e0fc53b169a48bf950c838e92896 100644 --- a/crates/gpui/src/app/menu.rs +++ b/crates/gpui/src/app/menu.rs @@ -77,9 +77,9 @@ pub(crate) fn setup_menu_handlers(foreground_platform: &dyn ForegroundPlatform, let cx = app.0.clone(); move |action| { let mut cx = cx.borrow_mut(); - if let Some(key_window_id) = cx.cx.platform.key_window_id() { - if let Some(view_id) = cx.focused_view_id(key_window_id) { - cx.handle_dispatch_action_from_effect(key_window_id, Some(view_id), action); + if let Some(main_window_id) = cx.cx.platform.main_window_id() { + if let Some(view_id) = cx.focused_view_id(main_window_id) { + cx.handle_dispatch_action_from_effect(main_window_id, Some(view_id), action); return; } } diff --git a/crates/gpui/src/app/test_app_context.rs b/crates/gpui/src/app/test_app_context.rs index 1366e7e6edb2877033de15318f3310d164d37967..ad78346991e809751b89d06555ed1f72a97c94c2 100644 --- a/crates/gpui/src/app/test_app_context.rs +++ b/crates/gpui/src/app/test_app_context.rs @@ -18,9 +18,10 @@ use smol::stream::StreamExt; use crate::{ executor, geometry::vector::Vector2F, keymap_matcher::Keystroke, platform, Action, - AnyViewHandle, AppContext, Appearance, Entity, Event, FontCache, InputHandler, KeyDownEvent, - ModelContext, ModelHandle, MutableAppContext, Platform, ReadModelWith, ReadViewWith, - RenderContext, Task, UpdateModel, UpdateView, View, ViewContext, ViewHandle, WeakHandle, + AnyViewHandle, AppContext, Appearance, Entity, Event, FontCache, Handle, InputHandler, + KeyDownEvent, ModelContext, ModelHandle, MutableAppContext, Platform, ReadModelWith, + ReadViewWith, RenderContext, Task, UpdateModel, UpdateView, View, ViewContext, ViewHandle, + WeakHandle, }; use collections::BTreeMap; @@ -329,6 +330,14 @@ impl TestAppContext { .assert_dropped(handle.id()) } + /// Drop a handle, assuming it is the last. If it is not the last, panic with debug information about + /// where the stray handles were created. + pub fn drop_last>(&mut self, handle: H) { + let weak = handle.downgrade(); + self.update(|_| drop(handle)); + self.assert_dropped(weak); + } + fn window_mut(&self, window_id: usize) -> std::cell::RefMut { std::cell::RefMut::map(self.cx.borrow_mut(), |state| { let (_, window) = state diff --git a/crates/gpui/src/assets.rs b/crates/gpui/src/assets.rs index ac0d72dee97a8d5553963c4677bc3f6051fbaf34..2170d215af078257326c258c7502f4c90c84e19e 100644 --- a/crates/gpui/src/assets.rs +++ b/crates/gpui/src/assets.rs @@ -1,5 +1,8 @@ use anyhow::{anyhow, Result}; -use std::{borrow::Cow, cell::RefCell, collections::HashMap}; +use image::ImageFormat; +use std::{borrow::Cow, cell::RefCell, collections::HashMap, sync::Arc}; + +use crate::ImageData; pub trait AssetSource: 'static + Send + Sync { fn load(&self, path: &str) -> Result>; @@ -22,6 +25,7 @@ impl AssetSource for () { pub struct AssetCache { source: Box, svgs: RefCell>, + pngs: RefCell>>, } impl AssetCache { @@ -29,6 +33,7 @@ impl AssetCache { Self { source: Box::new(source), svgs: RefCell::new(HashMap::new()), + pngs: RefCell::new(HashMap::new()), } } @@ -43,4 +48,18 @@ impl AssetCache { Ok(svg) } } + + pub fn png(&self, path: &str) -> Result> { + let mut pngs = self.pngs.borrow_mut(); + if let Some(png) = pngs.get(path) { + Ok(png.clone()) + } else { + let bytes = self.source.load(path)?; + let image = ImageData::new( + image::load_from_memory_with_format(&bytes, ImageFormat::Png)?.into_bgra8(), + ); + pngs.insert(path.to_string(), image.clone()); + Ok(image) + } + } } diff --git a/crates/gpui/src/elements/constrained_box.rs b/crates/gpui/src/elements/constrained_box.rs index 2e232c6197774861fd14a453e62a8efb5772ee53..e4d51f5730be1bfa190ac7f2a7aa9e5e49b630e9 100644 --- a/crates/gpui/src/elements/constrained_box.rs +++ b/crates/gpui/src/elements/constrained_box.rs @@ -153,7 +153,9 @@ impl Element for ConstrainedBox { _: &mut Self::LayoutState, cx: &mut PaintContext, ) -> Self::PaintState { - self.child.paint(bounds.origin(), visible_bounds, cx); + cx.paint_layer(Some(visible_bounds), |cx| { + self.child.paint(bounds.origin(), visible_bounds, cx); + }) } fn rect_for_text_range( diff --git a/crates/gpui/src/elements/flex.rs b/crates/gpui/src/elements/flex.rs index ce595222f3f8a3c2e95019f19f790114f05653fd..5df283bfee2f20a7b51ec1900526764eab3ef0d4 100644 --- a/crates/gpui/src/elements/flex.rs +++ b/crates/gpui/src/elements/flex.rs @@ -22,6 +22,7 @@ pub struct Flex { axis: Axis, children: Vec, scroll_state: Option<(ElementStateHandle>, usize)>, + child_alignment: f32, } impl Flex { @@ -30,6 +31,7 @@ impl Flex { axis, children: Default::default(), scroll_state: None, + child_alignment: -1., } } @@ -41,6 +43,15 @@ impl Flex { Self::new(Axis::Vertical) } + /// Render children centered relative to the cross-axis of the parent flex. + /// + /// If this is a flex row, children will be centered vertically. If this is a + /// flex column, children will be centered horizontally. + pub fn align_children_center(mut self) -> Self { + self.child_alignment = 0.; + self + } + pub fn scrollable( mut self, element_id: usize, @@ -309,7 +320,30 @@ impl Element for Flex { } } - child.paint(child_origin, visible_bounds, cx); + // We use the child_alignment f32 to determine a point along the cross axis of the + // overall flex element and each child. We then align these points. So 0 would center + // each child relative to the overall height/width of the flex. -1 puts children at + // the start. 1 puts children at the end. + let aligned_child_origin = { + let cross_axis = self.axis.invert(); + let my_center = bounds.size().along(cross_axis) / 2.; + let my_target = my_center + my_center * self.child_alignment; + + let child_center = child.size().along(cross_axis) / 2.; + let child_target = child_center + child_center * self.child_alignment; + + let mut aligned_child_origin = child_origin; + match self.axis { + Axis::Horizontal => aligned_child_origin + .set_y(aligned_child_origin.y() - (child_target - my_target)), + Axis::Vertical => aligned_child_origin + .set_x(aligned_child_origin.x() - (child_target - my_target)), + } + + aligned_child_origin + }; + + child.paint(aligned_child_origin, visible_bounds, cx); match self.axis { Axis::Horizontal => child_origin += vec2f(child.size().x(), 0.0), diff --git a/crates/gpui/src/elements/image.rs b/crates/gpui/src/elements/image.rs index 37cb01ace8a6c43e0add3d366d888f06b4b0225e..cc49308e15011b317840da086170c28d1132996d 100644 --- a/crates/gpui/src/elements/image.rs +++ b/crates/gpui/src/elements/image.rs @@ -11,8 +11,13 @@ use crate::{ use serde::Deserialize; use std::{ops::Range, sync::Arc}; +enum ImageSource { + Path(&'static str), + Data(Arc), +} + pub struct Image { - data: Arc, + source: ImageSource, style: ImageStyle, } @@ -31,9 +36,16 @@ pub struct ImageStyle { } impl Image { - pub fn new(data: Arc) -> Self { + pub fn new(asset_path: &'static str) -> Self { + Self { + source: ImageSource::Path(asset_path), + style: Default::default(), + } + } + + pub fn from_data(data: Arc) -> Self { Self { - data, + source: ImageSource::Data(data), style: Default::default(), } } @@ -45,39 +57,53 @@ impl Image { } impl Element for Image { - type LayoutState = (); + type LayoutState = Option>; type PaintState = (); fn layout( &mut self, constraint: SizeConstraint, - _: &mut LayoutContext, + cx: &mut LayoutContext, ) -> (Vector2F, Self::LayoutState) { + let data = match &self.source { + ImageSource::Path(path) => match cx.asset_cache.png(path) { + Ok(data) => data, + Err(error) => { + log::error!("could not load image: {}", error); + return (Vector2F::zero(), None); + } + }, + ImageSource::Data(data) => data.clone(), + }; + let desired_size = vec2f( self.style.width.unwrap_or_else(|| constraint.max.x()), self.style.height.unwrap_or_else(|| constraint.max.y()), ); let size = constrain_size_preserving_aspect_ratio( constraint.constrain(desired_size), - self.data.size().to_f32(), + data.size().to_f32(), ); - (size, ()) + + (size, Some(data)) } fn paint( &mut self, bounds: RectF, _: RectF, - _: &mut Self::LayoutState, + layout: &mut Self::LayoutState, cx: &mut PaintContext, ) -> Self::PaintState { - cx.scene.push_image(scene::Image { - bounds, - border: self.style.border, - corner_radius: self.style.corner_radius, - grayscale: self.style.grayscale, - data: self.data.clone(), - }); + if let Some(data) = layout { + cx.scene.push_image(scene::Image { + bounds, + border: self.style.border, + corner_radius: self.style.corner_radius, + grayscale: self.style.grayscale, + data: data.clone(), + }); + } } fn rect_for_text_range( diff --git a/crates/gpui/src/platform.rs b/crates/gpui/src/platform.rs index 76c2707d2638823e057eecdd30a2614d592f717f..4c1ba49690c43d6b0e0f033b2d2177311a9e8873 100644 --- a/crates/gpui/src/platform.rs +++ b/crates/gpui/src/platform.rs @@ -58,7 +58,7 @@ pub trait Platform: Send + Sync { options: WindowOptions, executor: Rc, ) -> Box; - fn key_window_id(&self) -> Option; + fn main_window_id(&self) -> Option; fn add_status_item(&self) -> Box; diff --git a/crates/gpui/src/platform/mac/platform.rs b/crates/gpui/src/platform/mac/platform.rs index aeec02c8caa230c9be46025f0d160ef371f67f06..9bb6430c644cdaea1b29610b3d01637e967b721c 100644 --- a/crates/gpui/src/platform/mac/platform.rs +++ b/crates/gpui/src/platform/mac/platform.rs @@ -587,8 +587,8 @@ impl platform::Platform for MacPlatform { Box::new(Window::open(id, options, executor, self.fonts())) } - fn key_window_id(&self) -> Option { - Window::key_window_id() + fn main_window_id(&self) -> Option { + Window::main_window_id() } fn add_status_item(&self) -> Box { diff --git a/crates/gpui/src/platform/mac/window.rs b/crates/gpui/src/platform/mac/window.rs index 19173707fb13f1fa732b3bc20659dd6f0e6a4991..a0c1820368c802b5453ed33c2d8f1e50322228d8 100644 --- a/crates/gpui/src/platform/mac/window.rs +++ b/crates/gpui/src/platform/mac/window.rs @@ -604,12 +604,12 @@ impl Window { } } - pub fn key_window_id() -> Option { + pub fn main_window_id() -> Option { unsafe { let app = NSApplication::sharedApplication(nil); - let key_window: id = msg_send![app, keyWindow]; - if msg_send![key_window, isKindOfClass: WINDOW_CLASS] { - let id = get_window_state(&*key_window).borrow().id; + let main_window: id = msg_send![app, mainWindow]; + if msg_send![main_window, isKindOfClass: WINDOW_CLASS] { + let id = get_window_state(&*main_window).borrow().id; Some(id) } else { None diff --git a/crates/gpui/src/platform/test.rs b/crates/gpui/src/platform/test.rs index 194684bd12917daf64fa4e56c763b33663640abd..a3532dd96e1298fedaa6ffea2214a026cc46e085 100644 --- a/crates/gpui/src/platform/test.rs +++ b/crates/gpui/src/platform/test.rs @@ -157,7 +157,7 @@ impl super::Platform for Platform { })) } - fn key_window_id(&self) -> Option { + fn main_window_id(&self) -> Option { None } diff --git a/crates/install_cli/Cargo.toml b/crates/install_cli/Cargo.toml new file mode 100644 index 0000000000000000000000000000000000000000..bbbe9899209733f8abee8500139b56f10389c4a7 --- /dev/null +++ b/crates/install_cli/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "install_cli" +version = "0.1.0" +edition = "2021" +publish = false + +[lib] +path = "src/install_cli.rs" + +[features] +test-support = [] + +[dependencies] +smol = "1.2.5" +anyhow = "1.0.38" +log = "0.4" +gpui = { path = "../gpui" } +util = { path = "../util" } diff --git a/crates/install_cli/src/install_cli.rs b/crates/install_cli/src/install_cli.rs new file mode 100644 index 0000000000000000000000000000000000000000..adf50586d759ae24f39e3187b080a30d894f8eec --- /dev/null +++ b/crates/install_cli/src/install_cli.rs @@ -0,0 +1,55 @@ +use std::path::Path; + +use anyhow::{anyhow, Result}; +use gpui::{actions, AsyncAppContext}; +use util::ResultExt; + +actions!(cli, [Install]); + +pub async fn install_cli(cx: &AsyncAppContext) -> Result<()> { + let cli_path = cx.platform().path_for_auxiliary_executable("cli")?; + let link_path = Path::new("/usr/local/bin/zed"); + let bin_dir_path = link_path.parent().unwrap(); + + // Don't re-create symlink if it points to the same CLI binary. + if smol::fs::read_link(link_path).await.ok().as_ref() == Some(&cli_path) { + return Ok(()); + } + + // If the symlink is not there or is outdated, first try replacing it + // without escalating. + smol::fs::remove_file(link_path).await.log_err(); + if smol::fs::unix::symlink(&cli_path, link_path) + .await + .log_err() + .is_some() + { + return Ok(()); + } + + // The symlink could not be created, so use osascript with admin privileges + // to create it. + let status = smol::process::Command::new("osascript") + .args([ + "-e", + &format!( + "do shell script \" \ + mkdir -p \'{}\' && \ + ln -sf \'{}\' \'{}\' \ + \" with administrator privileges", + bin_dir_path.to_string_lossy(), + cli_path.to_string_lossy(), + link_path.to_string_lossy(), + ), + ]) + .stdout(smol::process::Stdio::inherit()) + .stderr(smol::process::Stdio::inherit()) + .output() + .await? + .status; + if status.success() { + Ok(()) + } else { + Err(anyhow!("error running osascript")) + } +} diff --git a/crates/journal/Cargo.toml b/crates/journal/Cargo.toml index b532397dd1ddcbc59bb24a21f3da0d6d61552c4c..a2656ad219f0c30916825f8776b667a3cf5add92 100644 --- a/crates/journal/Cargo.toml +++ b/crates/journal/Cargo.toml @@ -13,6 +13,7 @@ editor = { path = "../editor" } gpui = { path = "../gpui" } util = { path = "../util" } workspace = { path = "../workspace" } +anyhow = "1.0" chrono = "0.4" dirs = "4.0" log = { version = "0.4.16", features = ["kv_unstable_serde"] } diff --git a/crates/journal/src/journal.rs b/crates/journal/src/journal.rs index 76a56af93d8c4cdc45a60a31e4ebc007899f61cc..6db078ee93d11bac98aa1ead6c714ab9dbe30743 100644 --- a/crates/journal/src/journal.rs +++ b/crates/journal/src/journal.rs @@ -48,7 +48,7 @@ pub fn new_journal_entry(app_state: Arc, cx: &mut MutableAppContext) { async move { let (journal_dir, entry_path) = create_entry.await?; let (workspace, _) = cx - .update(|cx| workspace::open_paths(&[journal_dir], &app_state, cx)) + .update(|cx| workspace::open_paths(&[journal_dir], &app_state, None, cx)) .await; let opened = workspace @@ -73,7 +73,7 @@ pub fn new_journal_entry(app_state: Arc, cx: &mut MutableAppContext) { } } - Ok(()) + anyhow::Ok(()) } .log_err() }) diff --git a/crates/language/src/buffer.rs b/crates/language/src/buffer.rs index acccb553d3d14d11e6140fb8e2a81818214e84e1..09a96342271b00f508094b8426ab5ddf81b04131 100644 --- a/crates/language/src/buffer.rs +++ b/crates/language/src/buffer.rs @@ -1353,7 +1353,13 @@ impl Buffer { } pub fn remove_active_selections(&mut self, cx: &mut ModelContext) { - self.set_active_selections(Arc::from([]), false, Default::default(), cx); + if self + .remote_selections + .get(&self.text.replica_id()) + .map_or(true, |set| !set.selections.is_empty()) + { + self.set_active_selections(Arc::from([]), false, Default::default(), cx); + } } pub fn set_text(&mut self, text: T, cx: &mut ModelContext) -> Option diff --git a/crates/language/src/buffer_tests.rs b/crates/language/src/buffer_tests.rs index 0c375f0f4e1e27e59bbc737443307ae1b9c94c66..3e65ec3120445bb69604c056b200d4b8f66b6863 100644 --- a/crates/language/src/buffer_tests.rs +++ b/crates/language/src/buffer_tests.rs @@ -80,31 +80,49 @@ fn test_select_language() { // matching file extension assert_eq!( - registry.language_for_path("zed/lib.rs").map(|l| l.name()), + registry + .language_for_path("zed/lib.rs") + .now_or_never() + .and_then(|l| Some(l.ok()?.name())), Some("Rust".into()) ); assert_eq!( - registry.language_for_path("zed/lib.mk").map(|l| l.name()), + registry + .language_for_path("zed/lib.mk") + .now_or_never() + .and_then(|l| Some(l.ok()?.name())), Some("Make".into()) ); // matching filename assert_eq!( - registry.language_for_path("zed/Makefile").map(|l| l.name()), + registry + .language_for_path("zed/Makefile") + .now_or_never() + .and_then(|l| Some(l.ok()?.name())), Some("Make".into()) ); // matching suffix that is not the full file extension or filename assert_eq!( - registry.language_for_path("zed/cars").map(|l| l.name()), + registry + .language_for_path("zed/cars") + .now_or_never() + .and_then(|l| Some(l.ok()?.name())), None ); assert_eq!( - registry.language_for_path("zed/a.cars").map(|l| l.name()), + registry + .language_for_path("zed/a.cars") + .now_or_never() + .and_then(|l| Some(l.ok()?.name())), None ); assert_eq!( - registry.language_for_path("zed/sumk").map(|l| l.name()), + registry + .language_for_path("zed/sumk") + .now_or_never() + .and_then(|l| Some(l.ok()?.name())), None ); } @@ -666,14 +684,14 @@ fn test_enclosing_bracket_ranges(cx: &mut MutableAppContext) { indoc! {" mod x { moˇd y { - + } } let foo = 1;"}, vec![indoc! {" mod x «{» mod y { - + } «}» let foo = 1;"}], @@ -683,7 +701,7 @@ fn test_enclosing_bracket_ranges(cx: &mut MutableAppContext) { indoc! {" mod x { mod y ˇ{ - + } } let foo = 1;"}, @@ -691,14 +709,14 @@ fn test_enclosing_bracket_ranges(cx: &mut MutableAppContext) { indoc! {" mod x «{» mod y { - + } «}» let foo = 1;"}, indoc! {" mod x { mod y «{» - + «}» } let foo = 1;"}, @@ -709,7 +727,7 @@ fn test_enclosing_bracket_ranges(cx: &mut MutableAppContext) { indoc! {" mod x { mod y { - + }ˇ } let foo = 1;"}, @@ -717,14 +735,14 @@ fn test_enclosing_bracket_ranges(cx: &mut MutableAppContext) { indoc! {" mod x «{» mod y { - + } «}» let foo = 1;"}, indoc! {" mod x { mod y «{» - + «}» } let foo = 1;"}, @@ -735,14 +753,14 @@ fn test_enclosing_bracket_ranges(cx: &mut MutableAppContext) { indoc! {" mod x { mod y { - + } ˇ} let foo = 1;"}, vec![indoc! {" mod x «{» mod y { - + } «}» let foo = 1;"}], @@ -752,7 +770,7 @@ fn test_enclosing_bracket_ranges(cx: &mut MutableAppContext) { indoc! {" mod x { mod y { - + } } let fˇoo = 1;"}, @@ -764,7 +782,7 @@ fn test_enclosing_bracket_ranges(cx: &mut MutableAppContext) { indoc! {" mod x { mod y { - + } } let foo = 1;ˇ"}, @@ -1804,25 +1822,31 @@ fn test_random_collaboration(cx: &mut MutableAppContext, mut rng: StdRng) { } 30..=39 if mutation_count != 0 => { buffer.update(cx, |buffer, cx| { - let mut selections = Vec::new(); - for id in 0..rng.gen_range(1..=5) { - let range = buffer.random_byte_range(0, &mut rng); - selections.push(Selection { - id, - start: buffer.anchor_before(range.start), - end: buffer.anchor_before(range.end), - reversed: false, - goal: SelectionGoal::None, - }); + if rng.gen_bool(0.2) { + log::info!("peer {} clearing active selections", replica_id); + active_selections.remove(&replica_id); + buffer.remove_active_selections(cx); + } else { + let mut selections = Vec::new(); + for id in 0..rng.gen_range(1..=5) { + let range = buffer.random_byte_range(0, &mut rng); + selections.push(Selection { + id, + start: buffer.anchor_before(range.start), + end: buffer.anchor_before(range.end), + reversed: false, + goal: SelectionGoal::None, + }); + } + let selections: Arc<[Selection]> = selections.into(); + log::info!( + "peer {} setting active selections: {:?}", + replica_id, + selections + ); + active_selections.insert(replica_id, selections.clone()); + buffer.set_active_selections(selections, false, Default::default(), cx); } - let selections: Arc<[Selection]> = selections.into(); - log::info!( - "peer {} setting active selections: {:?}", - replica_id, - selections - ); - active_selections.insert(replica_id, selections.clone()); - buffer.set_active_selections(selections, false, Default::default(), cx); }); mutation_count -= 1; } diff --git a/crates/language/src/language.rs b/crates/language/src/language.rs index 983bc58f762e24a01fa1d9ff1beeca7585dda1e8..30fc5db56329d8621f0c52d506dbabce2cd428c3 100644 --- a/crates/language/src/language.rs +++ b/crates/language/src/language.rs @@ -13,8 +13,9 @@ use async_trait::async_trait; use client::http::HttpClient; use collections::HashMap; use futures::{ + channel::oneshot, future::{BoxFuture, Shared}, - FutureExt, TryFutureExt, + FutureExt, TryFutureExt as _, }; use gpui::{executor::Background, MutableAppContext, Task}; use highlight_map::HighlightMap; @@ -43,7 +44,7 @@ use syntax_map::SyntaxSnapshot; use theme::{SyntaxTheme, Theme}; use tree_sitter::{self, Query}; use unicase::UniCase; -use util::ResultExt; +use util::{ResultExt, TryFutureExt as _, UnwrapFuture}; #[cfg(any(test, feature = "test-support"))] use futures::channel::mpsc; @@ -484,7 +485,7 @@ impl LanguageRegistry { let (lsp_binary_statuses_tx, lsp_binary_statuses_rx) = async_broadcast::broadcast(16); Self { language_server_download_dir: None, - languages: Default::default(), + languages: RwLock::new(vec![PLAIN_TEXT.clone()]), available_languages: Default::default(), lsp_binary_statuses_tx, lsp_binary_statuses_rx, @@ -568,12 +569,18 @@ impl LanguageRegistry { self.language_server_download_dir = Some(path.into()); } - pub fn language_for_name(self: &Arc, name: &str) -> Option> { + pub fn language_for_name( + self: &Arc, + name: &str, + ) -> UnwrapFuture>>> { let name = UniCase::new(name); self.get_or_load_language(|config| UniCase::new(config.name.as_ref()) == name) } - pub fn language_for_name_or_extension(self: &Arc, string: &str) -> Option> { + pub fn language_for_name_or_extension( + self: &Arc, + string: &str, + ) -> UnwrapFuture>>> { let string = UniCase::new(string); self.get_or_load_language(|config| { UniCase::new(config.name.as_ref()) == string @@ -584,7 +591,10 @@ impl LanguageRegistry { }) } - pub fn language_for_path(self: &Arc, path: impl AsRef) -> Option> { + pub fn language_for_path( + self: &Arc, + path: impl AsRef, + ) -> UnwrapFuture>>> { let path = path.as_ref(); let filename = path.file_name().and_then(|name| name.to_str()); let extension = path.extension().and_then(|name| name.to_str()); @@ -600,17 +610,17 @@ impl LanguageRegistry { fn get_or_load_language( self: &Arc, callback: impl Fn(&LanguageConfig) -> bool, - ) -> Option> { + ) -> UnwrapFuture>>> { + let (tx, rx) = oneshot::channel(); + if let Some(language) = self .languages .read() .iter() .find(|language| callback(&language.config)) { - return Some(language.clone()); - } - - if let Some(executor) = self.executor.clone() { + let _ = tx.send(Ok(language.clone())); + } else if let Some(executor) = self.executor.clone() { let mut available_languages = self.available_languages.write(); if let Some(ix) = available_languages.iter().position(|l| callback(&l.config)) { @@ -625,18 +635,29 @@ impl LanguageRegistry { .with_lsp_adapter(language.lsp_adapter) .await; match language.with_queries(queries) { - Ok(language) => this.add(Arc::new(language)), + Ok(language) => { + let language = Arc::new(language); + this.add(language.clone()); + let _ = tx.send(Ok(language)); + } Err(err) => { - log::error!("failed to load language {}: {}", name, err); - return; + let _ = tx.send(Err(anyhow!( + "failed to load language {}: {}", + name, + err + ))); } }; }) .detach(); + } else { + let _ = tx.send(Err(anyhow!("language not found"))); } + } else { + let _ = tx.send(Err(anyhow!("executor does not exist"))); } - None + rx.unwrap() } pub fn to_vec(&self) -> Vec> { diff --git a/crates/language/src/syntax_map.rs b/crates/language/src/syntax_map.rs index 5923241955e417d0b7d5ddf429647ac76b778305..88de8e690a6101bc461ef03d641db97da0a4d7c8 100644 --- a/crates/language/src/syntax_map.rs +++ b/crates/language/src/syntax_map.rs @@ -1,5 +1,6 @@ use crate::{Grammar, InjectionConfig, Language, LanguageRegistry}; use collections::HashMap; +use futures::FutureExt; use lazy_static::lazy_static; use parking_lot::Mutex; use std::{ @@ -382,11 +383,11 @@ impl SyntaxSnapshot { cursor.next(text); while let Some(layer) = cursor.item() { let SyntaxLayerContent::Pending { language_name } = &layer.content else { unreachable!() }; - if { - let language_registry = ®istry; - language_registry.language_for_name_or_extension(language_name) - } - .is_some() + if registry + .language_for_name_or_extension(language_name) + .now_or_never() + .and_then(|language| language.ok()) + .is_some() { resolved_injection_ranges.push(layer.range.to_offset(text)); } @@ -1116,7 +1117,10 @@ fn get_injections( combined_injection_ranges.clear(); for pattern in &config.patterns { if let (Some(language_name), true) = (pattern.language.as_ref(), pattern.combined) { - if let Some(language) = language_registry.language_for_name_or_extension(language_name) + if let Some(language) = language_registry + .language_for_name_or_extension(language_name) + .now_or_never() + .and_then(|language| language.ok()) { combined_injection_ranges.insert(language, Vec::new()); } @@ -1162,10 +1166,10 @@ fn get_injections( }; if let Some(language_name) = language_name { - let language = { - let language_name: &str = &language_name; - language_registry.language_for_name_or_extension(language_name) - }; + let language = language_registry + .language_for_name_or_extension(&language_name) + .now_or_never() + .and_then(|language| language.ok()); let range = text.anchor_before(step_range.start)..text.anchor_after(step_range.end); if let Some(language) = language { if combined { @@ -2522,7 +2526,11 @@ mod tests { registry.add(Arc::new(html_lang())); registry.add(Arc::new(erb_lang())); registry.add(Arc::new(markdown_lang())); - let language = registry.language_for_name(language_name).unwrap(); + let language = registry + .language_for_name(language_name) + .now_or_never() + .unwrap() + .unwrap(); let mut buffer = Buffer::new(0, 0, Default::default()); let mut mutated_syntax_map = SyntaxMap::new(); diff --git a/crates/language_selector/Cargo.toml b/crates/language_selector/Cargo.toml new file mode 100644 index 0000000000000000000000000000000000000000..14aa41a4651072cc95773d177ab771779b62849d --- /dev/null +++ b/crates/language_selector/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "language_selector" +version = "0.1.0" +edition = "2021" +publish = false + +[lib] +path = "src/language_selector.rs" +doctest = false + +[dependencies] +editor = { path = "../editor" } +fuzzy = { path = "../fuzzy" } +language = { path = "../language" } +gpui = { path = "../gpui" } +picker = { path = "../picker" } +project = { path = "../project" } +theme = { path = "../theme" } +settings = { path = "../settings" } +workspace = { path = "../workspace" } +anyhow = "1.0" diff --git a/crates/language_selector/src/active_buffer_language.rs b/crates/language_selector/src/active_buffer_language.rs new file mode 100644 index 0000000000000000000000000000000000000000..1da0b4323c5134da7a0336f6816509ccbf0ba7d3 --- /dev/null +++ b/crates/language_selector/src/active_buffer_language.rs @@ -0,0 +1,87 @@ +use editor::Editor; +use gpui::{ + elements::*, CursorStyle, Entity, MouseButton, RenderContext, Subscription, View, ViewContext, + ViewHandle, +}; +use settings::Settings; +use std::sync::Arc; +use workspace::{item::ItemHandle, StatusItemView}; + +pub struct ActiveBufferLanguage { + active_language: Option>, + _observe_active_editor: Option, +} + +impl Default for ActiveBufferLanguage { + fn default() -> Self { + Self::new() + } +} + +impl ActiveBufferLanguage { + pub fn new() -> Self { + Self { + active_language: None, + _observe_active_editor: None, + } + } + + fn update_language(&mut self, editor: ViewHandle, cx: &mut ViewContext) { + self.active_language.take(); + + let editor = editor.read(cx); + if let Some((_, buffer, _)) = editor.active_excerpt(cx) { + if let Some(language) = buffer.read(cx).language() { + self.active_language = Some(language.name()); + } + } + + cx.notify(); + } +} + +impl Entity for ActiveBufferLanguage { + type Event = (); +} + +impl View for ActiveBufferLanguage { + fn ui_name() -> &'static str { + "ActiveBufferLanguage" + } + + fn render(&mut self, cx: &mut RenderContext) -> ElementBox { + if let Some(active_language) = self.active_language.as_ref() { + MouseEventHandler::::new(0, cx, |state, cx| { + let theme = &cx.global::().theme.workspace.status_bar; + let style = theme.active_language.style_for(state, false); + Label::new(active_language.to_string(), style.text.clone()) + .contained() + .with_style(style.container) + .boxed() + }) + .with_cursor_style(CursorStyle::PointingHand) + .on_click(MouseButton::Left, |_, cx| cx.dispatch_action(crate::Toggle)) + .boxed() + } else { + Empty::new().boxed() + } + } +} + +impl StatusItemView for ActiveBufferLanguage { + fn set_active_pane_item( + &mut self, + active_pane_item: Option<&dyn ItemHandle>, + cx: &mut ViewContext, + ) { + if let Some(editor) = active_pane_item.and_then(|item| item.act_as::(cx)) { + self._observe_active_editor = Some(cx.observe(&editor, Self::update_language)); + self.update_language(editor, cx); + } else { + self.active_language = None; + self._observe_active_editor = None; + } + + cx.notify(); + } +} diff --git a/crates/language_selector/src/language_selector.rs b/crates/language_selector/src/language_selector.rs new file mode 100644 index 0000000000000000000000000000000000000000..711e36f9c442ca1eaf297f7a14ed66a6cd0060bc --- /dev/null +++ b/crates/language_selector/src/language_selector.rs @@ -0,0 +1,230 @@ +mod active_buffer_language; + +pub use active_buffer_language::ActiveBufferLanguage; +use editor::Editor; +use fuzzy::{match_strings, StringMatch, StringMatchCandidate}; +use gpui::{ + actions, elements::*, AnyViewHandle, AppContext, Entity, ModelHandle, MouseState, + MutableAppContext, RenderContext, View, ViewContext, ViewHandle, +}; +use language::{Buffer, LanguageRegistry}; +use picker::{Picker, PickerDelegate}; +use project::Project; +use settings::Settings; +use std::sync::Arc; +use workspace::{AppState, Workspace}; + +actions!(language_selector, [Toggle]); + +pub fn init(app_state: Arc, cx: &mut MutableAppContext) { + Picker::::init(cx); + cx.add_action({ + let language_registry = app_state.languages.clone(); + move |workspace, _: &Toggle, cx| { + LanguageSelector::toggle(workspace, language_registry.clone(), cx) + } + }); +} + +pub enum Event { + Dismissed, +} + +pub struct LanguageSelector { + buffer: ModelHandle, + project: ModelHandle, + language_registry: Arc, + candidates: Vec, + matches: Vec, + picker: ViewHandle>, + selected_index: usize, +} + +impl LanguageSelector { + fn new( + buffer: ModelHandle, + project: ModelHandle, + language_registry: Arc, + cx: &mut ViewContext, + ) -> Self { + let handle = cx.weak_handle(); + let picker = cx.add_view(|cx| Picker::new("Select Language...", handle, cx)); + + let candidates = language_registry + .language_names() + .into_iter() + .enumerate() + .map(|(candidate_id, name)| StringMatchCandidate::new(candidate_id, name)) + .collect::>(); + let mut matches = candidates + .iter() + .map(|candidate| StringMatch { + candidate_id: candidate.id, + score: 0., + positions: Default::default(), + string: candidate.string.clone(), + }) + .collect::>(); + matches.sort_unstable_by(|mat1, mat2| mat1.string.cmp(&mat2.string)); + + Self { + buffer, + project, + language_registry, + candidates, + matches, + picker, + selected_index: 0, + } + } + + fn toggle( + workspace: &mut Workspace, + registry: Arc, + cx: &mut ViewContext, + ) { + if let Some((_, buffer, _)) = workspace + .active_item(cx) + .and_then(|active_item| active_item.act_as::(cx)) + .and_then(|editor| editor.read(cx).active_excerpt(cx)) + { + workspace.toggle_modal(cx, |workspace, cx| { + let project = workspace.project().clone(); + let this = cx.add_view(|cx| Self::new(buffer, project, registry, cx)); + cx.subscribe(&this, Self::on_event).detach(); + this + }); + } + } + + fn on_event( + workspace: &mut Workspace, + _: ViewHandle, + event: &Event, + cx: &mut ViewContext, + ) { + match event { + Event::Dismissed => { + workspace.dismiss_modal(cx); + } + } + } +} + +impl Entity for LanguageSelector { + type Event = Event; +} + +impl View for LanguageSelector { + fn ui_name() -> &'static str { + "LanguageSelector" + } + + fn render(&mut self, cx: &mut RenderContext) -> ElementBox { + ChildView::new(self.picker.clone(), cx).boxed() + } + + fn focus_in(&mut self, _: AnyViewHandle, cx: &mut ViewContext) { + if cx.is_self_focused() { + cx.focus(&self.picker); + } + } +} + +impl PickerDelegate for LanguageSelector { + fn match_count(&self) -> usize { + self.matches.len() + } + + fn confirm(&mut self, cx: &mut ViewContext) { + if let Some(mat) = self.matches.get(self.selected_index) { + let language_name = &self.candidates[mat.candidate_id].string; + let language = self.language_registry.language_for_name(language_name); + cx.spawn(|this, mut cx| async move { + let language = language.await?; + this.update(&mut cx, |this, cx| { + this.project.update(cx, |project, cx| { + project.set_language_for_buffer(&this.buffer, language, cx); + }); + }); + anyhow::Ok(()) + }) + .detach_and_log_err(cx); + } + + cx.emit(Event::Dismissed); + } + + fn dismiss(&mut self, cx: &mut ViewContext) { + cx.emit(Event::Dismissed); + } + + fn selected_index(&self) -> usize { + self.selected_index + } + + fn set_selected_index(&mut self, ix: usize, _: &mut ViewContext) { + self.selected_index = ix; + } + + fn update_matches(&mut self, query: String, cx: &mut ViewContext) -> gpui::Task<()> { + let background = cx.background().clone(); + let candidates = self.candidates.clone(); + cx.spawn(|this, mut cx| async move { + let matches = if query.is_empty() { + candidates + .into_iter() + .enumerate() + .map(|(index, candidate)| StringMatch { + candidate_id: index, + string: candidate.string, + positions: Vec::new(), + score: 0.0, + }) + .collect() + } else { + match_strings( + &candidates, + &query, + false, + 100, + &Default::default(), + background, + ) + .await + }; + + this.update(&mut cx, |this, cx| { + this.matches = matches; + this.selected_index = this + .selected_index + .min(this.matches.len().saturating_sub(1)); + cx.notify(); + }); + }) + } + + fn render_match( + &self, + ix: usize, + mouse_state: &mut MouseState, + selected: bool, + cx: &AppContext, + ) -> ElementBox { + let settings = cx.global::(); + let theme = &settings.theme; + let mat = &self.matches[ix]; + let style = theme.picker.item.style_for(mouse_state, selected); + let buffer_language_name = self.buffer.read(cx).language().map(|l| l.name()); + let mut label = mat.string.clone(); + if buffer_language_name.as_deref() == Some(mat.string.as_str()) { + label.push_str(" (current)"); + } + + Label::new(label, style.label.clone()) + .with_highlights(mat.positions.clone()) + .contained() + .with_style(style.container) + .boxed() + } +} diff --git a/crates/lsp/src/lsp.rs b/crates/lsp/src/lsp.rs index 6982445982fe0383460b79983e60a35c693d4705..81568b9e3e449b4a922d6c7e64dfe0df309fbb6c 100644 --- a/crates/lsp/src/lsp.rs +++ b/crates/lsp/src/lsp.rs @@ -160,15 +160,13 @@ impl LanguageServer { server: Option, root_path: &Path, cx: AsyncAppContext, - mut on_unhandled_notification: F, + on_unhandled_notification: F, ) -> Self where Stdin: AsyncWrite + Unpin + Send + 'static, Stdout: AsyncRead + Unpin + Send + 'static, F: FnMut(AnyNotification) + 'static + Send, { - let mut stdin = BufWriter::new(stdin); - let mut stdout = BufReader::new(stdout); let (outbound_tx, outbound_rx) = channel::unbounded::>(); let notification_handlers = Arc::new(Mutex::new(HashMap::<_, NotificationHandler>::default())); @@ -177,89 +175,19 @@ impl LanguageServer { let input_task = cx.spawn(|cx| { let notification_handlers = notification_handlers.clone(); let response_handlers = response_handlers.clone(); - async move { - let _clear_response_handlers = util::defer({ - let response_handlers = response_handlers.clone(); - move || { - response_handlers.lock().take(); - } - }); - let mut buffer = Vec::new(); - loop { - buffer.clear(); - stdout.read_until(b'\n', &mut buffer).await?; - stdout.read_until(b'\n', &mut buffer).await?; - let message_len: usize = std::str::from_utf8(&buffer)? - .strip_prefix(CONTENT_LEN_HEADER) - .ok_or_else(|| anyhow!("invalid header"))? - .trim_end() - .parse()?; - - buffer.resize(message_len, 0); - stdout.read_exact(&mut buffer).await?; - log::trace!("incoming message:{}", String::from_utf8_lossy(&buffer)); - - if let Ok(msg) = serde_json::from_slice::(&buffer) { - if let Some(handler) = notification_handlers.lock().get_mut(msg.method) { - handler(msg.id, msg.params.get(), cx.clone()); - } else { - on_unhandled_notification(msg); - } - } else if let Ok(AnyResponse { - id, error, result, .. - }) = serde_json::from_slice(&buffer) - { - if let Some(handler) = response_handlers - .lock() - .as_mut() - .and_then(|handlers| handlers.remove(&id)) - { - if let Some(error) = error { - handler(Err(error)); - } else if let Some(result) = result { - handler(Ok(result.get())); - } else { - handler(Ok("null")); - } - } - } else { - warn!( - "Failed to deserialize message:\n{}", - std::str::from_utf8(&buffer)? - ); - } - - // Don't starve the main thread when receiving lots of messages at once. - smol::future::yield_now().await; - } - } + Self::handle_input( + stdout, + on_unhandled_notification, + notification_handlers, + response_handlers, + cx, + ) .log_err() }); let (output_done_tx, output_done_rx) = barrier::channel(); let output_task = cx.background().spawn({ let response_handlers = response_handlers.clone(); - async move { - let _clear_response_handlers = util::defer({ - let response_handlers = response_handlers.clone(); - move || { - response_handlers.lock().take(); - } - }); - let mut content_len_buffer = Vec::new(); - while let Ok(message) = outbound_rx.recv().await { - log::trace!("outgoing message:{}", String::from_utf8_lossy(&message)); - content_len_buffer.clear(); - write!(content_len_buffer, "{}", message.len()).unwrap(); - stdin.write_all(CONTENT_LEN_HEADER.as_bytes()).await?; - stdin.write_all(&content_len_buffer).await?; - stdin.write_all("\r\n\r\n".as_bytes()).await?; - stdin.write_all(&message).await?; - stdin.flush().await?; - } - drop(output_done_tx); - Ok(()) - } - .log_err() + Self::handle_output(stdin, outbound_rx, output_done_tx, response_handlers).log_err() }); Self { @@ -278,6 +206,105 @@ impl LanguageServer { } } + async fn handle_input( + stdout: Stdout, + mut on_unhandled_notification: F, + notification_handlers: Arc>>, + response_handlers: Arc>>>, + cx: AsyncAppContext, + ) -> anyhow::Result<()> + where + Stdout: AsyncRead + Unpin + Send + 'static, + F: FnMut(AnyNotification) + 'static + Send, + { + let mut stdout = BufReader::new(stdout); + let _clear_response_handlers = util::defer({ + let response_handlers = response_handlers.clone(); + move || { + response_handlers.lock().take(); + } + }); + let mut buffer = Vec::new(); + loop { + buffer.clear(); + stdout.read_until(b'\n', &mut buffer).await?; + stdout.read_until(b'\n', &mut buffer).await?; + let message_len: usize = std::str::from_utf8(&buffer)? + .strip_prefix(CONTENT_LEN_HEADER) + .ok_or_else(|| anyhow!("invalid header"))? + .trim_end() + .parse()?; + + buffer.resize(message_len, 0); + stdout.read_exact(&mut buffer).await?; + log::trace!("incoming message:{}", String::from_utf8_lossy(&buffer)); + + if let Ok(msg) = serde_json::from_slice::(&buffer) { + if let Some(handler) = notification_handlers.lock().get_mut(msg.method) { + handler(msg.id, msg.params.get(), cx.clone()); + } else { + on_unhandled_notification(msg); + } + } else if let Ok(AnyResponse { + id, error, result, .. + }) = serde_json::from_slice(&buffer) + { + if let Some(handler) = response_handlers + .lock() + .as_mut() + .and_then(|handlers| handlers.remove(&id)) + { + if let Some(error) = error { + handler(Err(error)); + } else if let Some(result) = result { + handler(Ok(result.get())); + } else { + handler(Ok("null")); + } + } + } else { + warn!( + "Failed to deserialize message:\n{}", + std::str::from_utf8(&buffer)? + ); + } + + // Don't starve the main thread when receiving lots of messages at once. + smol::future::yield_now().await; + } + } + + async fn handle_output( + stdin: Stdin, + outbound_rx: channel::Receiver>, + output_done_tx: barrier::Sender, + response_handlers: Arc>>>, + ) -> anyhow::Result<()> + where + Stdin: AsyncWrite + Unpin + Send + 'static, + { + let mut stdin = BufWriter::new(stdin); + let _clear_response_handlers = util::defer({ + let response_handlers = response_handlers.clone(); + move || { + response_handlers.lock().take(); + } + }); + let mut content_len_buffer = Vec::new(); + while let Ok(message) = outbound_rx.recv().await { + log::trace!("outgoing message:{}", String::from_utf8_lossy(&message)); + content_len_buffer.clear(); + write!(content_len_buffer, "{}", message.len()).unwrap(); + stdin.write_all(CONTENT_LEN_HEADER.as_bytes()).await?; + stdin.write_all(&content_len_buffer).await?; + stdin.write_all("\r\n\r\n".as_bytes()).await?; + stdin.write_all(&message).await?; + stdin.flush().await?; + } + drop(output_done_tx); + Ok(()) + } + /// Initializes a language server. /// Note that `options` is used directly to construct [`InitializeParams`], /// which is why it is owned. @@ -389,7 +416,7 @@ impl LanguageServer { output_done.recv().await; log::debug!("language server shutdown finished"); drop(tasks); - Ok(()) + anyhow::Ok(()) } .log_err(), ) diff --git a/crates/picker/src/picker.rs b/crates/picker/src/picker.rs index e4d062d575aa6a44909d0a757c766470987abb87..fb4b51a1638f3556d9567d88cb598d5364ba788c 100644 --- a/crates/picker/src/picker.rs +++ b/crates/picker/src/picker.rs @@ -102,7 +102,10 @@ impl View for Picker { .read(cx) .render_match(ix, state, ix == selected_ix, cx) }) - .on_down(MouseButton::Left, move |_, cx| { + // Capture mouse events + .on_down(MouseButton::Left, |_, _| {}) + .on_up(MouseButton::Left, |_, _| {}) + .on_click(MouseButton::Left, move |_, cx| { cx.dispatch_action(SelectIndex(ix)) }) .with_cursor_style(CursorStyle::PointingHand) @@ -205,6 +208,11 @@ impl Picker { self.query_editor.read(cx).text(cx) } + pub fn set_query(&self, query: impl Into>, cx: &mut ViewContext) { + self.query_editor + .update(cx, |editor, cx| editor.set_text(query, cx)); + } + fn on_query_editor_event( &mut self, _: ViewHandle, diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 2ebea8d07dad811a378a065a7f666caaf594c6d3..a164b5b885f927737efba79a5693ff22aa1500aa 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -1464,7 +1464,7 @@ impl Project { }) .await?; this.update(&mut cx, |this, cx| { - this.assign_language_to_buffer(&buffer, cx); + this.detect_language_for_buffer(&buffer, cx); this.register_buffer_with_language_server(&buffer, cx); }); Ok(()) @@ -1531,7 +1531,7 @@ impl Project { }) .detach(); - self.assign_language_to_buffer(buffer, cx); + self.detect_language_for_buffer(buffer, cx); self.register_buffer_with_language_server(buffer, cx); cx.observe_release(buffer, |this, buffer, cx| { if let Some(file) = File::from_dyn(buffer.file()) { @@ -1818,7 +1818,7 @@ impl Project { } for buffer in plain_text_buffers { - project.assign_language_to_buffer(&buffer, cx); + project.detect_language_for_buffer(&buffer, cx); project.register_buffer_with_language_server(&buffer, cx); } @@ -1831,14 +1831,28 @@ impl Project { }) } - fn assign_language_to_buffer( + fn detect_language_for_buffer( &mut self, buffer: &ModelHandle, cx: &mut ModelContext, ) -> Option<()> { // If the buffer has a language, set it and start the language server if we haven't already. let full_path = buffer.read(cx).file()?.full_path(cx); - let new_language = self.languages.language_for_path(&full_path)?; + let new_language = self + .languages + .language_for_path(&full_path) + .now_or_never()? + .ok()?; + self.set_language_for_buffer(buffer, new_language, cx); + None + } + + pub fn set_language_for_buffer( + &mut self, + buffer: &ModelHandle, + new_language: Arc, + cx: &mut ModelContext, + ) { buffer.update(cx, |buffer, cx| { if buffer.language().map_or(true, |old_language| { !Arc::ptr_eq(old_language, &new_language) @@ -1847,13 +1861,13 @@ impl Project { } }); - let file = File::from_dyn(buffer.read(cx).file())?; - let worktree = file.worktree.read(cx).as_local()?; - let worktree_id = worktree.id(); - let worktree_abs_path = worktree.abs_path().clone(); - self.start_language_server(worktree_id, worktree_abs_path, new_language, cx); - - None + if let Some(file) = File::from_dyn(buffer.read(cx).file()) { + if let Some(worktree) = file.worktree.read(cx).as_local() { + let worktree_id = worktree.id(); + let worktree_abs_path = worktree.abs_path().clone(); + self.start_language_server(worktree_id, worktree_abs_path, new_language, cx); + } + } } fn merge_json_value_into(source: serde_json::Value, target: &mut serde_json::Value) { @@ -2248,8 +2262,14 @@ impl Project { }) .collect(); for (worktree_id, worktree_abs_path, full_path) in language_server_lookup_info { - let language = self.languages.language_for_path(&full_path)?; - self.restart_language_server(worktree_id, worktree_abs_path, language, cx); + if let Some(language) = self + .languages + .language_for_path(&full_path) + .now_or_never() + .and_then(|language| language.ok()) + { + self.restart_language_server(worktree_id, worktree_abs_path, language, cx); + } } None @@ -3278,12 +3298,14 @@ impl Project { path: path.into(), }; let signature = this.symbol_signature(&project_path); + let adapter_language = adapter_language.clone(); let language = this .languages .language_for_path(&project_path.path) - .unwrap_or(adapter_language.clone()); + .unwrap_or_else(move |_| adapter_language); let language_server_name = adapter.name.clone(); Some(async move { + let language = language.await; let label = language .label_for_symbol(&lsp_symbol.name, lsp_symbol.kind) .await; @@ -4541,7 +4563,7 @@ impl Project { for (buffer, old_path) in renamed_buffers { self.unregister_buffer_from_language_server(&buffer, old_path, cx); - self.assign_language_to_buffer(&buffer, cx); + self.detect_language_for_buffer(&buffer, cx); self.register_buffer_with_language_server(&buffer, cx); } } @@ -5210,7 +5232,7 @@ impl Project { buffer.update(cx, |buffer, cx| { buffer.file_updated(Arc::new(file), cx).detach(); }); - this.assign_language_to_buffer(&buffer, cx); + this.detect_language_for_buffer(&buffer, cx); } Ok(()) }) @@ -5831,7 +5853,7 @@ impl Project { })?; } - Ok(()) + anyhow::Ok(()) } .log_err(), ) @@ -6060,7 +6082,7 @@ impl Project { worktree_id, path: PathBuf::from(serialized_symbol.path).into(), }; - let language = languages.language_for_path(&path.path); + let language = languages.language_for_path(&path.path).await.log_err(); Ok(Symbol { language_server_name: LanguageServerName( serialized_symbol.language_server_name.into(), diff --git a/crates/project/src/worktree.rs b/crates/project/src/worktree.rs index 8b622ab607db49f6f5b54845ccf7d70397752225..59a98bdcfa9915046b964b19c398024d08ab7643 100644 --- a/crates/project/src/worktree.rs +++ b/crates/project/src/worktree.rs @@ -867,7 +867,7 @@ impl LocalWorktree { let old_path = self.entry_for_id(entry_id)?.path.clone(); let new_path = new_path.into(); let abs_old_path = self.absolutize(&old_path); - let abs_new_path = self.absolutize(&new_path); + let abs_new_path = self.absolutize(new_path.as_ref()); let rename = cx.background().spawn({ let fs = self.fs.clone(); let abs_new_path = abs_new_path.clone(); @@ -2361,7 +2361,7 @@ impl BackgroundScanner { job: &ScanJob, ) -> Result<()> { let mut new_entries: Vec = Vec::new(); - let mut new_jobs: Vec = Vec::new(); + let mut new_jobs: Vec> = Vec::new(); let mut ignore_stack = job.ignore_stack.clone(); let mut new_ignore = None; @@ -2374,6 +2374,7 @@ impl BackgroundScanner { continue; } }; + let child_name = child_abs_path.file_name().unwrap(); let child_path: Arc = job.path.join(child_name).into(); let child_metadata = match self.fs.metadata(&child_abs_path).await { @@ -2412,12 +2413,15 @@ impl BackgroundScanner { let entry_abs_path = self.abs_path().join(&entry.path); entry.is_ignored = ignore_stack.is_abs_path_ignored(&entry_abs_path, entry.is_dir()); + if entry.is_dir() { - new_jobs.next().unwrap().ignore_stack = if entry.is_ignored { - IgnoreStack::all() - } else { - ignore_stack.clone() - }; + if let Some(job) = new_jobs.next().expect("Missing scan job for entry") { + job.ignore_stack = if entry.is_ignored { + IgnoreStack::all() + } else { + ignore_stack.clone() + }; + } } } } @@ -2433,10 +2437,12 @@ impl BackgroundScanner { let is_ignored = ignore_stack.is_abs_path_ignored(&child_abs_path, true); child_entry.is_ignored = is_ignored; + // Avoid recursing until crash in the case of a recursive symlink if !job.ancestor_inodes.contains(&child_entry.inode) { let mut ancestor_inodes = job.ancestor_inodes.clone(); ancestor_inodes.insert(child_entry.inode); - new_jobs.push(ScanJob { + + new_jobs.push(Some(ScanJob { abs_path: child_abs_path, path: child_path, ignore_stack: if is_ignored { @@ -2446,7 +2452,9 @@ impl BackgroundScanner { }, ancestor_inodes, scan_queue: job.scan_queue.clone(), - }); + })); + } else { + new_jobs.push(None); } } else { child_entry.is_ignored = ignore_stack.is_abs_path_ignored(&child_abs_path, false); @@ -2461,8 +2469,11 @@ impl BackgroundScanner { new_ignore, self.fs.as_ref(), ); + for new_job in new_jobs { - job.scan_queue.send(new_job).await.unwrap(); + if let Some(new_job) = new_job { + job.scan_queue.send(new_job).await.unwrap(); + } } Ok(()) diff --git a/crates/project_panel/src/project_panel.rs b/crates/project_panel/src/project_panel.rs index 4b3c5b7bc53a452b8b6d11ec41e7e41b8228a7f3..079de79b1124b6064e7df9ca81e211147b2db1f0 100644 --- a/crates/project_panel/src/project_panel.rs +++ b/crates/project_panel/src/project_panel.rs @@ -1262,54 +1262,89 @@ impl View for ProjectPanel { let padding = std::mem::take(&mut container_style.padding); let last_worktree_root_id = self.last_worktree_root_id; - Stack::new() - .with_child( - MouseEventHandler::::new(0, cx, |_, cx| { - UniformList::new( - self.list.clone(), - self.visible_entries - .iter() - .map(|(_, worktree_entries)| worktree_entries.len()) - .sum(), - cx, - move |this, range, items, cx| { - let theme = cx.global::().theme.clone(); - let mut dragged_entry_destination = - this.dragged_entry_destination.clone(); - this.for_each_visible_entry(range, cx, |id, details, cx| { - items.push(Self::render_entry( - id, - details, - &this.filename_editor, - &mut dragged_entry_destination, - &theme.project_panel, - cx, - )); - }); - this.dragged_entry_destination = dragged_entry_destination; - }, - ) - .with_padding_top(padding.top) - .with_padding_bottom(padding.bottom) - .contained() - .with_style(container_style) - .expanded() - .boxed() - }) - .on_down(MouseButton::Right, move |e, cx| { - // When deploying the context menu anywhere below the last project entry, - // act as if the user clicked the root of the last worktree. - if let Some(entry_id) = last_worktree_root_id { - cx.dispatch_action(DeployContextMenu { - entry_id, - position: e.position, - }) - } - }) - .boxed(), - ) - .with_child(ChildView::new(&self.context_menu, cx).boxed()) - .boxed() + let has_worktree = self.visible_entries.len() != 0; + + if has_worktree { + Stack::new() + .with_child( + MouseEventHandler::::new(0, cx, |_, cx| { + UniformList::new( + self.list.clone(), + self.visible_entries + .iter() + .map(|(_, worktree_entries)| worktree_entries.len()) + .sum(), + cx, + move |this, range, items, cx| { + let theme = cx.global::().theme.clone(); + let mut dragged_entry_destination = + this.dragged_entry_destination.clone(); + this.for_each_visible_entry(range, cx, |id, details, cx| { + items.push(Self::render_entry( + id, + details, + &this.filename_editor, + &mut dragged_entry_destination, + &theme.project_panel, + cx, + )); + }); + this.dragged_entry_destination = dragged_entry_destination; + }, + ) + .with_padding_top(padding.top) + .with_padding_bottom(padding.bottom) + .contained() + .with_style(container_style) + .expanded() + .boxed() + }) + .on_down(MouseButton::Right, move |e, cx| { + // When deploying the context menu anywhere below the last project entry, + // act as if the user clicked the root of the last worktree. + if let Some(entry_id) = last_worktree_root_id { + cx.dispatch_action(DeployContextMenu { + entry_id, + position: e.position, + }) + } + }) + .boxed(), + ) + .with_child(ChildView::new(&self.context_menu, cx).boxed()) + .boxed() + } else { + Flex::column() + .with_child( + MouseEventHandler::::new(2, cx, { + let button_style = theme.open_project_button.clone(); + let context_menu_item_style = + cx.global::().theme.context_menu.item.clone(); + move |state, cx| { + let button_style = button_style.style_for(state, false).clone(); + let context_menu_item = + context_menu_item_style.style_for(state, true).clone(); + + theme::ui::keystroke_label( + "Open a project", + &button_style, + &context_menu_item.keystroke, + Box::new(workspace::Open), + cx, + ) + .boxed() + } + }) + .on_click(MouseButton::Left, move |_, cx| { + cx.dispatch_action(workspace::Open) + }) + .with_cursor_style(CursorStyle::PointingHand) + .boxed(), + ) + .contained() + .with_style(container_style) + .boxed() + } } fn keymap_context(&self, _: &AppContext) -> KeymapContext { @@ -1404,15 +1439,7 @@ mod tests { .await; let project = Project::test(fs.clone(), ["/root1".as_ref(), "/root2".as_ref()], cx).await; - let (_, workspace) = cx.add_window(|cx| { - Workspace::new( - Default::default(), - 0, - project.clone(), - |_, _| unimplemented!(), - cx, - ) - }); + let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx)); let panel = workspace.update(cx, |_, cx| ProjectPanel::new(project, cx)); assert_eq!( visible_entries_as_strings(&panel, 0..50, cx), @@ -1504,15 +1531,7 @@ mod tests { .await; let project = Project::test(fs.clone(), ["/root1".as_ref(), "/root2".as_ref()], cx).await; - let (_, workspace) = cx.add_window(|cx| { - Workspace::new( - Default::default(), - 0, - project.clone(), - |_, _| unimplemented!(), - cx, - ) - }); + let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx)); let panel = workspace.update(cx, |_, cx| ProjectPanel::new(project, cx)); select_path(&panel, "root1", cx); diff --git a/crates/rpc/src/peer.rs b/crates/rpc/src/peer.rs index 4740120fc4422c018042aa7564625a0ab900e760..0df87fd92d9ab92237ccefe98afe9b457cec9a49 100644 --- a/crates/rpc/src/peer.rs +++ b/crates/rpc/src/peer.rs @@ -114,7 +114,7 @@ pub struct ConnectionState { const KEEPALIVE_INTERVAL: Duration = Duration::from_secs(1); const WRITE_TIMEOUT: Duration = Duration::from_secs(2); -pub const RECEIVE_TIMEOUT: Duration = Duration::from_secs(5); +pub const RECEIVE_TIMEOUT: Duration = Duration::from_secs(10); impl Peer { pub fn new(epoch: u32) -> Arc { diff --git a/crates/settings/src/keymap_file.rs b/crates/settings/src/keymap_file.rs index 01992d94311e7dd348b06d3a7b6b042780204010..2235bc351da078cd53129171b31b963e658962cc 100644 --- a/crates/settings/src/keymap_file.rs +++ b/crates/settings/src/keymap_file.rs @@ -1,4 +1,4 @@ -use crate::parse_json_with_comments; +use crate::{parse_json_with_comments, Settings}; use anyhow::{Context, Result}; use assets::Assets; use collections::BTreeMap; @@ -45,6 +45,10 @@ impl KeymapFileContent { for path in ["keymaps/default.json", "keymaps/vim.json"] { Self::load(path, cx).unwrap(); } + + if let Some(asset_path) = cx.global::().base_keymap.asset_path() { + Self::load(asset_path, cx).log_err(); + } } pub fn load(asset_path: &str, cx: &mut MutableAppContext) -> Result<()> { diff --git a/crates/settings/src/settings.rs b/crates/settings/src/settings.rs index 6b51b06c9c0e4e3e8617c3bd26a58c7e902ed188..49f43e7a2d7b605703489fc292bbb5ad4107df67 100644 --- a/crates/settings/src/settings.rs +++ b/crates/settings/src/settings.rs @@ -18,12 +18,13 @@ use sqlez::{ bindable::{Bind, Column, StaticColumnCount}, statement::Statement, }; -use std::{collections::HashMap, fmt::Write as _, num::NonZeroU32, str, sync::Arc}; +use std::{collections::HashMap, num::NonZeroU32, str, sync::Arc}; use theme::{Theme, ThemeRegistry}; use tree_sitter::Query; use util::ResultExt as _; pub use keymap_file::{keymap_file_json_schema, KeymapFileContent}; +pub use watched_json::watch_files; #[derive(Clone)] pub struct Settings { @@ -54,6 +55,46 @@ pub struct Settings { pub telemetry_defaults: TelemetrySettings, pub telemetry_overrides: TelemetrySettings, pub auto_update: bool, + pub base_keymap: BaseKeymap, +} + +#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq, Default)] +pub enum BaseKeymap { + #[default] + VSCode, + JetBrains, + Sublime, + Atom, +} + +impl BaseKeymap { + pub const OPTIONS: [(&'static str, Self); 4] = [ + ("VSCode (Default)", Self::VSCode), + ("Atom", Self::Atom), + ("JetBrains", Self::JetBrains), + ("Sublime", Self::Sublime), + ]; + + pub fn asset_path(&self) -> Option<&'static str> { + match self { + BaseKeymap::JetBrains => Some("keymaps/jetbrains.json"), + BaseKeymap::Sublime => Some("keymaps/sublime_text.json"), + BaseKeymap::Atom => Some("keymaps/atom.json"), + BaseKeymap::VSCode => None, + } + } + + pub fn names() -> impl Iterator { + Self::OPTIONS.iter().map(|(name, _)| *name) + } + + pub fn from_names(option: &str) -> BaseKeymap { + Self::OPTIONS + .iter() + .copied() + .find_map(|(name, value)| (name == option).then(|| value)) + .unwrap_or_default() + } } #[derive(Copy, Clone, Debug, Default, Serialize, Deserialize, JsonSchema)] @@ -66,9 +107,18 @@ impl TelemetrySettings { pub fn metrics(&self) -> bool { self.metrics.unwrap() } + pub fn diagnostics(&self) -> bool { self.diagnostics.unwrap() } + + pub fn set_metrics(&mut self, value: bool) { + self.metrics = Some(value); + } + + pub fn set_diagnostics(&mut self, value: bool) { + self.diagnostics = Some(value); + } } #[derive(Copy, Clone, Debug, Default, Serialize, Deserialize, JsonSchema)] @@ -317,6 +367,8 @@ pub struct SettingsFileContent { pub telemetry: TelemetrySettings, #[serde(default)] pub auto_update: Option, + #[serde(default)] + pub base_keymap: Option, } #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] @@ -387,6 +439,7 @@ impl Settings { telemetry_defaults: defaults.telemetry, telemetry_overrides: Default::default(), auto_update: defaults.auto_update.unwrap(), + base_keymap: Default::default(), } } @@ -424,6 +477,7 @@ impl Settings { merge(&mut self.vim_mode, data.vim_mode); merge(&mut self.autosave, data.autosave); merge(&mut self.default_dock_anchor, data.default_dock_anchor); + merge(&mut self.base_keymap, data.base_keymap); // Ensure terminal font is loaded, so we can request it in terminal_element layout if let Some(terminal_font) = &data.terminal.font_family { @@ -601,6 +655,7 @@ impl Settings { }, telemetry_overrides: Default::default(), auto_update: true, + base_keymap: Default::default(), } } @@ -677,13 +732,19 @@ pub fn settings_file_json_schema( serde_json::to_value(root_schema).unwrap() } -/// Expects the key to be unquoted, and the value to be valid JSON -/// (e.g. values should be unquoted for numbers and bools, quoted for strings) -pub fn write_top_level_setting( - mut settings_content: String, - top_level_key: &str, - new_val: &str, -) -> String { +fn merge(target: &mut T, value: Option) { + if let Some(value) = value { + *target = value; + } +} + +pub fn parse_json_with_comments(content: &str) -> Result { + Ok(serde_json::from_reader( + json_comments::CommentSettings::c_style().strip_comments(content.as_bytes()), + )?) +} + +fn write_settings_key(settings_content: &mut String, key_path: &[&str], new_value: &Value) { let mut parser = tree_sitter::Parser::new(); parser.set_language(tree_sitter_json::language()).unwrap(); let tree = parser.parse(&settings_content, None).unwrap(); @@ -693,56 +754,64 @@ pub fn write_top_level_setting( let query = Query::new( tree_sitter_json::language(), " - (document - (object - (pair - key: (string) @key - value: (_) @value))) - ", + (pair + key: (string) @key + value: (_) @value) + ", ) .unwrap(); + let mut depth = 0; let mut first_key_start = None; - let mut existing_value_range = None; + let mut existing_value_range = 0..settings_content.len(); let matches = cursor.matches(&query, tree.root_node(), settings_content.as_bytes()); for mat in matches { if mat.captures.len() != 2 { continue; } - let key = mat.captures[0]; - let value = mat.captures[1]; + let key_range = mat.captures[0].node.byte_range(); + let value_range = mat.captures[1].node.byte_range(); + + if key_range.start > existing_value_range.end { + break; + } + + first_key_start.get_or_insert_with(|| key_range.start); - first_key_start.get_or_insert_with(|| key.node.start_byte()); + let found_key = settings_content + .get(key_range.clone()) + .map(|key_text| key_text == format!("\"{}\"", key_path[depth])) + .unwrap_or(false); - if let Some(key_text) = settings_content.get(key.node.byte_range()) { - if key_text == format!("\"{top_level_key}\"") { - existing_value_range = Some(value.node.byte_range()); + if found_key { + existing_value_range = value_range; + depth += 1; + + if depth == key_path.len() { break; + } else { + first_key_start = None; } } } - match (first_key_start, existing_value_range) { - (None, None) => { - // No document, create a new object and overwrite - settings_content.clear(); - write!( - settings_content, - "{{\n \"{}\": {new_val}\n}}\n", - top_level_key - ) - .unwrap(); - } - - (_, Some(existing_value_range)) => { - // Existing theme key, overwrite - settings_content.replace_range(existing_value_range, &new_val); + // We found the exact key we want, insert the new value + if depth == key_path.len() { + let new_val = serde_json::to_string_pretty(new_value) + .expect("Could not serialize new json field to string"); + settings_content.replace_range(existing_value_range, &new_val); + } else { + // We have key paths, construct the sub objects + let new_key = key_path[depth]; + + // We don't have the key, construct the nested objects + let mut new_value = serde_json::to_value(new_value).unwrap(); + for key in key_path[(depth + 1)..].iter().rev() { + new_value = serde_json::json!({ key.to_string(): new_value }); } - (Some(first_key_start), None) => { - // No existing theme key, but other settings. Prepend new theme settings and - // match style of first key + if let Some(first_key_start) = first_key_start { let mut row = 0; let mut column = 0; for (ix, char) in settings_content.char_indices() { @@ -757,142 +826,374 @@ pub fn write_top_level_setting( } } - let content = format!(r#""{top_level_key}": {new_val},"#); - settings_content.insert_str(first_key_start, &content); - if row > 0 { + let new_val = to_pretty_json(&new_value, column, column); + let content = format!(r#""{new_key}": {new_val},"#); + settings_content.insert_str(first_key_start, &content); + settings_content.insert_str( first_key_start + content.len(), &format!("\n{:width$}", ' ', width = column), ) } else { - settings_content.insert_str(first_key_start + content.len(), " ") + let new_val = serde_json::to_string(&new_value).unwrap(); + let mut content = format!(r#""{new_key}": {new_val},"#); + content.push(' '); + settings_content.insert_str(first_key_start, &content); + } + } else { + new_value = serde_json::json!({ new_key.to_string(): new_value }); + let indent_prefix_len = 4 * depth; + let new_val = to_pretty_json(&new_value, 4, indent_prefix_len); + + settings_content.replace_range(existing_value_range, &new_val); + if depth == 0 { + settings_content.push('\n'); } } } +} + +fn to_pretty_json( + value: &serde_json::Value, + indent_size: usize, + indent_prefix_len: usize, +) -> String { + const SPACES: [u8; 32] = [b' '; 32]; + + debug_assert!(indent_size <= SPACES.len()); + debug_assert!(indent_prefix_len <= SPACES.len()); - settings_content + let mut output = Vec::new(); + let mut ser = serde_json::Serializer::with_formatter( + &mut output, + serde_json::ser::PrettyFormatter::with_indent(&SPACES[0..indent_size.min(SPACES.len())]), + ); + + value.serialize(&mut ser).unwrap(); + let text = String::from_utf8(output).unwrap(); + + let mut adjusted_text = String::new(); + for (i, line) in text.split('\n').enumerate() { + if i > 0 { + adjusted_text.push_str(str::from_utf8(&SPACES[0..indent_prefix_len]).unwrap()); + } + adjusted_text.push_str(line); + adjusted_text.push('\n'); + } + adjusted_text.pop(); + adjusted_text } -fn merge(target: &mut T, value: Option) { - if let Some(value) = value { - *target = value; +pub fn update_settings_file( + mut text: String, + old_file_content: SettingsFileContent, + update: impl FnOnce(&mut SettingsFileContent), +) -> String { + let mut new_file_content = old_file_content.clone(); + + update(&mut new_file_content); + + let old_object = to_json_object(old_file_content); + let new_object = to_json_object(new_file_content); + + fn apply_changes_to_json_text( + old_object: &serde_json::Map, + new_object: &serde_json::Map, + current_key_path: Vec<&str>, + json_text: &mut String, + ) { + for (key, old_value) in old_object.iter() { + // We know that these two are from the same shape of object, so we can just unwrap + let new_value = new_object.get(key).unwrap(); + if old_value != new_value { + match new_value { + Value::Bool(_) | Value::Number(_) | Value::String(_) => { + let mut key_path = current_key_path.clone(); + key_path.push(key); + write_settings_key(json_text, &key_path, &new_value); + } + Value::Object(new_sub_object) => { + let mut key_path = current_key_path.clone(); + key_path.push(key); + if let Value::Object(old_sub_object) = old_value { + apply_changes_to_json_text( + old_sub_object, + new_sub_object, + key_path, + json_text, + ); + } else { + unimplemented!("This function doesn't support changing values from simple values to objects yet"); + } + } + Value::Null | Value::Array(_) => { + unimplemented!("We only support objects and simple values"); + } + } + } + } } + + apply_changes_to_json_text(&old_object, &new_object, vec![], &mut text); + + text } -pub fn parse_json_with_comments(content: &str) -> Result { - Ok(serde_json::from_reader( - json_comments::CommentSettings::c_style().strip_comments(content.as_bytes()), - )?) +fn to_json_object(settings_file: SettingsFileContent) -> serde_json::Map { + let tmp = serde_json::to_value(settings_file).unwrap(); + match tmp { + Value::Object(map) => map, + _ => unreachable!("SettingsFileContent represents a JSON map"), + } } #[cfg(test)] mod tests { - use crate::write_top_level_setting; + use super::*; use unindent::Unindent; - #[test] - fn test_write_theme_into_settings_with_theme() { - let settings = r#" - { - "theme": "One Dark" - } - "# - .unindent(); - - let new_settings = r#" - { - "theme": "summerfruit-light" - } - "# - .unindent(); + fn assert_new_settings, S2: Into>( + old_json: S1, + update: fn(&mut SettingsFileContent), + expected_new_json: S2, + ) { + let old_json = old_json.into(); + let old_content: SettingsFileContent = serde_json::from_str(&old_json).unwrap_or_default(); + let new_json = update_settings_file(old_json, old_content, update); + assert_eq!(new_json, expected_new_json.into()); + } - let settings_after_theme = - write_top_level_setting(settings, "theme", "\"summerfruit-light\""); + #[test] + fn test_update_telemetry_setting_multiple_fields() { + assert_new_settings( + r#" + { + "telemetry": { + "metrics": false, + "diagnostics": false + } + } + "# + .unindent(), + |settings| { + settings.telemetry.set_diagnostics(true); + settings.telemetry.set_metrics(true); + }, + r#" + { + "telemetry": { + "metrics": true, + "diagnostics": true + } + } + "# + .unindent(), + ); + } - assert_eq!(settings_after_theme, new_settings) + #[test] + fn test_update_telemetry_setting_weird_formatting() { + assert_new_settings( + r#"{ + "telemetry": { "metrics": false, "diagnostics": true } + }"# + .unindent(), + |settings| settings.telemetry.set_diagnostics(false), + r#"{ + "telemetry": { "metrics": false, "diagnostics": false } + }"# + .unindent(), + ); } #[test] - fn test_write_theme_into_empty_settings() { - let settings = r#" - { - } - "# - .unindent(); + fn test_update_telemetry_setting_other_fields() { + assert_new_settings( + r#" + { + "telemetry": { + "metrics": false, + "diagnostics": true + } + } + "# + .unindent(), + |settings| settings.telemetry.set_diagnostics(false), + r#" + { + "telemetry": { + "metrics": false, + "diagnostics": false + } + } + "# + .unindent(), + ); + } - let new_settings = r#" - { - "theme": "summerfruit-light" - } - "# - .unindent(); + #[test] + fn test_update_telemetry_setting_empty_telemetry() { + assert_new_settings( + r#" + { + "telemetry": {} + } + "# + .unindent(), + |settings| settings.telemetry.set_diagnostics(false), + r#" + { + "telemetry": { + "diagnostics": false + } + } + "# + .unindent(), + ); + } - let settings_after_theme = - write_top_level_setting(settings, "theme", "\"summerfruit-light\""); + #[test] + fn test_update_telemetry_setting_pre_existing() { + assert_new_settings( + r#" + { + "telemetry": { + "diagnostics": true + } + } + "# + .unindent(), + |settings| settings.telemetry.set_diagnostics(false), + r#" + { + "telemetry": { + "diagnostics": false + } + } + "# + .unindent(), + ); + } - assert_eq!(settings_after_theme, new_settings) + #[test] + fn test_update_telemetry_setting() { + assert_new_settings( + "{}", + |settings| settings.telemetry.set_diagnostics(true), + r#" + { + "telemetry": { + "diagnostics": true + } + } + "# + .unindent(), + ); } #[test] - fn test_write_theme_into_no_settings() { - let settings = "".to_string(); + fn test_update_object_empty_doc() { + assert_new_settings( + "", + |settings| settings.telemetry.set_diagnostics(true), + r#" + { + "telemetry": { + "diagnostics": true + } + } + "# + .unindent(), + ); + } - let new_settings = r#" - { - "theme": "summerfruit-light" - } - "# - .unindent(); + #[test] + fn test_write_theme_into_settings_with_theme() { + assert_new_settings( + r#" + { + "theme": "One Dark" + } + "# + .unindent(), + |settings| settings.theme = Some("summerfruit-light".to_string()), + r#" + { + "theme": "summerfruit-light" + } + "# + .unindent(), + ); + } - let settings_after_theme = - write_top_level_setting(settings, "theme", "\"summerfruit-light\""); + #[test] + fn test_write_theme_into_empty_settings() { + assert_new_settings( + r#" + { + } + "# + .unindent(), + |settings| settings.theme = Some("summerfruit-light".to_string()), + r#" + { + "theme": "summerfruit-light" + } + "# + .unindent(), + ); + } - assert_eq!(settings_after_theme, new_settings) + #[test] + fn write_key_no_document() { + assert_new_settings( + "", + |settings| settings.theme = Some("summerfruit-light".to_string()), + r#" + { + "theme": "summerfruit-light" + } + "# + .unindent(), + ); } #[test] fn test_write_theme_into_single_line_settings_without_theme() { - let settings = r#"{ "a": "", "ok": true }"#.to_string(); - let new_settings = r#"{ "theme": "summerfruit-light", "a": "", "ok": true }"#; - - let settings_after_theme = - write_top_level_setting(settings, "theme", "\"summerfruit-light\""); - - assert_eq!(settings_after_theme, new_settings) + assert_new_settings( + r#"{ "a": "", "ok": true }"#, + |settings| settings.theme = Some("summerfruit-light".to_string()), + r#"{ "theme": "summerfruit-light", "a": "", "ok": true }"#, + ); } #[test] fn test_write_theme_pre_object_whitespace() { - let settings = r#" { "a": "", "ok": true }"#.to_string(); - let new_settings = r#" { "theme": "summerfruit-light", "a": "", "ok": true }"#; - - let settings_after_theme = - write_top_level_setting(settings, "theme", "\"summerfruit-light\""); - - assert_eq!(settings_after_theme, new_settings) + assert_new_settings( + r#" { "a": "", "ok": true }"#, + |settings| settings.theme = Some("summerfruit-light".to_string()), + r#" { "theme": "summerfruit-light", "a": "", "ok": true }"#.unindent(), + ); } #[test] fn test_write_theme_into_multi_line_settings_without_theme() { - let settings = r#" - { - "a": "b" - } - "# - .unindent(); - - let new_settings = r#" - { - "theme": "summerfruit-light", - "a": "b" - } - "# - .unindent(); - - let settings_after_theme = - write_top_level_setting(settings, "theme", "\"summerfruit-light\""); - - assert_eq!(settings_after_theme, new_settings) + assert_new_settings( + r#" + { + "a": "b" + } + "# + .unindent(), + |settings| settings.theme = Some("summerfruit-light".to_string()), + r#" + { + "theme": "summerfruit-light", + "a": "b" + } + "# + .unindent(), + ); } } diff --git a/crates/settings/src/settings_file.rs b/crates/settings/src/settings_file.rs index 506ebc8c3ddea5eafa35259b77b7e000be737584..50638205c519f5f882f963baeeb45c943e2895e6 100644 --- a/crates/settings/src/settings_file.rs +++ b/crates/settings/src/settings_file.rs @@ -1,8 +1,7 @@ -use crate::{watched_json::WatchedJsonFile, write_top_level_setting, SettingsFileContent}; +use crate::{update_settings_file, watched_json::WatchedJsonFile, SettingsFileContent}; use anyhow::Result; use fs::Fs; use gpui::MutableAppContext; -use serde_json::Value; use std::{path::Path, sync::Arc}; // TODO: Switch SettingsFile to open a worktree and buffer for synchronization @@ -27,57 +26,24 @@ impl SettingsFile { } } - pub fn update(cx: &mut MutableAppContext, update: impl FnOnce(&mut SettingsFileContent)) { + pub fn update( + cx: &mut MutableAppContext, + update: impl 'static + Send + FnOnce(&mut SettingsFileContent), + ) { let this = cx.global::(); let current_file_content = this.settings_file_content.current(); - let mut new_file_content = current_file_content.clone(); - - update(&mut new_file_content); let fs = this.fs.clone(); let path = this.path.clone(); cx.background() .spawn(async move { - // Unwrap safety: These values are all guarnteed to be well formed, and we know - // that they will deserialize to our settings object. All of the following unwraps - // are therefore safe. - let tmp = serde_json::to_value(current_file_content).unwrap(); - let old_json = tmp.as_object().unwrap(); - - let new_tmp = serde_json::to_value(new_file_content).unwrap(); - let new_json = new_tmp.as_object().unwrap(); - - // Find changed fields - let mut diffs = vec![]; - for (key, old_value) in old_json.iter() { - let new_value = new_json.get(key).unwrap(); - if old_value != new_value { - if matches!( - new_value, - &Value::Null | &Value::Object(_) | &Value::Array(_) - ) { - unimplemented!( - "We only support updating basic values at the top level" - ); - } - - let new_json = serde_json::to_string_pretty(new_value) - .expect("Could not serialize new json field to string"); - - diffs.push((key, new_json)); - } - } + let old_text = fs.load(path).await?; - // Have diffs, rewrite the settings file now. - let mut content = fs.load(path).await?; + let new_text = update_settings_file(old_text, current_file_content, update); - for (key, new_value) in diffs { - content = write_top_level_setting(content, key, &new_value) - } - - fs.atomic_write(path.to_path_buf(), content).await?; + fs.atomic_write(path.to_path_buf(), new_text).await?; Ok(()) as Result<()> }) @@ -88,10 +54,164 @@ impl SettingsFile { #[cfg(test)] mod tests { use super::*; - use crate::{watched_json::watch_settings_file, EditorSettings, Settings, SoftWrap}; + use crate::{ + watch_files, watched_json::watch_settings_file, EditorSettings, Settings, SoftWrap, + }; use fs::FakeFs; + use gpui::{actions, Action}; use theme::ThemeRegistry; + #[gpui::test] + async fn test_base_keymap(cx: &mut gpui::TestAppContext) { + let executor = cx.background(); + let fs = FakeFs::new(executor.clone()); + let font_cache = cx.font_cache(); + + actions!(test, [A, B]); + // From the Atom keymap + actions!(workspace, [ActivatePreviousPane]); + // From the JetBrains keymap + actions!(pane, [ActivatePrevItem]); + + fs.save( + "/settings.json".as_ref(), + &r#" + { + "base_keymap": "Atom" + } + "# + .into(), + Default::default(), + ) + .await + .unwrap(); + + fs.save( + "/keymap.json".as_ref(), + &r#" + [ + { + "bindings": { + "backspace": "test::A" + } + } + ] + "# + .into(), + Default::default(), + ) + .await + .unwrap(); + + let settings_file = + WatchedJsonFile::new(fs.clone(), &executor, "/settings.json".as_ref()).await; + let keymaps_file = + WatchedJsonFile::new(fs.clone(), &executor, "/keymap.json".as_ref()).await; + + let default_settings = cx.read(Settings::test); + + cx.update(|cx| { + cx.add_global_action(|_: &A, _cx| {}); + cx.add_global_action(|_: &B, _cx| {}); + cx.add_global_action(|_: &ActivatePreviousPane, _cx| {}); + cx.add_global_action(|_: &ActivatePrevItem, _cx| {}); + watch_files( + default_settings, + settings_file, + ThemeRegistry::new((), font_cache), + keymaps_file, + cx, + ) + }); + + cx.foreground().run_until_parked(); + + // Test loading the keymap base at all + cx.update(|cx| { + assert_keybindings_for( + cx, + vec![("backspace", &A), ("k", &ActivatePreviousPane)], + line!(), + ); + }); + + // Test modifying the users keymap, while retaining the base keymap + fs.save( + "/keymap.json".as_ref(), + &r#" + [ + { + "bindings": { + "backspace": "test::B" + } + } + ] + "# + .into(), + Default::default(), + ) + .await + .unwrap(); + + cx.foreground().run_until_parked(); + + cx.update(|cx| { + assert_keybindings_for( + cx, + vec![("backspace", &B), ("k", &ActivatePreviousPane)], + line!(), + ); + }); + + // Test modifying the base, while retaining the users keymap + fs.save( + "/settings.json".as_ref(), + &r#" + { + "base_keymap": "JetBrains" + } + "# + .into(), + Default::default(), + ) + .await + .unwrap(); + + cx.foreground().run_until_parked(); + + cx.update(|cx| { + assert_keybindings_for( + cx, + vec![("backspace", &B), ("[", &ActivatePrevItem)], + line!(), + ); + }); + } + + fn assert_keybindings_for<'a>( + cx: &mut MutableAppContext, + actions: Vec<(&'static str, &'a dyn Action)>, + line: u32, + ) { + for (key, action) in actions { + // assert that... + assert!( + cx.available_actions(0, 0).any(|(_, bound_action, b)| { + // action names match... + bound_action.name() == action.name() + && bound_action.namespace() == action.namespace() + // and key strokes contain the given key + && b.iter() + .any(|binding| binding.keystrokes().iter().any(|k| k.key == key)) + }), + "On {} Failed to find {} with keybinding {}", + line, + action.name(), + key + ); + } + } + #[gpui::test] async fn test_watch_settings_files(cx: &mut gpui::TestAppContext) { let executor = cx.background(); diff --git a/crates/settings/src/watched_json.rs b/crates/settings/src/watched_json.rs index e304842aa2127cba2b3d25f533b9e7c19c71a8d9..c4cc64cd620542548d3d17ea830d5f84db63b8cf 100644 --- a/crates/settings/src/watched_json.rs +++ b/crates/settings/src/watched_json.rs @@ -62,7 +62,18 @@ where } } -pub fn watch_settings_file( +pub fn watch_files( + defaults: Settings, + settings_file: WatchedJsonFile, + theme_registry: Arc, + keymap_file: WatchedJsonFile, + cx: &mut MutableAppContext, +) { + watch_settings_file(defaults, settings_file, theme_registry, cx); + watch_keymap_file(keymap_file, cx); +} + +pub(crate) fn watch_settings_file( defaults: Settings, mut file: WatchedJsonFile, theme_registry: Arc, @@ -77,13 +88,13 @@ pub fn watch_settings_file( .detach(); } -pub fn keymap_updated(content: KeymapFileContent, cx: &mut MutableAppContext) { +fn keymap_updated(content: KeymapFileContent, cx: &mut MutableAppContext) { cx.clear_bindings(); KeymapFileContent::load_defaults(cx); content.add_to_cx(cx).log_err(); } -pub fn settings_updated( +fn settings_updated( defaults: &Settings, content: SettingsFileContent, theme_registry: &Arc, @@ -95,10 +106,20 @@ pub fn settings_updated( cx.refresh_windows(); } -pub fn watch_keymap_file(mut file: WatchedJsonFile, cx: &mut MutableAppContext) { +fn watch_keymap_file(mut file: WatchedJsonFile, cx: &mut MutableAppContext) { cx.spawn(|mut cx| async move { + let mut settings_subscription = None; while let Some(content) = file.0.recv().await { - cx.update(|cx| keymap_updated(content, cx)); + cx.update(|cx| { + let old_base_keymap = cx.global::().base_keymap; + keymap_updated(content.clone(), cx); + settings_subscription = Some(cx.observe_global::(move |cx| { + let settings = cx.global::(); + if settings.base_keymap != old_base_keymap { + keymap_updated(content.clone(), cx); + } + })); + }); } }) .detach(); diff --git a/crates/terminal_view/src/terminal_element.rs b/crates/terminal_view/src/terminal_element.rs index 08ed3ecc2d1da04352dac73a9438a5f3087c8b74..5d03d6304e28ed93d334c308bd5c7d11dc526b07 100644 --- a/crates/terminal_view/src/terminal_element.rs +++ b/crates/terminal_view/src/terminal_element.rs @@ -720,7 +720,7 @@ impl Element for TerminalElement { cx.paint_layer(clip_bounds, |cx| { let origin = bounds.origin() + vec2f(layout.size.cell_width, 0.); - //Elements are ephemeral, only at paint time do we know what could be clicked by a mouse + // Elements are ephemeral, only at paint time do we know what could be clicked by a mouse self.attach_mouse_handlers(origin, self.view.id(), visible_bounds, layout.mode, cx); cx.scene.push_cursor_region(gpui::CursorRegion { diff --git a/crates/terminal_view/src/terminal_view.rs b/crates/terminal_view/src/terminal_view.rs index 634fab938db32268719d83de5c7109cfbedd0709..f1a627e29d2dbade2e4d8398727c166028709352 100644 --- a/crates/terminal_view/src/terminal_view.rs +++ b/crates/terminal_view/src/terminal_view.rs @@ -623,7 +623,7 @@ impl Item for TerminalView { fn deserialize( project: ModelHandle, - _workspace: WeakViewHandle, + workspace: WeakViewHandle, workspace_id: workspace::WorkspaceId, item_id: workspace::ItemId, cx: &mut ViewContext, @@ -633,7 +633,18 @@ impl Item for TerminalView { let cwd = TERMINAL_DB .get_working_directory(item_id, workspace_id) .log_err() - .flatten(); + .flatten() + .or_else(|| { + cx.read(|cx| { + let strategy = cx.global::().terminal_strategy(); + workspace + .upgrade(cx) + .map(|workspace| { + get_working_directory(workspace.read(cx), cx, strategy) + }) + .flatten() + }) + }); cx.update(|cx| { let terminal = project.update(cx, |project, cx| { @@ -940,15 +951,7 @@ mod tests { let params = cx.update(AppState::test); let project = Project::test(params.fs.clone(), [], cx).await; - let (_, workspace) = cx.add_window(|cx| { - Workspace::new( - Default::default(), - 0, - project.clone(), - |_, _| unimplemented!(), - cx, - ) - }); + let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx)); (project, workspace) } diff --git a/crates/theme/src/theme.rs b/crates/theme/src/theme.rs index 484c542edeffce8b22c8f2f29ae4f672c0693399..dcaf2a9c9004cfd8ef958395ed04f0f65be91a27 100644 --- a/crates/theme/src/theme.rs +++ b/crates/theme/src/theme.rs @@ -9,6 +9,9 @@ use gpui::{ use serde::{de::DeserializeOwned, Deserialize}; use serde_json::Value; use std::{collections::HashMap, sync::Arc}; +use ui::{CheckboxStyle, IconStyle}; + +pub mod ui; pub use theme_registry::*; @@ -37,6 +40,7 @@ pub struct Theme { pub tooltip: TooltipStyle, pub terminal: TerminalStyle, pub feedback: FeedbackStyle, + pub welcome: WelcomeStyle, pub color_scheme: ColorScheme, } @@ -49,6 +53,7 @@ pub struct ThemeMeta { #[derive(Deserialize, Default)] pub struct Workspace { pub background: Color, + pub blank_pane: BlankPaneStyle, pub titlebar: Titlebar, pub tab_bar: TabBar, pub pane_divider: Border, @@ -68,6 +73,16 @@ pub struct Workspace { pub drop_target_overlay_color: Color, } +#[derive(Clone, Deserialize, Default)] +pub struct BlankPaneStyle { + pub logo: IconStyle, + pub logo_shadow: IconStyle, + pub logo_container: ContainerStyle, + pub keyboard_hints: ContainerStyle, + pub keyboard_hint: Interactive, + pub keyboard_hint_width: f32, +} + #[derive(Clone, Deserialize, Default)] pub struct Titlebar { #[serde(flatten)] @@ -277,10 +292,10 @@ pub struct StatusBar { pub height: f32, pub item_spacing: f32, pub cursor_position: TextStyle, + pub active_language: Interactive, pub auto_update_progress_message: TextStyle, pub auto_update_done_message: TextStyle, pub lsp_status: Interactive, - pub feedback: Interactive, pub sidebar_buttons: StatusBarSidebarButtons, pub diagnostic_summary: Interactive, pub diagnostic_message: Interactive, @@ -345,6 +360,7 @@ pub struct ProjectPanel { pub cut_entry: Interactive, pub filename_editor: FieldEditor, pub indent_width: f32, + pub open_project_button: Interactive, } #[derive(Clone, Debug, Deserialize, Default)] @@ -850,13 +866,25 @@ pub struct FeedbackStyle { pub link_text_hover: ContainedText, } +#[derive(Clone, Deserialize, Default)] +pub struct WelcomeStyle { + pub page_width: f32, + pub logo: IconStyle, + pub logo_subheading: ContainedText, + pub usage_note: ContainedText, + pub checkbox: CheckboxStyle, + pub checkbox_container: ContainerStyle, + pub button: Interactive, + pub button_group: ContainerStyle, + pub heading_group: ContainerStyle, + pub checkbox_group: ContainerStyle, +} + #[derive(Clone, Deserialize, Default)] pub struct ColorScheme { pub name: String, pub is_light: bool, - pub ramps: RampSet, - pub lowest: Layer, pub middle: Layer, pub highest: Layer, diff --git a/crates/theme/src/ui.rs b/crates/theme/src/ui.rs new file mode 100644 index 0000000000000000000000000000000000000000..5441e711685970d3c33c9db94f9ac03815626531 --- /dev/null +++ b/crates/theme/src/ui.rs @@ -0,0 +1,149 @@ +use gpui::{ + color::Color, + elements::{ + ConstrainedBox, Container, ContainerStyle, Empty, Flex, KeystrokeLabel, Label, + MouseEventHandler, ParentElement, Svg, + }, + Action, Element, ElementBox, EventContext, RenderContext, View, +}; +use serde::Deserialize; + +use crate::ContainedText; + +#[derive(Clone, Deserialize, Default)] +pub struct CheckboxStyle { + pub icon: IconStyle, + pub label: ContainedText, + pub default: ContainerStyle, + pub checked: ContainerStyle, + pub hovered: ContainerStyle, + pub hovered_and_checked: ContainerStyle, +} + +pub fn checkbox( + label: &'static str, + style: &CheckboxStyle, + checked: bool, + cx: &mut RenderContext, + change: fn(checked: bool, cx: &mut EventContext) -> (), +) -> MouseEventHandler { + let label = Label::new(label, style.label.text.clone()) + .contained() + .with_style(style.label.container) + .boxed(); + + checkbox_with_label(label, style, checked, cx, change) +} + +pub fn checkbox_with_label( + label: ElementBox, + style: &CheckboxStyle, + checked: bool, + cx: &mut RenderContext, + change: fn(checked: bool, cx: &mut EventContext) -> (), +) -> MouseEventHandler { + MouseEventHandler::::new(0, cx, |state, _| { + let indicator = if checked { + icon(&style.icon) + } else { + Empty::new() + .constrained() + .with_width(style.icon.dimensions.width) + .with_height(style.icon.dimensions.height) + }; + + Flex::row() + .with_children([ + indicator + .contained() + .with_style(if checked { + if state.hovered() { + style.hovered_and_checked + } else { + style.checked + } + } else { + if state.hovered() { + style.hovered + } else { + style.default + } + }) + .boxed(), + label, + ]) + .align_children_center() + .boxed() + }) + .on_click(gpui::MouseButton::Left, move |_, cx| change(!checked, cx)) + .with_cursor_style(gpui::CursorStyle::PointingHand) +} + +#[derive(Clone, Deserialize, Default)] +pub struct IconStyle { + pub color: Color, + pub icon: String, + pub dimensions: Dimensions, +} + +#[derive(Clone, Deserialize, Default)] +pub struct Dimensions { + pub width: f32, + pub height: f32, +} + +pub fn icon(style: &IconStyle) -> ConstrainedBox { + Svg::new(style.icon.clone()) + .with_color(style.color) + .constrained() + .with_width(style.dimensions.width) + .with_height(style.dimensions.height) +} + +pub fn keystroke_label( + label_text: &'static str, + label_style: &ContainedText, + keystroke_style: &ContainedText, + action: Box, + cx: &mut RenderContext, +) -> Container { + // FIXME: Put the theme in it's own global so we can + // query the keystroke style on our own + keystroke_label_for( + cx.window_id(), + cx.handle().id(), + label_text, + label_style, + keystroke_style, + action, + ) +} + +pub fn keystroke_label_for( + window_id: usize, + view_id: usize, + label_text: &'static str, + label_style: &ContainedText, + keystroke_style: &ContainedText, + action: Box, +) -> Container { + Flex::row() + .with_child( + Label::new(label_text, label_style.text.clone()) + .contained() + .boxed(), + ) + .with_child({ + KeystrokeLabel::new( + window_id, + view_id, + action, + keystroke_style.container, + keystroke_style.text.clone(), + ) + .flex_float() + .boxed() + }) + .contained() + .with_style(label_style.container) +} diff --git a/crates/theme_selector/src/theme_selector.rs b/crates/theme_selector/src/theme_selector.rs index d999730a0d8b7abe21c0d9b39bfc3a9fb4ca49ca..ae3278b7111c8e903293723759a1e8fd238a8697 100644 --- a/crates/theme_selector/src/theme_selector.rs +++ b/crates/theme_selector/src/theme_selector.rs @@ -47,12 +47,7 @@ impl ThemeSelector { let mut theme_names = registry .list(**cx.default_global::()) .collect::>(); - theme_names.sort_unstable_by(|a, b| { - a.is_light - .cmp(&b.is_light) - .reverse() - .then(a.name.cmp(&b.name)) - }); + theme_names.sort_unstable_by(|a, b| a.is_light.cmp(&b.is_light).then(a.name.cmp(&b.name))); let matches = theme_names .iter() .map(|meta| StringMatch { diff --git a/crates/util/src/util.rs b/crates/util/src/util.rs index 5ecf889f9b1a016a86987827390831b37efe8d1c..3824312a4fa9ec0d9993063c855b37bc55c3121c 100644 --- a/crates/util/src/util.rs +++ b/crates/util/src/util.rs @@ -124,11 +124,15 @@ pub trait TryFutureExt { fn warn_on_err(self) -> LogErrorFuture where Self: Sized; + fn unwrap(self) -> UnwrapFuture + where + Self: Sized; } -impl TryFutureExt for F +impl TryFutureExt for F where - F: Future>, + F: Future>, + E: std::fmt::Debug, { fn log_err(self) -> LogErrorFuture where @@ -143,17 +147,25 @@ where { LogErrorFuture(self, log::Level::Warn) } + + fn unwrap(self) -> UnwrapFuture + where + Self: Sized, + { + UnwrapFuture(self) + } } pub struct LogErrorFuture(F, log::Level); -impl Future for LogErrorFuture +impl Future for LogErrorFuture where - F: Future>, + F: Future>, + E: std::fmt::Debug, { type Output = Option; - fn poll(self: std::pin::Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { + fn poll(self: Pin<&mut Self>, cx: &mut Context) -> Poll { let level = self.1; let inner = unsafe { Pin::new_unchecked(&mut self.get_unchecked_mut().0) }; match inner.poll(cx) { @@ -169,6 +181,24 @@ where } } +pub struct UnwrapFuture(F); + +impl Future for UnwrapFuture +where + F: Future>, + E: std::fmt::Debug, +{ + type Output = T; + + fn poll(self: Pin<&mut Self>, cx: &mut Context) -> Poll { + let inner = unsafe { Pin::new_unchecked(&mut self.get_unchecked_mut().0) }; + match inner.poll(cx) { + Poll::Ready(result) => Poll::Ready(result.unwrap()), + Poll::Pending => Poll::Pending, + } + } +} + struct Defer(Option); impl Drop for Defer { diff --git a/crates/welcome/Cargo.toml b/crates/welcome/Cargo.toml new file mode 100644 index 0000000000000000000000000000000000000000..3da90deb2d1a2f1f6a9fdbe761bb4edcc35f664e --- /dev/null +++ b/crates/welcome/Cargo.toml @@ -0,0 +1,27 @@ +[package] +name = "welcome" +version = "0.1.0" +edition = "2021" +publish = false + +[lib] +path = "src/welcome.rs" + +[features] +test-support = [] + +[dependencies] +anyhow = "1.0.38" +log = "0.4" +editor = { path = "../editor" } +fuzzy = { path = "../fuzzy" } +gpui = { path = "../gpui" } +db = { path = "../db" } +install_cli = { path = "../install_cli" } +project = { path = "../project" } +settings = { path = "../settings" } +theme = { path = "../theme" } +theme_selector = { path = "../theme_selector" } +util = { path = "../util" } +picker = { path = "../picker" } +workspace = { path = "../workspace" } \ No newline at end of file diff --git a/crates/welcome/src/base_keymap_picker.rs b/crates/welcome/src/base_keymap_picker.rs new file mode 100644 index 0000000000000000000000000000000000000000..a37bcb18371c95f8ea8fdc1181ec9d0af3a6409e --- /dev/null +++ b/crates/welcome/src/base_keymap_picker.rs @@ -0,0 +1,175 @@ +use fuzzy::{match_strings, StringMatch, StringMatchCandidate}; +use gpui::{ + actions, + elements::{ChildView, Element as _, Label}, + AnyViewHandle, Entity, MutableAppContext, View, ViewContext, ViewHandle, +}; +use picker::{Picker, PickerDelegate}; +use settings::{settings_file::SettingsFile, BaseKeymap, Settings}; +use workspace::Workspace; + +pub struct BaseKeymapSelector { + matches: Vec, + picker: ViewHandle>, + selected_index: usize, +} + +actions!(welcome, [ToggleBaseKeymapSelector]); + +pub fn init(cx: &mut MutableAppContext) { + Picker::::init(cx); + cx.add_action({ + move |workspace, _: &ToggleBaseKeymapSelector, cx| BaseKeymapSelector::toggle(workspace, cx) + }); +} + +pub enum Event { + Dismissed, +} + +impl BaseKeymapSelector { + fn toggle(workspace: &mut Workspace, cx: &mut ViewContext) { + workspace.toggle_modal(cx, |_, cx| { + let this = cx.add_view(|cx| Self::new(cx)); + cx.subscribe(&this, Self::on_event).detach(); + this + }); + } + + fn new(cx: &mut ViewContext) -> Self { + let base = cx.global::().base_keymap; + let selected_index = BaseKeymap::OPTIONS + .iter() + .position(|(_, value)| *value == base) + .unwrap_or(0); + + let this = cx.weak_handle(); + Self { + picker: cx.add_view(|cx| Picker::new("Select a base keymap", this, cx)), + matches: Vec::new(), + selected_index, + } + } + + fn on_event( + workspace: &mut Workspace, + _: ViewHandle, + event: &Event, + cx: &mut ViewContext, + ) { + match event { + Event::Dismissed => { + workspace.dismiss_modal(cx); + } + } + } +} + +impl Entity for BaseKeymapSelector { + type Event = Event; +} + +impl View for BaseKeymapSelector { + fn ui_name() -> &'static str { + "BaseKeymapSelector" + } + + fn render(&mut self, cx: &mut gpui::RenderContext<'_, Self>) -> gpui::ElementBox { + ChildView::new(self.picker.clone(), cx).boxed() + } + + fn focus_in(&mut self, _: AnyViewHandle, cx: &mut ViewContext) { + if cx.is_self_focused() { + cx.focus(&self.picker); + } + } +} + +impl PickerDelegate for BaseKeymapSelector { + fn match_count(&self) -> usize { + self.matches.len() + } + + fn selected_index(&self) -> usize { + self.selected_index + } + + fn set_selected_index(&mut self, ix: usize, _: &mut ViewContext) { + self.selected_index = ix; + } + + fn update_matches(&mut self, query: String, cx: &mut ViewContext) -> gpui::Task<()> { + let background = cx.background().clone(); + let candidates = BaseKeymap::names() + .enumerate() + .map(|(id, name)| StringMatchCandidate { + id, + char_bag: name.into(), + string: name.into(), + }) + .collect::>(); + + cx.spawn(|this, mut cx| async move { + let matches = if query.is_empty() { + candidates + .into_iter() + .enumerate() + .map(|(index, candidate)| StringMatch { + candidate_id: index, + string: candidate.string, + positions: Vec::new(), + score: 0.0, + }) + .collect() + } else { + match_strings( + &candidates, + &query, + false, + 100, + &Default::default(), + background, + ) + .await + }; + + this.update(&mut cx, |this, cx| { + this.matches = matches; + this.selected_index = this + .selected_index + .min(this.matches.len().saturating_sub(1)); + cx.notify(); + }); + }) + } + + fn confirm(&mut self, cx: &mut ViewContext) { + if let Some(selection) = self.matches.get(self.selected_index) { + let base_keymap = BaseKeymap::from_names(&selection.string); + SettingsFile::update(cx, move |settings| settings.base_keymap = Some(base_keymap)); + } + cx.emit(Event::Dismissed); + } + + fn dismiss(&mut self, cx: &mut ViewContext) { + cx.emit(Event::Dismissed) + } + + fn render_match( + &self, + ix: usize, + mouse_state: &mut gpui::MouseState, + selected: bool, + cx: &gpui::AppContext, + ) -> gpui::ElementBox { + let theme = &cx.global::().theme; + let keymap_match = &self.matches[ix]; + let style = theme.picker.item.style_for(mouse_state, selected); + + Label::new(keymap_match.string.clone(), style.label.clone()) + .with_highlights(keymap_match.positions.clone()) + .contained() + .with_style(style.container) + .boxed() + } +} diff --git a/crates/welcome/src/welcome.rs b/crates/welcome/src/welcome.rs new file mode 100644 index 0000000000000000000000000000000000000000..3a35920b88922bb2b7bf13e5bd849acd77e4be3a --- /dev/null +++ b/crates/welcome/src/welcome.rs @@ -0,0 +1,316 @@ +mod base_keymap_picker; + +use std::{borrow::Cow, sync::Arc}; + +use db::kvp::KEY_VALUE_STORE; +use gpui::{ + elements::{Flex, Label, MouseEventHandler, ParentElement}, + Action, Element, ElementBox, Entity, MouseButton, MutableAppContext, RenderContext, + Subscription, View, ViewContext, +}; +use settings::{settings_file::SettingsFile, Settings}; + +use workspace::{ + item::Item, open_new, sidebar::SidebarSide, AppState, PaneBackdrop, Welcome, Workspace, + WorkspaceId, +}; + +use crate::base_keymap_picker::ToggleBaseKeymapSelector; + +pub const FIRST_OPEN: &str = "first_open"; + +pub fn init(cx: &mut MutableAppContext) { + cx.add_action(|workspace: &mut Workspace, _: &Welcome, cx| { + let welcome_page = cx.add_view(WelcomePage::new); + workspace.add_item(Box::new(welcome_page), cx) + }); + + base_keymap_picker::init(cx); +} + +pub fn show_welcome_experience(app_state: &Arc, cx: &mut MutableAppContext) { + open_new(&app_state, cx, |workspace, cx| { + workspace.toggle_sidebar(SidebarSide::Left, cx); + let welcome_page = cx.add_view(|cx| WelcomePage::new(cx)); + workspace.add_item_to_center(Box::new(welcome_page.clone()), cx); + cx.focus(welcome_page); + cx.notify(); + }) + .detach(); + + db::write_and_log(cx, || { + KEY_VALUE_STORE.write_kvp(FIRST_OPEN.to_string(), "false".to_string()) + }); +} + +pub struct WelcomePage { + _settings_subscription: Subscription, +} + +impl Entity for WelcomePage { + type Event = (); +} + +impl View for WelcomePage { + fn ui_name() -> &'static str { + "WelcomePage" + } + + fn render(&mut self, cx: &mut gpui::RenderContext<'_, Self>) -> ElementBox { + let self_handle = cx.handle(); + let settings = cx.global::(); + let theme = settings.theme.clone(); + + let width = theme.welcome.page_width; + + let (diagnostics, metrics) = { + let telemetry = settings.telemetry(); + (telemetry.diagnostics(), telemetry.metrics()) + }; + + enum Metrics {} + enum Diagnostics {} + + PaneBackdrop::new( + self_handle.id(), + Flex::column() + .with_children([ + Flex::column() + .with_children([ + theme::ui::icon(&theme.welcome.logo) + .aligned() + .contained() + .aligned() + .boxed(), + Label::new( + "Code at the speed of thought", + theme.welcome.logo_subheading.text.clone(), + ) + .aligned() + .contained() + .with_style(theme.welcome.logo_subheading.container) + .boxed(), + ]) + .contained() + .with_style(theme.welcome.heading_group) + .constrained() + .with_width(width) + .boxed(), + Flex::column() + .with_children([ + self.render_cta_button( + "Choose a theme", + theme_selector::Toggle, + width, + cx, + ), + self.render_cta_button( + "Choose a keymap", + ToggleBaseKeymapSelector, + width, + cx, + ), + self.render_cta_button( + "Install the CLI", + install_cli::Install, + width, + cx, + ), + ]) + .contained() + .with_style(theme.welcome.button_group) + .constrained() + .with_width(width) + .boxed(), + Flex::column() + .with_children([ + theme::ui::checkbox_with_label::( + Flex::column() + .with_children([ + Label::new( + "Send anonymous usage data", + theme.welcome.checkbox.label.text.clone(), + ) + .contained() + .with_style(theme.welcome.checkbox.label.container) + .boxed(), + Label::new( + "Help > View Telemetry", + theme.welcome.usage_note.text.clone(), + ) + .contained() + .with_style(theme.welcome.usage_note.container) + .boxed(), + ]) + .boxed(), + &theme.welcome.checkbox, + metrics, + cx, + |checked, cx| { + SettingsFile::update(cx, move |file| { + file.telemetry.set_metrics(checked) + }) + }, + ) + .contained() + .with_style(theme.welcome.checkbox_container) + .boxed(), + theme::ui::checkbox::( + "Send crash reports", + &theme.welcome.checkbox, + diagnostics, + cx, + |checked, cx| { + SettingsFile::update(cx, move |file| { + file.telemetry.set_diagnostics(checked) + }) + }, + ) + .contained() + .with_style(theme.welcome.checkbox_container) + .boxed(), + ]) + .contained() + .with_style(theme.welcome.checkbox_group) + .constrained() + .with_width(width) + .boxed(), + ]) + .constrained() + .with_max_width(width) + .contained() + .with_uniform_padding(10.) + .aligned() + .boxed(), + ) + .boxed() + } +} + +impl WelcomePage { + pub fn new(cx: &mut ViewContext) -> Self { + let handle = cx.weak_handle(); + + let settings_subscription = cx.observe_global::(move |cx| { + if let Some(handle) = handle.upgrade(cx) { + handle.update(cx, |_, cx| cx.notify()) + } + }); + + WelcomePage { + _settings_subscription: settings_subscription, + } + } + + fn render_cta_button( + &self, + label: L, + action: A, + width: f32, + cx: &mut RenderContext, + ) -> ElementBox + where + L: Into>, + A: 'static + Action + Clone, + { + let theme = cx.global::().theme.clone(); + MouseEventHandler::::new(0, cx, |state, _| { + let style = theme.welcome.button.style_for(state, false); + Label::new(label, style.text.clone()) + .aligned() + .contained() + .with_style(style.container) + .constrained() + .with_max_width(width) + .boxed() + }) + .on_click(MouseButton::Left, move |_, cx| { + cx.dispatch_action(action.clone()) + }) + .with_cursor_style(gpui::CursorStyle::PointingHand) + .boxed() + } + + // fn render_settings_checkbox( + // &self, + // label: &'static str, + // style: &CheckboxStyle, + // checked: bool, + // cx: &mut RenderContext, + // set_value: fn(&mut SettingsFileContent, checked: bool) -> (), + // ) -> ElementBox { + // MouseEventHandler::::new(0, cx, |state, _| { + // let indicator = if checked { + // Svg::new(style.check_icon.clone()) + // .with_color(style.check_icon_color) + // .constrained() + // } else { + // Empty::new().constrained() + // }; + + // Flex::row() + // .with_children([ + // indicator + // .with_width(style.width) + // .with_height(style.height) + // .contained() + // .with_style(if checked { + // if state.hovered() { + // style.hovered_and_checked + // } else { + // style.checked + // } + // } else { + // if state.hovered() { + // style.hovered + // } else { + // style.default + // } + // }) + // .boxed(), + // Label::new(label, style.label.text.clone()) + // .contained() + // .with_style(style.label.container) + // .boxed(), + // ]) + // .align_children_center() + // .boxed() + // }) + // .on_click(gpui::MouseButton::Left, move |_, cx| { + // SettingsFile::update(cx, move |content| set_value(content, !checked)) + // }) + // .with_cursor_style(gpui::CursorStyle::PointingHand) + // .contained() + // .with_style(style.container) + // .boxed() + // } +} + +impl Item for WelcomePage { + fn tab_content( + &self, + _detail: Option, + style: &theme::Tab, + _cx: &gpui::AppContext, + ) -> gpui::ElementBox { + Flex::row() + .with_child( + Label::new("Welcome to Zed!", style.label.clone()) + .aligned() + .contained() + .boxed(), + ) + .boxed() + } + + fn show_toolbar(&self) -> bool { + false + } + fn clone_on_split( + &self, + _workspace_id: WorkspaceId, + cx: &mut ViewContext, + ) -> Option { + Some(WelcomePage::new(cx)) + } +} diff --git a/crates/workspace/Cargo.toml b/crates/workspace/Cargo.toml index cdc1a799da08e30fdfdde63523e63467b6c32a70..a143e1a2400af479a8f08fae08c8d82d86135484 100644 --- a/crates/workspace/Cargo.toml +++ b/crates/workspace/Cargo.toml @@ -27,6 +27,7 @@ context_menu = { path = "../context_menu" } drag_and_drop = { path = "../drag_and_drop" } fs = { path = "../fs" } gpui = { path = "../gpui" } +install_cli = { path = "../install_cli" } language = { path = "../language" } menu = { path = "../menu" } project = { path = "../project" } diff --git a/crates/workspace/src/dock.rs b/crates/workspace/src/dock.rs index 2bd880828106cbfaf6dd6fb9797876b5d53ead0d..f5ee8cad51988f6662fd2106a60c89f2798ede88 100644 --- a/crates/workspace/src/dock.rs +++ b/crates/workspace/src/dock.rs @@ -13,7 +13,7 @@ use gpui::{ use settings::{DockAnchor, Settings}; use theme::Theme; -use crate::{sidebar::SidebarSide, ItemHandle, Pane, Workspace}; +use crate::{sidebar::SidebarSide, BackgroundActions, ItemHandle, Pane, Workspace}; pub use toggle_dock_button::ToggleDockButton; #[derive(PartialEq, Clone, Deserialize)] @@ -39,20 +39,24 @@ impl_internal_actions!(dock, [MoveDock, AddDefaultItemToDock]); pub fn init(cx: &mut MutableAppContext) { cx.add_action(Dock::focus_dock); cx.add_action(Dock::hide_dock); - cx.add_action(Dock::move_dock); + cx.add_action( + |workspace: &mut Workspace, &MoveDock(dock_anchor), cx: &mut ViewContext| { + Dock::move_dock(workspace, dock_anchor, true, cx); + }, + ); cx.add_action( |workspace: &mut Workspace, _: &AnchorDockRight, cx: &mut ViewContext| { - Dock::move_dock(workspace, &MoveDock(DockAnchor::Right), cx) + Dock::move_dock(workspace, DockAnchor::Right, true, cx); }, ); cx.add_action( |workspace: &mut Workspace, _: &AnchorDockBottom, cx: &mut ViewContext| { - Dock::move_dock(workspace, &MoveDock(DockAnchor::Bottom), cx) + Dock::move_dock(workspace, DockAnchor::Bottom, true, cx) }, ); cx.add_action( |workspace: &mut Workspace, _: &ExpandDock, cx: &mut ViewContext| { - Dock::move_dock(workspace, &MoveDock(DockAnchor::Expanded), cx) + Dock::move_dock(workspace, DockAnchor::Expanded, true, cx) }, ); cx.add_action( @@ -177,12 +181,21 @@ pub struct Dock { impl Dock { pub fn new( + workspace_id: usize, default_item_factory: DockDefaultItemFactory, + background_actions: BackgroundActions, cx: &mut ViewContext, ) -> Self { let position = DockPosition::Hidden(cx.global::().default_dock_anchor); - let pane = cx.add_view(|cx| Pane::new(Some(position.anchor()), cx)); + let pane = cx.add_view(|cx| { + Pane::new( + workspace_id, + Some(position.anchor()), + background_actions, + cx, + ) + }); pane.update(cx, |pane, cx| { pane.set_active(false, cx); }); @@ -215,6 +228,7 @@ impl Dock { pub(crate) fn set_dock_position( workspace: &mut Workspace, new_position: DockPosition, + focus: bool, cx: &mut ViewContext, ) { workspace.dock.position = new_position; @@ -235,19 +249,23 @@ impl Dock { let pane = workspace.dock.pane.clone(); if pane.read(cx).items().next().is_none() { if let Some(item_to_add) = (workspace.dock.default_item_factory)(workspace, cx) { - Pane::add_item(workspace, &pane, item_to_add, true, true, None, cx); + Pane::add_item(workspace, &pane, item_to_add, focus, focus, None, cx); } else { workspace.dock.position = workspace.dock.position.hide(); } } else { - cx.focus(pane); + if focus { + cx.focus(pane); + } } } else if let Some(last_active_center_pane) = workspace .last_active_center_pane .as_ref() .and_then(|pane| pane.upgrade(cx)) { - cx.focus(last_active_center_pane); + if focus { + cx.focus(last_active_center_pane); + } } cx.emit(crate::Event::DockAnchorChanged); workspace.serialize_workspace(cx); @@ -255,11 +273,11 @@ impl Dock { } pub fn hide(workspace: &mut Workspace, cx: &mut ViewContext) { - Self::set_dock_position(workspace, workspace.dock.position.hide(), cx); + Self::set_dock_position(workspace, workspace.dock.position.hide(), true, cx); } - pub fn show(workspace: &mut Workspace, cx: &mut ViewContext) { - Self::set_dock_position(workspace, workspace.dock.position.show(), cx); + pub fn show(workspace: &mut Workspace, focus: bool, cx: &mut ViewContext) { + Self::set_dock_position(workspace, workspace.dock.position.show(), focus, cx); } pub fn hide_on_sidebar_shown( @@ -275,19 +293,20 @@ impl Dock { } fn focus_dock(workspace: &mut Workspace, _: &FocusDock, cx: &mut ViewContext) { - Self::set_dock_position(workspace, workspace.dock.position.show(), cx); + Self::set_dock_position(workspace, workspace.dock.position.show(), true, cx); } fn hide_dock(workspace: &mut Workspace, _: &HideDock, cx: &mut ViewContext) { - Self::set_dock_position(workspace, workspace.dock.position.hide(), cx); + Self::set_dock_position(workspace, workspace.dock.position.hide(), true, cx); } - fn move_dock( + pub fn move_dock( workspace: &mut Workspace, - &MoveDock(new_anchor): &MoveDock, + new_anchor: DockAnchor, + focus: bool, cx: &mut ViewContext, ) { - Self::set_dock_position(workspace, DockPosition::Shown(new_anchor), cx); + Self::set_dock_position(workspace, DockPosition::Shown(new_anchor), focus, cx); } pub fn render( @@ -482,6 +501,7 @@ mod tests { 0, project.clone(), default_item_factory, + || &[], cx, ) }); @@ -610,7 +630,14 @@ mod tests { cx.update(|cx| init(cx)); let project = Project::test(fs, [], cx).await; let (window_id, workspace) = cx.add_window(|cx| { - Workspace::new(Default::default(), 0, project, default_item_factory, cx) + Workspace::new( + Default::default(), + 0, + project, + default_item_factory, + || &[], + cx, + ) }); workspace.update(cx, |workspace, cx| { diff --git a/crates/workspace/src/dock/toggle_dock_button.rs b/crates/workspace/src/dock/toggle_dock_button.rs index cafbea7db37c6fdccd0e3534bd2c4a2757aef2f0..0b96c3e67b6c2883b5704ed09e12ba23c11ffac4 100644 --- a/crates/workspace/src/dock/toggle_dock_button.rs +++ b/crates/workspace/src/dock/toggle_dock_button.rs @@ -42,6 +42,7 @@ impl View for ToggleDockButton { let workspace = workspace.unwrap(); let dock_position = workspace.read(cx).dock.position; + let dock_pane = workspace.read(cx.app).dock_pane().clone(); let theme = cx.global::().theme.clone(); @@ -67,7 +68,6 @@ impl View for ToggleDockButton { }) .with_cursor_style(CursorStyle::PointingHand) .on_up(MouseButton::Left, move |event, cx| { - let dock_pane = workspace.read(cx.app).dock_pane(); let drop_index = dock_pane.read(cx.app).items_len() + 1; handle_dropped_item(event, &dock_pane.downgrade(), drop_index, false, None, cx); }); diff --git a/crates/workspace/src/item.rs b/crates/workspace/src/item.rs index a80b9f8d83d9dc4d407b337a944537143e24581e..b55c9942f8b51707c3b550545971cc57e047b2ce 100644 --- a/crates/workspace/src/item.rs +++ b/crates/workspace/src/item.rs @@ -151,6 +151,9 @@ pub trait Item: View { "deserialize() must be implemented if serialized_item_kind() returns Some(_)" ) } + fn show_toolbar(&self) -> bool { + true + } } pub trait ItemHandle: 'static + fmt::Debug { @@ -213,6 +216,7 @@ pub trait ItemHandle: 'static + fmt::Debug { fn breadcrumb_location(&self, cx: &AppContext) -> ToolbarItemLocation; fn breadcrumbs(&self, theme: &Theme, cx: &AppContext) -> Option>; fn serialized_item_kind(&self) -> Option<&'static str>; + fn show_toolbar(&self, cx: &AppContext) -> bool; } pub trait WeakItemHandle { @@ -591,6 +595,10 @@ impl ItemHandle for ViewHandle { fn serialized_item_kind(&self) -> Option<&'static str> { T::serialized_item_kind() } + + fn show_toolbar(&self, cx: &AppContext) -> bool { + self.read(cx).show_toolbar() + } } impl From> for AnyViewHandle { diff --git a/crates/workspace/src/notifications.rs b/crates/workspace/src/notifications.rs index 141a345382603ea59c1699b008d2104deb4d18ae..76f46f83c5f87c88b8dd5c68e2e2d98e39798a79 100644 --- a/crates/workspace/src/notifications.rs +++ b/crates/workspace/src/notifications.rs @@ -122,6 +122,8 @@ impl Workspace { pub mod simple_message_notification { + use std::borrow::Cow; + use gpui::{ actions, elements::{Flex, MouseEventHandler, Padding, ParentElement, Svg, Text}, @@ -153,9 +155,9 @@ pub mod simple_message_notification { } pub struct MessageNotification { - message: String, + message: Cow<'static, str>, click_action: Option>, - click_message: Option, + click_message: Option>, } pub enum MessageNotificationEvent { @@ -167,23 +169,23 @@ pub mod simple_message_notification { } impl MessageNotification { - pub fn new_message>(message: S) -> MessageNotification { + pub fn new_message>>(message: S) -> MessageNotification { Self { - message: message.as_ref().to_string(), + message: message.into(), click_action: None, click_message: None, } } - pub fn new, A: Action, S2: AsRef>( + pub fn new>, A: Action, S2: Into>>( message: S1, click_action: A, click_message: S2, ) -> Self { Self { - message: message.as_ref().to_string(), + message: message.into(), click_action: Some(Box::new(click_action) as Box), - click_message: Some(click_message.as_ref().to_string()), + click_message: Some(click_message.into()), } } @@ -210,6 +212,8 @@ pub mod simple_message_notification { let click_message = self.click_message.as_ref().map(|message| message.clone()); let message = self.message.clone(); + let has_click_action = click_action.is_some(); + MouseEventHandler::::new(0, cx, |state, cx| { Flex::column() .with_child( @@ -243,6 +247,7 @@ pub mod simple_message_notification { .on_click(MouseButton::Left, move |_, cx| { cx.dispatch_action(CancelMessageNotification) }) + .with_cursor_style(CursorStyle::PointingHand) .aligned() .constrained() .with_height( @@ -272,12 +277,19 @@ pub mod simple_message_notification { .contained() .boxed() }) - .with_cursor_style(CursorStyle::PointingHand) + // Since we're not using a proper overlay, we have to capture these extra events + .on_down(MouseButton::Left, |_, _| {}) + .on_up(MouseButton::Left, |_, _| {}) .on_click(MouseButton::Left, move |_, cx| { if let Some(click_action) = click_action.as_ref() { cx.dispatch_any_action(click_action.boxed_clone()) } }) + .with_cursor_style(if has_click_action { + CursorStyle::PointingHand + } else { + CursorStyle::Arrow + }) .boxed() } } diff --git a/crates/workspace/src/pane.rs b/crates/workspace/src/pane.rs index f9ba6ef5b4e452aff4670900c30a16c904f451ee..b0d7b7870895ef9718c81d7cda8985c54bc7554a 100644 --- a/crates/workspace/src/pane.rs +++ b/crates/workspace/src/pane.rs @@ -24,8 +24,8 @@ use gpui::{ keymap_matcher::KeymapContext, platform::{CursorStyle, NavigationDirection}, Action, AnyViewHandle, AnyWeakViewHandle, AppContext, AsyncAppContext, Entity, EventContext, - ModelHandle, MouseButton, MutableAppContext, PromptLevel, Quad, RenderContext, Task, View, - ViewContext, ViewHandle, WeakViewHandle, + ModelHandle, MouseButton, MouseRegion, MutableAppContext, PromptLevel, Quad, RenderContext, + Task, View, ViewContext, ViewHandle, WeakViewHandle, }; use project::{Project, ProjectEntryId, ProjectPath}; use serde::Deserialize; @@ -110,6 +110,8 @@ impl_internal_actions!( const MAX_NAVIGATION_HISTORY_LEN: usize = 1024; +pub type BackgroundActions = fn() -> &'static [(&'static str, &'static dyn Action)]; + pub fn init(cx: &mut MutableAppContext) { cx.add_action(|pane: &mut Pane, action: &ActivateItem, cx| { pane.activate_item(action.0, true, true, cx); @@ -215,6 +217,8 @@ pub struct Pane { toolbar: ViewHandle, tab_bar_context_menu: ViewHandle, docked: Option, + _background_actions: BackgroundActions, + _workspace_id: usize, } pub struct ItemNavHistory { @@ -271,7 +275,12 @@ enum ItemType { } impl Pane { - pub fn new(docked: Option, cx: &mut ViewContext) -> Self { + pub fn new( + workspace_id: usize, + docked: Option, + background_actions: BackgroundActions, + cx: &mut ViewContext, + ) -> Self { let handle = cx.weak_handle(); let context_menu = cx.add_view(ContextMenu::new); Self { @@ -292,6 +301,8 @@ impl Pane { toolbar: cx.add_view(|_| Toolbar::new(handle)), tab_bar_context_menu: context_menu, docked, + _background_actions: background_actions, + _workspace_id: workspace_id, } } @@ -1415,6 +1426,14 @@ impl Pane { .flex(1., false) .boxed() } + + fn render_blank_pane(&mut self, theme: &Theme, _cx: &mut RenderContext) -> ElementBox { + let background = theme.workspace.background; + Empty::new() + .contained() + .with_background_color(background) + .boxed() + } } impl Entity for Pane { @@ -1485,11 +1504,12 @@ impl View for Pane { cx, { let toolbar = self.toolbar.clone(); + let toolbar_hidden = toolbar.read(cx).hidden(); move |_, cx| { Flex::column() - .with_child( - ChildView::new(&toolbar, cx).expanded().boxed(), - ) + .with_children((!toolbar_hidden).then(|| { + ChildView::new(&toolbar, cx).expanded().boxed() + })) .with_child( ChildView::new(active_item, cx) .flex(1., true) @@ -1507,11 +1527,8 @@ impl View for Pane { enum EmptyPane {} let theme = cx.global::().theme.clone(); - dragged_item_receiver::(0, 0, false, None, cx, |_, _| { - Empty::new() - .contained() - .with_background_color(theme.workspace.background) - .boxed() + dragged_item_receiver::(0, 0, false, None, cx, |_, cx| { + self.render_blank_pane(&theme, cx) }) .on_down(MouseButton::Left, |_, cx| { cx.focus_parent_view(); @@ -1705,6 +1722,93 @@ impl NavHistory { } } +pub struct PaneBackdrop { + child_view: usize, + child: ElementBox, +} +impl PaneBackdrop { + pub fn new(pane_item_view: usize, child: ElementBox) -> Self { + PaneBackdrop { + child, + child_view: pane_item_view, + } + } +} + +impl Element for PaneBackdrop { + type LayoutState = (); + + type PaintState = (); + + fn layout( + &mut self, + constraint: gpui::SizeConstraint, + cx: &mut gpui::LayoutContext, + ) -> (Vector2F, Self::LayoutState) { + let size = self.child.layout(constraint, cx); + (size, ()) + } + + fn paint( + &mut self, + bounds: RectF, + visible_bounds: RectF, + _: &mut Self::LayoutState, + cx: &mut gpui::PaintContext, + ) -> Self::PaintState { + let background = cx.global::().theme.editor.background; + + let visible_bounds = bounds.intersection(visible_bounds).unwrap_or_default(); + + cx.scene.push_quad(gpui::Quad { + bounds: RectF::new(bounds.origin(), bounds.size()), + background: Some(background), + ..Default::default() + }); + + let child_view_id = self.child_view; + cx.scene.push_mouse_region( + MouseRegion::new::(child_view_id, 0, visible_bounds).on_down( + gpui::MouseButton::Left, + move |_, cx| { + let window_id = cx.window_id; + cx.focus(window_id, Some(child_view_id)) + }, + ), + ); + + cx.paint_layer(Some(bounds), |cx| { + self.child.paint(bounds.origin(), visible_bounds, cx) + }) + } + + fn rect_for_text_range( + &self, + range_utf16: std::ops::Range, + _bounds: RectF, + _visible_bounds: RectF, + _layout: &Self::LayoutState, + _paint: &Self::PaintState, + cx: &gpui::MeasurementContext, + ) -> Option { + self.child.rect_for_text_range(range_utf16, cx) + } + + fn debug( + &self, + _bounds: RectF, + _layout: &Self::LayoutState, + _paint: &Self::PaintState, + cx: &gpui::DebugContext, + ) -> serde_json::Value { + gpui::json::json!({ + "type": "Pane Back Drop", + "view": self.child_view, + "child": self.child.debug(cx), + }) + } +} + #[cfg(test)] mod tests { use std::sync::Arc; @@ -1721,9 +1825,7 @@ mod tests { let fs = FakeFs::new(cx.background()); let project = Project::test(fs, None, cx).await; - let (_, workspace) = cx.add_window(|cx| { - Workspace::new(Default::default(), 0, project, |_, _| unimplemented!(), cx) - }); + let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx)); let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone()); // 1. Add with a destination index @@ -1811,9 +1913,7 @@ mod tests { let fs = FakeFs::new(cx.background()); let project = Project::test(fs, None, cx).await; - let (_, workspace) = cx.add_window(|cx| { - Workspace::new(Default::default(), 0, project, |_, _| unimplemented!(), cx) - }); + let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx)); let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone()); // 1. Add with a destination index @@ -1889,9 +1989,7 @@ mod tests { let fs = FakeFs::new(cx.background()); let project = Project::test(fs, None, cx).await; - let (_, workspace) = cx.add_window(|cx| { - Workspace::new(Default::default(), 0, project, |_, _| unimplemented!(), cx) - }); + let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx)); let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone()); // singleton view @@ -2000,8 +2098,7 @@ mod tests { let fs = FakeFs::new(cx.background()); let project = Project::test(fs, None, cx).await; - let (_, workspace) = - cx.add_window(|cx| Workspace::new(None, 0, project, |_, _| unimplemented!(), cx)); + let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx)); let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone()); add_labled_item(&workspace, &pane, "A", cx); diff --git a/crates/workspace/src/toolbar.rs b/crates/workspace/src/toolbar.rs index 7443f19003fabcf14771655a39d51bd07f8a6813..df10db91a05dbd24a31d6122eb3a39ee6ca4de2f 100644 --- a/crates/workspace/src/toolbar.rs +++ b/crates/workspace/src/toolbar.rs @@ -42,6 +42,7 @@ pub enum ToolbarItemLocation { pub struct Toolbar { active_pane_item: Option>, + hidden: bool, pane: WeakViewHandle, items: Vec<(Box, ToolbarItemLocation)>, } @@ -211,6 +212,7 @@ impl Toolbar { active_pane_item: None, pane, items: Default::default(), + hidden: false, } } @@ -243,6 +245,12 @@ impl Toolbar { cx: &mut ViewContext, ) { self.active_pane_item = pane_item.map(|item| item.boxed_clone()); + self.hidden = self + .active_pane_item + .as_ref() + .map(|item| !item.show_toolbar(cx)) + .unwrap_or(false); + for (toolbar_item, current_location) in self.items.iter_mut() { let new_location = toolbar_item.set_active_pane_item(pane_item, cx); if new_location != *current_location { @@ -257,6 +265,10 @@ impl Toolbar { .iter() .find_map(|(item, _)| item.to_any().downcast()) } + + pub fn hidden(&self) -> bool { + self.hidden + } } impl ToolbarItemViewHandle for ViewHandle { diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index c134c7f68c9e04a7dd03a8ed86fc5959632745c7..43ae12b732938e0c02fb055a82632bfa7b163a5a 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -16,7 +16,7 @@ mod toolbar; pub use smallvec; -use anyhow::{anyhow, Result}; +use anyhow::{anyhow, Context, Result}; use call::ActiveCall; use client::{ proto::{self, PeerId}, @@ -43,7 +43,8 @@ use gpui::{ platform::{CursorStyle, WindowOptions}, AnyModelHandle, AnyViewHandle, AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle, MouseButton, MutableAppContext, PathPromptOptions, Platform, PromptLevel, RenderContext, - SizeConstraint, Task, View, ViewContext, ViewHandle, WeakViewHandle, WindowBounds, + SizeConstraint, Subscription, Task, View, ViewContext, ViewHandle, WeakViewHandle, + WindowBounds, }; use item::{FollowableItem, FollowableItemHandle, Item, ItemHandle, ProjectItem}; use language::LanguageRegistry; @@ -63,7 +64,7 @@ use crate::{ }; use lazy_static::lazy_static; use log::{error, warn}; -use notifications::NotificationHandle; +use notifications::{NotificationHandle, NotifyResultExt}; pub use pane::*; pub use pane_group::*; use persistence::{model::SerializedItem, DB}; @@ -116,7 +117,8 @@ actions!( NewTerminal, NewSearch, Feedback, - Restart + Restart, + Welcome ] ); @@ -185,21 +187,66 @@ pub fn init(app_state: Arc, cx: &mut MutableAppContext) { dock::init(cx); notifications::init(cx); - cx.add_global_action(open); + cx.add_global_action(|_: &Open, cx: &mut MutableAppContext| { + let mut paths = cx.prompt_for_paths(PathPromptOptions { + files: true, + directories: true, + multiple: true, + }); + + cx.spawn(|mut cx| async move { + if let Some(paths) = paths.recv().await.flatten() { + cx.update(|cx| cx.dispatch_global_action(OpenPaths { paths })); + } + }) + .detach(); + }); + cx.add_action(|_, _: &Open, cx: &mut ViewContext| { + let mut paths = cx.prompt_for_paths(PathPromptOptions { + files: true, + directories: true, + multiple: true, + }); + + let handle = cx.handle().downgrade(); + cx.spawn(|_, mut cx| async move { + if let Some(paths) = paths.recv().await.flatten() { + cx.update(|cx| { + cx.dispatch_action_at(handle.window_id(), handle.id(), OpenPaths { paths }) + }) + } + }) + .detach(); + }); cx.add_global_action({ let app_state = Arc::downgrade(&app_state); move |action: &OpenPaths, cx: &mut MutableAppContext| { if let Some(app_state) = app_state.upgrade() { - open_paths(&action.paths, &app_state, cx).detach(); + open_paths(&action.paths, &app_state, None, cx).detach(); } } }); - cx.add_global_action({ + cx.add_async_action({ let app_state = Arc::downgrade(&app_state); - move |_: &NewFile, cx: &mut MutableAppContext| { - if let Some(app_state) = app_state.upgrade() { - open_new(&app_state, cx).detach(); + move |workspace, action: &OpenPaths, cx: &mut ViewContext| { + if !workspace.project().read(cx).is_local() { + cx.propagate_action(); + return None; } + + let app_state = app_state.upgrade()?; + let window_id = cx.window_id(); + let action = action.clone(); + let close = workspace.prepare_to_close(false, cx); + + Some(cx.spawn_weak(|_, mut cx| async move { + let can_close = close.await?; + if can_close { + cx.update(|cx| open_paths(&action.paths, &app_state, Some(window_id), cx)) + .await; + } + Ok(()) + })) } }); @@ -207,7 +254,7 @@ pub fn init(app_state: Arc, cx: &mut MutableAppContext) { let app_state = Arc::downgrade(&app_state); move |_: &NewWindow, cx: &mut MutableAppContext| { if let Some(app_state) = app_state.upgrade() { - open_new(&app_state, cx).detach(); + open_new(&app_state, cx, |_, cx| cx.dispatch_action(NewFile)).detach(); } } }); @@ -273,6 +320,31 @@ pub fn init(app_state: Arc, cx: &mut MutableAppContext) { }, ); + cx.add_action(|_: &mut Workspace, _: &install_cli::Install, cx| { + cx.spawn(|workspace, mut cx| async move { + let err = install_cli::install_cli(&cx) + .await + .context("Failed to create CLI symlink"); + + cx.update(|cx| { + workspace.update(cx, |workspace, cx| { + if matches!(err, Err(_)) { + err.notify_err(workspace, cx); + } else { + workspace.show_notification(1, cx, |cx| { + cx.add_view(|_| { + MessageNotification::new_message( + "Successfully installed the `zed` binary", + ) + }) + }); + } + }) + }) + }) + .detach(); + }); + let client = &app_state.client; client.add_view_request_handler(Workspace::handle_follow); client.add_view_message_handler(Workspace::handle_unfollow); @@ -358,6 +430,7 @@ pub struct AppState { fn(Option, Option, &dyn Platform) -> WindowOptions<'static>, pub initialize_workspace: fn(&mut Workspace, &Arc, &mut ViewContext), pub dock_default_item_factory: DockDefaultItemFactory, + pub background_actions: BackgroundActions, } impl AppState { @@ -380,7 +453,8 @@ impl AppState { user_store, initialize_workspace: |_, _, _| {}, build_window_options: |_, _, _| Default::default(), - dock_default_item_factory: |_, _| unimplemented!(), + dock_default_item_factory: |_, _| None, + background_actions: || &[], }) } } @@ -468,6 +542,8 @@ pub struct Workspace { active_call: Option<(ModelHandle, Vec)>, leader_updates_tx: mpsc::UnboundedSender<(PeerId, proto::UpdateFollowers)>, database_id: WorkspaceId, + background_actions: BackgroundActions, + _window_subscriptions: [Subscription; 3], _apply_leader_updates: Task>, _observe_current_user: Task<()>, } @@ -497,12 +573,9 @@ impl Workspace { workspace_id: WorkspaceId, project: ModelHandle, dock_default_factory: DockDefaultItemFactory, + background_actions: BackgroundActions, cx: &mut ViewContext, ) -> Self { - cx.observe_fullscreen(|_, _, cx| cx.notify()).detach(); - - cx.observe_window_activation(Self::on_window_activation_changed) - .detach(); cx.observe(&project, |_, _, cx| cx.notify()).detach(); cx.subscribe(&project, move |this, _, event, cx| { match event { @@ -531,7 +604,10 @@ impl Workspace { }) .detach(); - let center_pane = cx.add_view(|cx| Pane::new(None, cx)); + let weak_handle = cx.weak_handle(); + + let center_pane = + cx.add_view(|cx| Pane::new(weak_handle.id(), None, background_actions, cx)); let pane_id = center_pane.id(); cx.subscribe(¢er_pane, move |this, _, event, cx| { this.handle_pane_event(pane_id, event, cx) @@ -539,7 +615,12 @@ impl Workspace { .detach(); cx.focus(¢er_pane); cx.emit(Event::PaneAdded(center_pane.clone())); - let dock = Dock::new(dock_default_factory, cx); + let dock = Dock::new( + weak_handle.id(), + dock_default_factory, + background_actions, + cx, + ); let dock_pane = dock.pane().clone(); let fs = project.read(cx).fs().clone(); @@ -562,7 +643,6 @@ impl Workspace { } }); let handle = cx.handle(); - let weak_handle = cx.weak_handle(); // All leader updates are enqueued and then processed in a single task, so // that each asynchronous operation can be run in order. @@ -607,6 +687,28 @@ impl Workspace { active_call = Some((call, subscriptions)); } + let subscriptions = [ + cx.observe_fullscreen(|_, _, cx| cx.notify()), + cx.observe_window_activation(Self::on_window_activation_changed), + cx.observe_window_bounds(move |_, mut bounds, display, cx| { + // Transform fixed bounds to be stored in terms of the containing display + if let WindowBounds::Fixed(mut window_bounds) = bounds { + if let Some(screen) = cx.platform().screen_by_id(display) { + let screen_bounds = screen.bounds(); + window_bounds + .set_origin_x(window_bounds.origin_x() - screen_bounds.origin_x()); + window_bounds + .set_origin_y(window_bounds.origin_y() - screen_bounds.origin_y()); + bounds = WindowBounds::Fixed(window_bounds); + } + } + + cx.background() + .spawn(DB.set_window_bounds(workspace_id, bounds, display)) + .detach_and_log_err(cx); + }), + ]; + let mut this = Workspace { modal: None, weak_self: weak_handle.clone(), @@ -635,9 +737,11 @@ impl Workspace { window_edited: false, active_call, database_id: workspace_id, + background_actions, _observe_current_user, _apply_leader_updates, leader_updates_tx, + _window_subscriptions: subscriptions, }; this.project_remote_id_changed(project.read(cx).remote_id(), cx); cx.defer(|this, cx| this.update_window_title(cx)); @@ -646,6 +750,10 @@ impl Workspace { cx.defer(move |_, cx| { Self::load_from_serialized_workspace(weak_handle, serialized_workspace, cx) }); + } else { + if cx.global::().default_dock_anchor != DockAnchor::Expanded { + Dock::show(&mut this, false, cx); + } } this @@ -654,6 +762,7 @@ impl Workspace { fn new_local( abs_paths: Vec, app_state: Arc, + requesting_window_id: Option, cx: &mut MutableAppContext, ) -> Task<( ViewHandle, @@ -709,73 +818,65 @@ impl Workspace { )) }); - let (bounds, display) = if let Some(bounds) = window_bounds_override { - (Some(bounds), None) - } else { - serialized_workspace - .as_ref() - .and_then(|serialized_workspace| { - let display = serialized_workspace.display?; - let mut bounds = serialized_workspace.bounds?; - - // Stored bounds are relative to the containing display. - // So convert back to global coordinates if that screen still exists - if let WindowBounds::Fixed(mut window_bounds) = bounds { - if let Some(screen) = cx.platform().screen_by_id(display) { - let screen_bounds = screen.bounds(); - window_bounds.set_origin_x( - window_bounds.origin_x() + screen_bounds.origin_x(), - ); - window_bounds.set_origin_y( - window_bounds.origin_y() + screen_bounds.origin_y(), - ); - bounds = WindowBounds::Fixed(window_bounds); - } else { - // Screen no longer exists. Return none here. - return None; - } - } - - Some((bounds, display)) - }) - .unzip() - }; - - // Use the serialized workspace to construct the new window - let (_, workspace) = cx.add_window( - (app_state.build_window_options)(bounds, display, cx.platform().as_ref()), - |cx| { + let build_workspace = + |cx: &mut ViewContext, + serialized_workspace: Option| { let mut workspace = Workspace::new( serialized_workspace, workspace_id, project_handle, app_state.dock_default_item_factory, + app_state.background_actions, cx, ); (app_state.initialize_workspace)(&mut workspace, &app_state, cx); - cx.observe_window_bounds(move |_, mut bounds, display, cx| { - // Transform fixed bounds to be stored in terms of the containing display - if let WindowBounds::Fixed(mut window_bounds) = bounds { - if let Some(screen) = cx.platform().screen_by_id(display) { - let screen_bounds = screen.bounds(); - window_bounds.set_origin_x( - window_bounds.origin_x() - screen_bounds.origin_x(), - ); - window_bounds.set_origin_y( - window_bounds.origin_y() - screen_bounds.origin_y(), - ); - bounds = WindowBounds::Fixed(window_bounds); + workspace + }; + + let workspace = if let Some(window_id) = requesting_window_id { + cx.update(|cx| { + cx.replace_root_view(window_id, |cx| build_workspace(cx, serialized_workspace)) + }) + } else { + let (bounds, display) = if let Some(bounds) = window_bounds_override { + (Some(bounds), None) + } else { + serialized_workspace + .as_ref() + .and_then(|serialized_workspace| { + let display = serialized_workspace.display?; + let mut bounds = serialized_workspace.bounds?; + + // Stored bounds are relative to the containing display. + // So convert back to global coordinates if that screen still exists + if let WindowBounds::Fixed(mut window_bounds) = bounds { + if let Some(screen) = cx.platform().screen_by_id(display) { + let screen_bounds = screen.bounds(); + window_bounds.set_origin_x( + window_bounds.origin_x() + screen_bounds.origin_x(), + ); + window_bounds.set_origin_y( + window_bounds.origin_y() + screen_bounds.origin_y(), + ); + bounds = WindowBounds::Fixed(window_bounds); + } else { + // Screen no longer exists. Return none here. + return None; + } } - } - cx.background() - .spawn(DB.set_window_bounds(workspace_id, bounds, display)) - .detach_and_log_err(cx); - }) - .detach(); - workspace - }, - ); + Some((bounds, display)) + }) + .unzip() + }; + + // Use the serialized workspace to construct the new window + cx.add_window( + (app_state.build_window_options)(bounds, display, cx.platform().as_ref()), + |cx| build_workspace(cx, serialized_workspace), + ) + .1 + }; notify_if_database_failed(&workspace, &mut cx); @@ -871,7 +972,7 @@ impl Workspace { if self.project.read(cx).is_local() { Task::Ready(Some(callback(self, cx))) } else { - let task = Self::new_local(Vec::new(), app_state.clone(), cx); + let task = Self::new_local(Vec::new(), app_state.clone(), None, cx); cx.spawn(|_vh, mut cx| async move { let (workspace, _) = task.await; workspace.update(&mut cx, callback) @@ -1340,7 +1441,8 @@ impl Workspace { } fn add_pane(&mut self, cx: &mut ViewContext) -> ViewHandle { - let pane = cx.add_view(|cx| Pane::new(None, cx)); + let pane = + cx.add_view(|cx| Pane::new(self.weak_handle().id(), None, self.background_actions, cx)); let pane_id = pane.id(); cx.subscribe(&pane, move |this, _, event, cx| { this.handle_pane_event(pane_id, event, cx) @@ -1352,6 +1454,23 @@ impl Workspace { pane } + pub fn add_item_to_center( + &mut self, + item: Box, + cx: &mut ViewContext, + ) -> bool { + if let Some(center_pane) = self.last_active_center_pane.clone() { + if let Some(center_pane) = center_pane.upgrade(cx) { + Pane::add_item(self, ¢er_pane, item, true, true, None, cx); + true + } else { + false + } + } else { + false + } + } + pub fn add_item(&mut self, item: Box, cx: &mut ViewContext) { let active_pane = self.active_pane().clone(); Pane::add_item(self, &active_pane, item, true, true, None, cx); @@ -1509,7 +1628,7 @@ impl Workspace { self.active_item_path_changed(cx); if &pane == self.dock_pane() { - Dock::show(self, cx); + Dock::show(self, true, cx); } else { self.last_active_center_pane = Some(pane.downgrade()); if self.dock.is_anchored_at(DockAnchor::Expanded) { @@ -2522,7 +2641,12 @@ impl Workspace { // the focus the dock generates start generating alternating // focus due to the deferred execution each triggering each other cx.after_window_update(move |workspace, cx| { - Dock::set_dock_position(workspace, serialized_workspace.dock_position, cx); + Dock::set_dock_position( + workspace, + serialized_workspace.dock_position, + true, + cx, + ); }); cx.notify(); @@ -2534,6 +2658,11 @@ impl Workspace { }) .detach(); } + + #[cfg(any(test, feature = "test-support"))] + pub fn test_new(project: ModelHandle, cx: &mut ViewContext) -> Self { + Self::new(None, 0, project, |_, _| None, || &[], cx) + } } fn notify_if_database_failed(workspace: &ViewHandle, cx: &mut AsyncAppContext) { @@ -2765,20 +2894,6 @@ impl std::fmt::Debug for OpenPaths { } } -fn open(_: &Open, cx: &mut MutableAppContext) { - let mut paths = cx.prompt_for_paths(PathPromptOptions { - files: true, - directories: true, - multiple: true, - }); - cx.spawn(|mut cx| async move { - if let Some(paths) = paths.recv().await.flatten() { - cx.update(|cx| cx.dispatch_global_action(OpenPaths { paths })); - } - }) - .detach(); -} - pub struct WorkspaceCreated(WeakViewHandle); pub fn activate_workspace_for_project( @@ -2805,6 +2920,7 @@ pub async fn last_opened_workspace_paths() -> Option { pub fn open_paths( abs_paths: &[PathBuf], app_state: &Arc, + requesting_window_id: Option, cx: &mut MutableAppContext, ) -> Task<( ViewHandle, @@ -2835,7 +2951,8 @@ pub fn open_paths( .contains(&false); cx.update(|cx| { - let task = Workspace::new_local(abs_paths, app_state.clone(), cx); + let task = + Workspace::new_local(abs_paths, app_state.clone(), requesting_window_id, cx); cx.spawn(|mut cx| async move { let (workspace, items) = task.await; @@ -2854,14 +2971,18 @@ pub fn open_paths( }) } -pub fn open_new(app_state: &Arc, cx: &mut MutableAppContext) -> Task<()> { - let task = Workspace::new_local(Vec::new(), app_state.clone(), cx); +pub fn open_new( + app_state: &Arc, + cx: &mut MutableAppContext, + init: impl FnOnce(&mut Workspace, &mut ViewContext) + 'static, +) -> Task<()> { + let task = Workspace::new_local(Vec::new(), app_state.clone(), None, cx); cx.spawn(|mut cx| async move { let (workspace, opened_paths) = task.await; - workspace.update(&mut cx, |_, cx| { + workspace.update(&mut cx, |workspace, cx| { if opened_paths.is_empty() { - cx.dispatch_action(NewFile); + init(workspace, cx) } }) }) @@ -2882,17 +3003,10 @@ mod tests { use super::*; use fs::FakeFs; - use gpui::{executor::Deterministic, TestAppContext, ViewContext}; + use gpui::{executor::Deterministic, TestAppContext}; use project::{Project, ProjectEntryId}; use serde_json::json; - pub fn default_item_factory( - _workspace: &mut Workspace, - _cx: &mut ViewContext, - ) -> Option> { - unimplemented!() - } - #[gpui::test] async fn test_tab_disambiguation(cx: &mut TestAppContext) { cx.foreground().forbid_parking(); @@ -2905,7 +3019,8 @@ mod tests { Default::default(), 0, project.clone(), - default_item_factory, + |_, _| None, + || &[], cx, ) }); @@ -2977,7 +3092,8 @@ mod tests { Default::default(), 0, project.clone(), - default_item_factory, + |_, _| None, + || &[], cx, ) }); @@ -3077,7 +3193,8 @@ mod tests { Default::default(), 0, project.clone(), - default_item_factory, + |_, _| None, + || &[], cx, ) }); @@ -3116,7 +3233,7 @@ mod tests { let project = Project::test(fs, None, cx).await; let (window_id, workspace) = cx.add_window(|cx| { - Workspace::new(Default::default(), 0, project, default_item_factory, cx) + Workspace::new(Default::default(), 0, project, |_, _| None, || &[], cx) }); let item1 = cx.add_view(&workspace, |cx| { @@ -3225,7 +3342,7 @@ mod tests { let project = Project::test(fs, [], cx).await; let (window_id, workspace) = cx.add_window(|cx| { - Workspace::new(Default::default(), 0, project, default_item_factory, cx) + Workspace::new(Default::default(), 0, project, |_, _| None, || &[], cx) }); // Create several workspace items with single project entries, and two @@ -3334,7 +3451,7 @@ mod tests { let project = Project::test(fs, [], cx).await; let (window_id, workspace) = cx.add_window(|cx| { - Workspace::new(Default::default(), 0, project, default_item_factory, cx) + Workspace::new(Default::default(), 0, project, |_, _| None, || &[], cx) }); let item = cx.add_view(&workspace, |cx| { @@ -3453,7 +3570,7 @@ mod tests { let project = Project::test(fs, [], cx).await; let (_, workspace) = cx.add_window(|cx| { - Workspace::new(Default::default(), 0, project, default_item_factory, cx) + Workspace::new(Default::default(), 0, project, |_, _| None, || &[], cx) }); let item = cx.add_view(&workspace, |cx| { diff --git a/crates/zed/Cargo.toml b/crates/zed/Cargo.toml index 19c9a3d727704176f7d36b216719a3584d8c4a8d..a1e3af98f8535ad077016544945d7a92b3604c36 100644 --- a/crates/zed/Cargo.toml +++ b/crates/zed/Cargo.toml @@ -3,7 +3,7 @@ authors = ["Nathan Sobo "] description = "The fast, collaborative code editor." edition = "2021" name = "zed" -version = "0.76.0" +version = "0.77.0" publish = false [lib] @@ -29,6 +29,7 @@ context_menu = { path = "../context_menu" } client = { path = "../client" } clock = { path = "../clock" } diagnostics = { path = "../diagnostics" } +db = { path = "../db" } editor = { path = "../editor" } feedback = { path = "../feedback" } file_finder = { path = "../file_finder" } @@ -38,8 +39,10 @@ fsevent = { path = "../fsevent" } fuzzy = { path = "../fuzzy" } go_to_line = { path = "../go_to_line" } gpui = { path = "../gpui" } +install_cli = { path = "../install_cli" } journal = { path = "../journal" } language = { path = "../language" } +language_selector = { path = "../language_selector" } lsp = { path = "../lsp" } outline = { path = "../outline" } plugin_runtime = { path = "../plugin_runtime" } @@ -58,6 +61,7 @@ theme_testbench = { path = "../theme_testbench" } util = { path = "../util" } vim = { path = "../vim" } workspace = { path = "../workspace" } +welcome = { path = "../welcome" } anyhow = "1.0.38" async-compression = { version = "0.3", features = ["gzip", "futures-bufread"] } async-tar = "0.4.2" diff --git a/crates/zed/src/main.rs b/crates/zed/src/main.rs index b9e3ed550beb434c7da92138308131f09f3c8ac2..8cf6879f7abcd5163463d0d6dc9ca2baadc7324d 100644 --- a/crates/zed/src/main.rs +++ b/crates/zed/src/main.rs @@ -13,11 +13,12 @@ use client::{ http::{self, HttpClient}, UserStore, ZED_APP_VERSION, ZED_SECRET_CLIENT_TOKEN, }; +use db::kvp::KEY_VALUE_STORE; use futures::{ channel::{mpsc, oneshot}, FutureExt, SinkExt, StreamExt, }; -use gpui::{App, AssetSource, AsyncAppContext, MutableAppContext, Task, ViewContext}; +use gpui::{Action, App, AssetSource, AsyncAppContext, MutableAppContext, Task, ViewContext}; use isahc::{config::Configurable, Request}; use language::LanguageRegistry; use log::LevelFilter; @@ -35,17 +36,19 @@ use std::{ path::PathBuf, sync::Arc, thread, time::Duration, }; use terminal_view::{get_working_directory, TerminalView}; +use welcome::{show_welcome_experience, FIRST_OPEN}; use fs::RealFs; -use settings::watched_json::{watch_keymap_file, watch_settings_file, WatchedJsonFile}; +use settings::watched_json::WatchedJsonFile; use theme::ThemeRegistry; #[cfg(debug_assertions)] use util::StaffMode; use util::{channel::RELEASE_CHANNEL, paths, ResultExt, TryFutureExt}; use workspace::{ - self, item::ItemHandle, notifications::NotifyResultExt, AppState, NewFile, OpenPaths, Workspace, + self, dock::FocusDock, item::ItemHandle, notifications::NotifyResultExt, AppState, NewFile, + OpenPaths, Workspace, }; -use zed::{self, build_window_options, initialize_workspace, languages, menus}; +use zed::{self, build_window_options, initialize_workspace, languages, menus, OpenSettings}; fn main() { let http = http::client(); @@ -119,7 +122,14 @@ fn main() { fs.clone(), )); - watch_settings_file(default_settings, settings_file_content, themes.clone(), cx); + settings::watch_files( + default_settings, + settings_file_content, + themes.clone(), + keymap_file, + cx, + ); + if !stdout_is_a_pty() { upload_previous_panics(http.clone(), cx); } @@ -132,8 +142,6 @@ fn main() { languages::init(languages.clone()); let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http.clone(), cx)); - watch_keymap_file(keymap_file, cx); - cx.set_global(client.clone()); context_menu::init(cx); @@ -179,16 +187,19 @@ fn main() { build_window_options, initialize_workspace, dock_default_item_factory, + background_actions, }); auto_update::init(http, client::ZED_SERVER_URL.clone(), cx); workspace::init(app_state.clone(), cx); journal::init(app_state.clone(), cx); + language_selector::init(app_state.clone(), cx); theme_selector::init(app_state.clone(), cx); zed::init(&app_state, cx); collab_ui::init(app_state.clone(), cx); feedback::init(app_state.clone(), cx); + welcome::init(cx); cx.set_menus(menus::menus()); @@ -196,7 +207,7 @@ fn main() { cx.platform().activate(true); let paths = collect_path_args(); if paths.is_empty() { - cx.spawn(|cx| async move { restore_or_create_workspace(cx).await }) + cx.spawn(|cx| async move { restore_or_create_workspace(&app_state, cx).await }) .detach() } else { cx.dispatch_global_action(OpenPaths { paths }); @@ -206,11 +217,14 @@ fn main() { cx.spawn(|cx| handle_cli_connection(connection, app_state.clone(), cx)) .detach(); } else if let Ok(Some(paths)) = open_paths_rx.try_next() { - cx.update(|cx| workspace::open_paths(&paths, &app_state, cx)) + cx.update(|cx| workspace::open_paths(&paths, &app_state, None, cx)) .detach(); } else { - cx.spawn(|cx| async move { restore_or_create_workspace(cx).await }) - .detach() + cx.spawn({ + let app_state = app_state.clone(); + |cx| async move { restore_or_create_workspace(&app_state, cx).await } + }) + .detach() } cx.spawn(|cx| { @@ -227,8 +241,7 @@ fn main() { let app_state = app_state.clone(); async move { while let Some(paths) = open_paths_rx.next().await { - log::error!("OPEN PATHS FROM HANDLE"); - cx.update(|cx| workspace::open_paths(&paths, &app_state, cx)) + cx.update(|cx| workspace::open_paths(&paths, &app_state, None, cx)) .detach(); } } @@ -250,13 +263,15 @@ fn main() { }); } -async fn restore_or_create_workspace(mut cx: AsyncAppContext) { +async fn restore_or_create_workspace(app_state: &Arc, mut cx: AsyncAppContext) { if let Some(location) = workspace::last_opened_workspace_paths().await { cx.update(|cx| { cx.dispatch_global_action(OpenPaths { paths: location.paths().as_ref().clone(), }) }); + } else if matches!(KEY_VALUE_STORE.read_kvp(FIRST_OPEN), Ok(None)) { + cx.update(|cx| show_welcome_experience(app_state, cx)); } else { cx.update(|cx| { cx.dispatch_global_action(NewFile); @@ -590,7 +605,7 @@ async fn handle_cli_connection( paths }; let (workspace, items) = cx - .update(|cx| workspace::open_paths(&paths, &app_state, cx)) + .update(|cx| workspace::open_paths(&paths, &app_state, None, cx)) .await; let mut errored = false; @@ -691,3 +706,13 @@ pub fn dock_default_item_factory( Some(Box::new(terminal_view)) } + +pub fn background_actions() -> &'static [(&'static str, &'static dyn Action)] { + &[ + ("Go to file", &file_finder::Toggle), + ("Open command palette", &command_palette::Toggle), + ("Focus the dock", &FocusDock), + ("Open recent projects", &recent_projects::OpenRecent), + ("Change your settings", &OpenSettings), + ] +} diff --git a/crates/zed/src/menus.rs b/crates/zed/src/menus.rs index bb519c7a9505bc2de07fa60c70014be3b4fbc169..e0a2f7473193582b5c3e6e9eb19ec697e625cda9 100644 --- a/crates/zed/src/menus.rs +++ b/crates/zed/src/menus.rs @@ -19,7 +19,7 @@ pub fn menus() -> Vec> { MenuItem::action("Select Theme", theme_selector::Toggle), ], }), - MenuItem::action("Install CLI", super::InstallCommandLineInterface), + MenuItem::action("Install CLI", install_cli::Install), MenuItem::separator(), MenuItem::action("Hide Zed", super::Hide), MenuItem::action("Hide Others", super::HideOthers), @@ -137,9 +137,11 @@ pub fn menus() -> Vec> { items: vec![ MenuItem::action("Command Palette", command_palette::Toggle), MenuItem::separator(), - MenuItem::action("View Telemetry Log", crate::OpenTelemetryLog), + MenuItem::action("View Telemetry", crate::OpenTelemetryLog), MenuItem::action("View Dependency Licenses", crate::OpenLicenses), + MenuItem::action("Show Welcome", workspace::Welcome), MenuItem::separator(), + MenuItem::action("Give us feedback", feedback::feedback_editor::GiveFeedback), MenuItem::action( "Copy System Specs Into Clipboard", feedback::CopySystemSpecsIntoClipboard, diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index 0d9d094f871fa6d6d53c2ff1c474f4e203bdab91..00e67f63b3635e032510aee6ab941a3ccdbedc50 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -2,7 +2,7 @@ pub mod languages; pub mod menus; #[cfg(any(test, feature = "test-support"))] pub mod test; -use anyhow::{anyhow, Context, Result}; +use anyhow::Context; use assets::Assets; use breadcrumbs::Breadcrumbs; pub use client; @@ -20,7 +20,7 @@ use gpui::{ geometry::vector::vec2f, impl_actions, platform::{WindowBounds, WindowOptions}, - AssetSource, AsyncAppContext, Platform, PromptLevel, TitlebarOptions, ViewContext, WindowKind, + AssetSource, Platform, PromptLevel, TitlebarOptions, ViewContext, WindowKind, }; use language::Rope; pub use lsp; @@ -66,7 +66,6 @@ actions!( IncreaseBufferFontSize, DecreaseBufferFontSize, ResetBufferFontSize, - InstallCommandLineInterface, ResetDatabase, ] ); @@ -142,9 +141,13 @@ pub fn init(app_state: &Arc, cx: &mut gpui::MutableAppContext) { cx.refresh_windows(); }); }); - cx.add_global_action(move |_: &InstallCommandLineInterface, cx| { - cx.spawn(|cx| async move { install_cli(&cx).await.context("error creating CLI symlink") }) - .detach_and_log_err(cx); + cx.add_global_action(move |_: &install_cli::Install, cx| { + cx.spawn(|cx| async move { + install_cli::install_cli(&cx) + .await + .context("error creating CLI symlink") + }) + .detach_and_log_err(cx); }); cx.add_action({ let app_state = app_state.clone(); @@ -169,9 +172,8 @@ pub fn init(app_state: &Arc, cx: &mut gpui::MutableAppContext) { }); cx.add_action({ let app_state = app_state.clone(); - move |workspace: &mut Workspace, _: &OpenLicenses, cx: &mut ViewContext| { + move |_: &mut Workspace, _: &OpenLicenses, cx: &mut ViewContext| { open_bundled_file( - workspace, app_state.clone(), "licenses.md", "Open Source License Attribution", @@ -194,9 +196,8 @@ pub fn init(app_state: &Arc, cx: &mut gpui::MutableAppContext) { }); cx.add_action({ let app_state = app_state.clone(); - move |workspace: &mut Workspace, _: &OpenDefaultKeymap, cx: &mut ViewContext| { + move |_: &mut Workspace, _: &OpenDefaultKeymap, cx: &mut ViewContext| { open_bundled_file( - workspace, app_state.clone(), "keymaps/default.json", "Default Key Bindings", @@ -207,11 +208,8 @@ pub fn init(app_state: &Arc, cx: &mut gpui::MutableAppContext) { }); cx.add_action({ let app_state = app_state.clone(); - move |workspace: &mut Workspace, - _: &OpenDefaultSettings, - cx: &mut ViewContext| { + move |_: &mut Workspace, _: &OpenDefaultSettings, cx: &mut ViewContext| { open_bundled_file( - workspace, app_state.clone(), "settings/default.json", "Default Settings", @@ -220,32 +218,41 @@ pub fn init(app_state: &Arc, cx: &mut gpui::MutableAppContext) { ); } }); - cx.add_action( - |workspace: &mut Workspace, _: &DebugElements, cx: &mut ViewContext| { + cx.add_action({ + let app_state = app_state.clone(); + move |_: &mut Workspace, _: &DebugElements, cx: &mut ViewContext| { + let app_state = app_state.clone(); + let markdown = app_state.languages.language_for_name("JSON"); let content = to_string_pretty(&cx.debug_elements()).unwrap(); - let project = workspace.project().clone(); - let json_language = project - .read(cx) - .languages() - .language_for_name("JSON") - .unwrap(); - if project.read(cx).is_remote() { - cx.propagate_action(); - } else if let Some(buffer) = project - .update(cx, |project, cx| { - project.create_buffer(&content, Some(json_language), cx) - }) - .log_err() - { - workspace.add_item( - Box::new( - cx.add_view(|cx| Editor::for_buffer(buffer, Some(project.clone()), cx)), - ), - cx, - ); - } - }, - ); + cx.spawn(|workspace, mut cx| async move { + let markdown = markdown.await.log_err(); + workspace + .update(&mut cx, |workspace, cx| { + workspace.with_local_workspace(&app_state, cx, move |workspace, cx| { + let project = workspace.project().clone(); + + let buffer = project + .update(cx, |project, cx| { + project.create_buffer(&content, markdown, cx) + }) + .expect("creating buffers on a local workspace always succeeds"); + let buffer = cx.add_model(|cx| { + MultiBuffer::singleton(buffer, cx) + .with_title("Debug Elements".into()) + }); + workspace.add_item( + Box::new(cx.add_view(|cx| { + Editor::for_multibuffer(buffer, Some(project.clone()), cx) + })), + cx, + ); + }) + }) + .await; + }) + .detach(); + } + }); cx.add_action( |workspace: &mut Workspace, _: &project_panel::ToggleFocus, @@ -253,7 +260,6 @@ pub fn init(app_state: &Arc, cx: &mut gpui::MutableAppContext) { workspace.toggle_sidebar_item_focus(SidebarSide::Left, 0, cx); }, ); - activity_indicator::init(cx); call::init(app_state.client.clone(), app_state.user_store.clone(), cx); settings::KeymapFileContent::load_defaults(cx); @@ -337,18 +343,19 @@ pub fn initialize_workspace( cx.add_view(|cx| diagnostics::items::DiagnosticIndicator::new(workspace.project(), cx)); let activity_indicator = activity_indicator::ActivityIndicator::new(workspace, app_state.languages.clone(), cx); - let cursor_position = cx.add_view(|_| editor::items::CursorPosition::new()); + let active_buffer_language = cx.add_view(|_| language_selector::ActiveBufferLanguage::new()); let feedback_button = - cx.add_view(|_| feedback::deploy_feedback_button::DeployFeedbackButton {}); + cx.add_view(|_| feedback::deploy_feedback_button::DeployFeedbackButton::new()); + let cursor_position = cx.add_view(|_| editor::items::CursorPosition::new()); workspace.status_bar().update(cx, |status_bar, cx| { status_bar.add_left_item(diagnostic_summary, cx); status_bar.add_left_item(activity_indicator, cx); - // TODO: Remove this when things are done if **cx.default_global::() { status_bar.add_right_item(toggle_terminal, cx); } - status_bar.add_right_item(cursor_position, cx); status_bar.add_right_item(feedback_button, cx); + status_bar.add_right_item(active_buffer_language, cx); + status_bar.add_right_item(cursor_position, cx); }); auto_update::notify_of_any_new_update(cx.weak_handle(), cx); @@ -485,54 +492,6 @@ fn about(_: &mut Workspace, _: &About, cx: &mut gpui::ViewContext) { ); } -async fn install_cli(cx: &AsyncAppContext) -> Result<()> { - let cli_path = cx.platform().path_for_auxiliary_executable("cli")?; - let link_path = Path::new("/usr/local/bin/zed"); - let bin_dir_path = link_path.parent().unwrap(); - - // Don't re-create symlink if it points to the same CLI binary. - if smol::fs::read_link(link_path).await.ok().as_ref() == Some(&cli_path) { - return Ok(()); - } - - // If the symlink is not there or is outdated, first try replacing it - // without escalating. - smol::fs::remove_file(link_path).await.log_err(); - if smol::fs::unix::symlink(&cli_path, link_path) - .await - .log_err() - .is_some() - { - return Ok(()); - } - - // The symlink could not be created, so use osascript with admin privileges - // to create it. - let status = smol::process::Command::new("osascript") - .args([ - "-e", - &format!( - "do shell script \" \ - mkdir -p \'{}\' && \ - ln -sf \'{}\' \'{}\' \ - \" with administrator privileges", - bin_dir_path.to_string_lossy(), - cli_path.to_string_lossy(), - link_path.to_string_lossy(), - ), - ]) - .stdout(smol::process::Stdio::inherit()) - .stderr(smol::process::Stdio::inherit()) - .output() - .await? - .status; - if status.success() { - Ok(()) - } else { - Err(anyhow!("error running osascript")) - } -} - fn open_config_file( path: &'static Path, app_state: Arc, @@ -626,8 +585,13 @@ fn open_telemetry_log_file( workspace.with_local_workspace(&app_state.clone(), cx, move |_, cx| { cx.spawn_weak(|workspace, mut cx| async move { let workspace = workspace.upgrade(&cx)?; - let path = app_state.client.telemetry_log_file_path()?; - let log = app_state.fs.load(&path).await.log_err()?; + + async fn fetch_log_string(app_state: &Arc) -> Option { + let path = app_state.client.telemetry_log_file_path()?; + app_state.fs.load(&path).await.log_err() + } + + let log = fetch_log_string(&app_state).await.unwrap_or_else(|| "// No data has been collected yet".to_string()); const MAX_TELEMETRY_LOG_LEN: usize = 5 * 1024 * 1024; let mut start_offset = log.len().saturating_sub(MAX_TELEMETRY_LOG_LEN); @@ -635,6 +599,7 @@ fn open_telemetry_log_file( start_offset += newline_offset + 1; } let log_suffix = &log[start_offset..]; + let json = app_state.languages.language_for_name("JSON").await.log_err(); workspace.update(&mut cx, |workspace, cx| { let project = workspace.project().clone(); @@ -642,7 +607,7 @@ fn open_telemetry_log_file( .update(cx, |project, cx| project.create_buffer("", None, cx)) .expect("creating buffers on a local workspace always succeeds"); buffer.update(cx, |buffer, cx| { - buffer.set_language(app_state.languages.language_for_name("JSON"), cx); + buffer.set_language(json, cx); buffer.edit( [( 0..0, @@ -675,35 +640,42 @@ fn open_telemetry_log_file( } fn open_bundled_file( - workspace: &mut Workspace, app_state: Arc, asset_path: &'static str, title: &'static str, language: &'static str, cx: &mut ViewContext, ) { - workspace - .with_local_workspace(&app_state, cx, |workspace, cx| { - let project = workspace.project().clone(); - let buffer = project.update(cx, |project, cx| { - let text = Assets::get(asset_path) - .map(|f| f.data) - .unwrap_or_else(|| Cow::Borrowed(b"File not found")); - let text = str::from_utf8(text.as_ref()).unwrap(); - project - .create_buffer(text, project.languages().language_for_name(language), cx) - .expect("creating buffers on a local workspace always succeeds") - }); - let buffer = - cx.add_model(|cx| MultiBuffer::singleton(buffer, cx).with_title(title.into())); - workspace.add_item( - Box::new( - cx.add_view(|cx| Editor::for_multibuffer(buffer, Some(project.clone()), cx)), - ), - cx, - ); - }) - .detach(); + let language = app_state.languages.language_for_name(language); + cx.spawn(|workspace, mut cx| async move { + let language = language.await.log_err(); + workspace + .update(&mut cx, |workspace, cx| { + workspace.with_local_workspace(&app_state, cx, |workspace, cx| { + let project = workspace.project(); + let buffer = project.update(cx, |project, cx| { + let text = Assets::get(asset_path) + .map(|f| f.data) + .unwrap_or_else(|| Cow::Borrowed(b"File not found")); + let text = str::from_utf8(text.as_ref()).unwrap(); + project + .create_buffer(text, language, cx) + .expect("creating buffers on a local workspace always succeeds") + }); + let buffer = cx.add_model(|cx| { + MultiBuffer::singleton(buffer, cx).with_title(title.into()) + }); + workspace.add_item( + Box::new(cx.add_view(|cx| { + Editor::for_multibuffer(buffer, Some(project.clone()), cx) + })), + cx, + ); + }) + }) + .await; + }) + .detach(); } fn schema_file_match(path: &Path) -> &Path { @@ -753,6 +725,10 @@ mod tests { "ca": null, "cb": null, }, + "d": { + "da": null, + "db": null, + }, }), ) .await; @@ -761,13 +737,14 @@ mod tests { open_paths( &[PathBuf::from("/root/a"), PathBuf::from("/root/b")], &app_state, + None, cx, ) }) .await; assert_eq!(cx.window_ids().len(), 1); - cx.update(|cx| open_paths(&[PathBuf::from("/root/a")], &app_state, cx)) + cx.update(|cx| open_paths(&[PathBuf::from("/root/a")], &app_state, None, cx)) .await; assert_eq!(cx.window_ids().len(), 1); let workspace_1 = cx.root_view::(cx.window_ids()[0]).unwrap(); @@ -781,11 +758,37 @@ mod tests { open_paths( &[PathBuf::from("/root/b"), PathBuf::from("/root/c")], &app_state, + None, + cx, + ) + }) + .await; + assert_eq!(cx.window_ids().len(), 2); + + // Replace existing windows + let window_id = cx.window_ids()[0]; + cx.update(|cx| { + open_paths( + &[PathBuf::from("/root/c"), PathBuf::from("/root/d")], + &app_state, + Some(window_id), cx, ) }) .await; assert_eq!(cx.window_ids().len(), 2); + let workspace_1 = cx.root_view::(window_id).unwrap(); + workspace_1.read_with(cx, |workspace, cx| { + assert_eq!( + workspace + .worktrees(cx) + .map(|w| w.read(cx).abs_path()) + .collect::>(), + &[Path::new("/root/c").into(), Path::new("/root/d").into()] + ); + assert!(workspace.left_sidebar().read(cx).is_open()); + assert!(workspace.active_pane().is_focused(cx)); + }); } #[gpui::test] @@ -797,7 +800,7 @@ mod tests { .insert_tree("/root", json!({"a": "hey"})) .await; - cx.update(|cx| open_paths(&[PathBuf::from("/root/a")], &app_state, cx)) + cx.update(|cx| open_paths(&[PathBuf::from("/root/a")], &app_state, None, cx)) .await; assert_eq!(cx.window_ids().len(), 1); @@ -835,7 +838,7 @@ mod tests { assert!(!cx.is_window_edited(workspace.window_id())); // Opening the buffer again doesn't impact the window's edited state. - cx.update(|cx| open_paths(&[PathBuf::from("/root/a")], &app_state, cx)) + cx.update(|cx| open_paths(&[PathBuf::from("/root/a")], &app_state, None, cx)) .await; let editor = workspace.read_with(cx, |workspace, cx| { workspace @@ -865,7 +868,8 @@ mod tests { #[gpui::test] async fn test_new_empty_workspace(cx: &mut TestAppContext) { let app_state = init(cx); - cx.update(|cx| open_new(&app_state, cx)).await; + cx.update(|cx| open_new(&app_state, cx, |_, cx| cx.dispatch_action(NewFile))) + .await; let window_id = *cx.window_ids().first().unwrap(); let workspace = cx.root_view::(window_id).unwrap(); @@ -910,9 +914,7 @@ mod tests { .await; let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await; - let (_, workspace) = cx.add_window(|cx| { - Workspace::new(Default::default(), 0, project, |_, _| unimplemented!(), cx) - }); + let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx)); let entries = cx.read(|cx| workspace.file_project_paths(cx)); let file1 = entries[0].clone(); @@ -1031,9 +1033,7 @@ mod tests { .await; let project = Project::test(app_state.fs.clone(), ["/dir1".as_ref()], cx).await; - let (_, workspace) = cx.add_window(|cx| { - Workspace::new(Default::default(), 0, project, |_, _| unimplemented!(), cx) - }); + let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx)); // Open a file within an existing worktree. cx.update(|cx| { @@ -1192,9 +1192,7 @@ mod tests { .await; let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await; - let (window_id, workspace) = cx.add_window(|cx| { - Workspace::new(Default::default(), 0, project, |_, _| unimplemented!(), cx) - }); + let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx)); // Open a file within an existing worktree. cx.update(|cx| { @@ -1236,9 +1234,7 @@ mod tests { let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await; project.update(cx, |project, _| project.languages().add(rust_lang())); - let (window_id, workspace) = cx.add_window(|cx| { - Workspace::new(Default::default(), 0, project, |_, _| unimplemented!(), cx) - }); + let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx)); let worktree = cx.read(|cx| workspace.read(cx).worktrees(cx).next().unwrap()); // Create a new untitled buffer @@ -1327,9 +1323,7 @@ mod tests { let project = Project::test(app_state.fs.clone(), [], cx).await; project.update(cx, |project, _| project.languages().add(rust_lang())); - let (window_id, workspace) = cx.add_window(|cx| { - Workspace::new(Default::default(), 0, project, |_, _| unimplemented!(), cx) - }); + let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx)); // Create a new untitled buffer cx.dispatch_action(window_id, NewFile); @@ -1382,9 +1376,7 @@ mod tests { .await; let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await; - let (window_id, workspace) = cx.add_window(|cx| { - Workspace::new(Default::default(), 0, project, |_, _| unimplemented!(), cx) - }); + let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx)); let entries = cx.read(|cx| workspace.file_project_paths(cx)); let file1 = entries[0].clone(); @@ -1458,15 +1450,7 @@ mod tests { .await; let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await; - let (_, workspace) = cx.add_window(|cx| { - Workspace::new( - Default::default(), - 0, - project.clone(), - |_, _| unimplemented!(), - cx, - ) - }); + let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx)); let entries = cx.read(|cx| workspace.file_project_paths(cx)); let file1 = entries[0].clone(); @@ -1730,15 +1714,7 @@ mod tests { .await; let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await; - let (_, workspace) = cx.add_window(|cx| { - Workspace::new( - Default::default(), - 0, - project.clone(), - |_, _| unimplemented!(), - cx, - ) - }); + let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx)); let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone()); let entries = cx.read(|cx| workspace.file_project_paths(cx)); diff --git a/script/generate-licenses b/script/generate-licenses index 8a41f55c025bf4909b4298180588f043a6153894..14c9d4c79f1646d4889da82d9ee24c7975003532 100755 --- a/script/generate-licenses +++ b/script/generate-licenses @@ -10,7 +10,7 @@ echo -e "# ###### THEME LICENSES ######\n" >> $OUTPUT_FILE echo "Generating theme licenses" cd styles -npm ci +npm --silent ci npm run --silent build-licenses >> $OUTPUT_FILE cd .. diff --git a/styles/src/buildLicenses.ts b/styles/src/buildLicenses.ts index 793a14a3b21d68542d7a17d2643e5c286574ce02..3367e50ff027601db2b68ead242dc3a266b21e1e 100644 --- a/styles/src/buildLicenses.ts +++ b/styles/src/buildLicenses.ts @@ -39,7 +39,7 @@ function getLicenseText( if (typeof meta.license.license_text == "string") { callback(meta, meta.license.license_text) } else { - let license_text_obj: Verification = meta.license.license_text; + let license_text_obj: Verification = meta.license.license_text // The following copied from the example code on nodejs.org: // https://nodejs.org/api/http.html#httpgetoptions-callback https diff --git a/styles/src/styleTree/app.ts b/styles/src/styleTree/app.ts index dc57468df65f079af5eb5af7596823024b5d4df2..423ce37d481e8f47bf9118ad6134b5c123a4ed31 100644 --- a/styles/src/styleTree/app.ts +++ b/styles/src/styleTree/app.ts @@ -20,6 +20,7 @@ import contactList from "./contactList" import incomingCallNotification from "./incomingCallNotification" import { ColorScheme } from "../themes/common/colorScheme" import feedback from "./feedback" +import welcome from "./welcome" export default function app(colorScheme: ColorScheme): Object { return { @@ -33,6 +34,7 @@ export default function app(colorScheme: ColorScheme): Object { incomingCallNotification: incomingCallNotification(colorScheme), picker: picker(colorScheme), workspace: workspace(colorScheme), + welcome: welcome(colorScheme), contextMenu: contextMenu(colorScheme), editor: editor(colorScheme), projectDiagnostics: projectDiagnostics(colorScheme), diff --git a/styles/src/styleTree/components.ts b/styles/src/styleTree/components.ts index edbced8323c390499388071f498ddfa912fc0aed..3f69df981e8124a343c3b534aced6261b72bf309 100644 --- a/styles/src/styleTree/components.ts +++ b/styles/src/styleTree/components.ts @@ -93,7 +93,7 @@ interface Text { underline?: boolean } -interface TextProperties { +export interface TextProperties { size?: keyof typeof fontSizes weight?: FontWeight underline?: boolean diff --git a/styles/src/styleTree/contextMenu.ts b/styles/src/styleTree/contextMenu.ts index 30f44a63141a70edd01998057385dfcd7f15adfc..b4a21deba4a7d21c0372dc34ff204922cdcf5b1b 100644 --- a/styles/src/styleTree/contextMenu.ts +++ b/styles/src/styleTree/contextMenu.ts @@ -26,14 +26,19 @@ export default function contextMenu(colorScheme: ColorScheme) { hover: { 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 }, + }, }, active: { background: background(layer, "active"), - label: text(layer, "sans", "active", { size: "sm" }), }, activeHover: { background: background(layer, "active"), - label: text(layer, "sans", "active", { size: "sm" }), }, }, separator: { diff --git a/styles/src/styleTree/editor.ts b/styles/src/styleTree/editor.ts index 799363a349191ffe9bd53748d818c7b0cb33177c..64a19d714fa6f5b7edeaa1bac255006f242e8b3f 100644 --- a/styles/src/styleTree/editor.ts +++ b/styles/src/styleTree/editor.ts @@ -90,7 +90,7 @@ export default function editor(colorScheme: ColorScheme) { clicked: { color: colorScheme.ramps.neutral(0.5).alpha(0.7).hex(), }, - } + }, }, foldBackground: foreground(layer, "variant"), }, diff --git a/styles/src/styleTree/projectPanel.ts b/styles/src/styleTree/projectPanel.ts index 90e0c82f5bf1d16e19acfa0c3df0a0454cabae86..80cb884c4896de11d5c852468849b35787858508 100644 --- a/styles/src/styleTree/projectPanel.ts +++ b/styles/src/styleTree/projectPanel.ts @@ -29,6 +29,28 @@ export default function projectPanel(colorScheme: ColorScheme) { } return { + openProjectButton: { + 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" }), + hover: { + ...text(layer, "sans", "default", { size: "sm" }), + background: background(layer, "hovered"), + border: border(layer, "active"), + }, + }, background: background(layer), padding: { left: 12, right: 12, top: 6, bottom: 6 }, indentWidth: 8, diff --git a/styles/src/styleTree/statusBar.ts b/styles/src/styleTree/statusBar.ts index a60a55df1ecaae3ce52edfc82c90222db30718cb..9fa427d302f19008f169c77e847cf67031e63562 100644 --- a/styles/src/styleTree/statusBar.ts +++ b/styles/src/styleTree/statusBar.ts @@ -25,6 +25,13 @@ export default function statusBar(colorScheme: ColorScheme) { }, border: border(layer, { top: true, overlay: true }), cursorPosition: text(layer, "sans", "variant"), + activeLanguage: { + padding: { left: 6, right: 6 }, + ...text(layer, "sans", "variant"), + hover: { + ...text(layer, "sans", "on") + } + }, autoUpdateProgressMessage: text(layer, "sans", "variant"), autoUpdateDoneMessage: text(layer, "sans", "variant"), lspStatus: { @@ -44,10 +51,6 @@ export default function statusBar(colorScheme: ColorScheme) { ...text(layer, "sans"), hover: text(layer, "sans", "hovered"), }, - feedback: { - ...text(layer, "sans", "variant"), - hover: text(layer, "sans", "hovered"), - }, diagnosticSummary: { height: 20, iconWidth: 16, diff --git a/styles/src/styleTree/welcome.ts b/styles/src/styleTree/welcome.ts new file mode 100644 index 0000000000000000000000000000000000000000..e1bd5c82bbac65595ed3e0cd60b1f5957158889c --- /dev/null +++ b/styles/src/styleTree/welcome.ts @@ -0,0 +1,139 @@ + +import { ColorScheme } from "../themes/common/colorScheme"; +import { withOpacity } from "../utils/color"; +import { border, background, foreground, text, TextProperties } from "./components"; + + +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: { + color: foreground(layer, "default"), + icon: "icons/logo_96.svg", + dimensions: { + width: 64, + height: 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: { + 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), + hover: { + ...text(layer, "sans", "default", interactive_text_size), + background: background(layer, "hovered"), + border: border(layer, "active"), + }, + }, + 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: { + color: foreground(layer, "on"), + icon: "icons/check_12.svg", + dimensions: { + width: 12, + height: 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") + } + } + } +} diff --git a/styles/src/styleTree/workspace.ts b/styles/src/styleTree/workspace.ts index f9f49e3c7df14b5ccf14a9322a1a461c1c153917..bebe87ce55c9ee15b7a13a49e6cffe9dba54cff6 100644 --- a/styles/src/styleTree/workspace.ts +++ b/styles/src/styleTree/workspace.ts @@ -40,7 +40,49 @@ export default function workspace(colorScheme: ColorScheme) { const followerAvatarOuterWidth = followerAvatarWidth + 4 return { - background: background(layer), + background: background(colorScheme.lowest), + blankPane: { + logoContainer: { + width: 256, + height: 256, + }, + logo: { + color: withOpacity("#000000", colorScheme.isLight ? 0.6 : 0.8), + icon: "icons/logo_96.svg", + dimensions: { + width: 256, + height: 256, + }, + }, + logoShadow: { + color: withOpacity(colorScheme.isLight ? "#FFFFFF" : colorScheme.lowest.base.default.background, colorScheme.isLight ? 1 : 0.6), + icon: "icons/logo_96.svg", + dimensions: { + width: 256, + height: 256, + }, + }, + keyboardHints: { + margin: { + top: 96, + }, + cornerRadius: 4, + }, + keyboardHint: { + ...text(layer, "sans", "variant", { size: "sm" }), + padding: { + top: 3, + left: 8, + right: 8, + bottom: 3 + }, + cornerRadius: 8, + hover: { + ...text(layer, "sans", "active", { size: "sm" }), + } + }, + keyboardHintWidth: 320, + }, joiningProjectAvatar: { cornerRadius: 40, width: 80, @@ -248,7 +290,7 @@ export default function workspace(colorScheme: ColorScheme) { }, dock: { initialSizeRight: 640, - initialSizeBottom: 480, + initialSizeBottom: 304, wash_color: withOpacity(background(colorScheme.highest), 0.5), panel: { border: border(colorScheme.middle), diff --git a/styles/src/themes/andromeda.ts b/styles/src/themes/andromeda.ts index 369f1d7f2b60ab7b957feaa36fcb70047d606f06..7eba7b1481aeba8b1429b323bb51c0f78c0a6069 100644 --- a/styles/src/themes/andromeda.ts +++ b/styles/src/themes/andromeda.ts @@ -39,7 +39,7 @@ export const meta: Meta = { "https://raw.githubusercontent.com/EliverLara/Andromeda/master/LICENSE.md", license_checksum: "2f7886f1a05cefc2c26f5e49de1a39fa4466413c1ccb06fc80960e73f5ed4b89", - } + }, }, url: "https://github.com/EliverLara/Andromeda", } diff --git a/styles/src/themes/atelier-cave-dark.ts b/styles/src/themes/atelier-cave-dark.ts new file mode 100644 index 0000000000000000000000000000000000000000..a56e22cd92373997cc40d19eddf5b05e9cad5a02 --- /dev/null +++ b/styles/src/themes/atelier-cave-dark.ts @@ -0,0 +1,66 @@ +import chroma from "chroma-js" +import { Meta } from "./common/colorScheme" +import { colorRamp, createColorScheme } from "./common/ramps" +import { metaCommon, name, buildSyntax, Variant } from "./common/atelier-common" + +const variant: Variant = { + meta: { + name: `${name} Cave Dark`, + ...metaCommon, + url: "https://atelierbram.github.io/syntax-highlighting/atelier-schemes/cave/", + }, + colors: { + base00: "#19171c", + base01: "#26232a", + base02: "#585260", + base03: "#655f6d", + base04: "#7e7887", + base05: "#8b8792", + base06: "#e2dfe7", + base07: "#efecf4", + base08: "#be4678", + base09: "#aa573c", + base0A: "#a06e3b", + base0B: "#2a9292", + base0C: "#398bc6", + base0D: "#576ddb", + base0E: "#955ae7", + base0F: "#bf40bf", + }, +} + +const syntax = buildSyntax(variant) + +const theme = (variant: Variant) => { + const { meta, colors } = variant + + return createColorScheme( + meta.name, + false, + { + neutral: chroma.scale([ + colors.base00, + colors.base01, + colors.base02, + colors.base03, + colors.base04, + colors.base05, + 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)), + }, + syntax + ) +} + +export const dark = theme(variant) + +export const meta: Meta = variant.meta diff --git a/styles/src/themes/atelier-cave-light.ts b/styles/src/themes/atelier-cave-light.ts new file mode 100644 index 0000000000000000000000000000000000000000..3b180752cf97ea1bcc515c904deb20eafc03aea6 --- /dev/null +++ b/styles/src/themes/atelier-cave-light.ts @@ -0,0 +1,68 @@ +import chroma from "chroma-js" +import { Meta } from "./common/colorScheme" +import { colorRamp, createColorScheme } from "./common/ramps" +import { metaCommon, name, buildSyntax, Variant } from "./common/atelier-common" + +const variant: Variant = { + meta: { + name: `${name} Cave Light`, + ...metaCommon, + url: "https://atelierbram.github.io/syntax-highlighting/atelier-schemes/cave/", + }, + colors: { + base00: "#efecf4", + base01: "#e2dfe7", + base02: "#8b8792", + base03: "#7e7887", + base04: "#655f6d", + base05: "#585260", + base06: "#26232a", + base07: "#19171c", + base08: "#be4678", + base09: "#aa573c", + base0A: "#a06e3b", + base0B: "#2a9292", + base0C: "#398bc6", + base0D: "#576ddb", + base0E: "#955ae7", + base0F: "#bf40bf", + }, +} + +const syntax = buildSyntax(variant) + +const theme = (variant: Variant) => { + const { meta, colors } = variant + + return createColorScheme( + meta.name, + true, + { + neutral: chroma.scale( + [ + colors.base00, + colors.base01, + colors.base02, + colors.base03, + colors.base04, + colors.base05, + colors.base06, + 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)), + }, + syntax + ) +} + +export const dark = theme(variant) + +export const meta: Meta = variant.meta diff --git a/styles/src/themes/atelier-cave.ts b/styles/src/themes/atelier-cave.ts deleted file mode 100644 index 21b846d708c349a8fa7b2779dd991ff5bcfa1605..0000000000000000000000000000000000000000 --- a/styles/src/themes/atelier-cave.ts +++ /dev/null @@ -1,65 +0,0 @@ -import chroma from "chroma-js" -import { Meta } from "./common/colorScheme" -import { colorRamp, createColorScheme } from "./common/ramps" - -const name = "Atelier Cave" - -export const dark = createColorScheme(`${name} Dark`, false, { - neutral: chroma - .scale([ - "#19171c", - "#26232a", - "#585260", - "#655f6d", - "#7e7887", - "#8b8792", - "#e2dfe7", - "#efecf4", - ]) - .domain([0, 0.15, 0.45, 0.6, 0.65, 0.7, 0.85, 1]), - red: colorRamp(chroma("#be4678")), - orange: colorRamp(chroma("#aa573c")), - yellow: colorRamp(chroma("#a06e3b")), - green: colorRamp(chroma("#2a9292")), - cyan: colorRamp(chroma("#398bc6")), - blue: colorRamp(chroma("#576ddb")), - violet: colorRamp(chroma("#955ae7")), - magenta: colorRamp(chroma("#bf40bf")), -}) - -export const light = createColorScheme(`${name} Light`, true, { - neutral: chroma - .scale([ - "#19171c", - "#26232a", - "#585260", - "#655f6d", - "#7e7887", - "#8b8792", - "#e2dfe7", - "#efecf4", - ]) - .correctLightness(), - red: colorRamp(chroma("#be4678")), - orange: colorRamp(chroma("#aa573c")), - yellow: colorRamp(chroma("#a06e3b")), - green: colorRamp(chroma("#2a9292")), - cyan: colorRamp(chroma("#398bc6")), - blue: colorRamp(chroma("#576ddb")), - violet: colorRamp(chroma("#955ae7")), - magenta: colorRamp(chroma("#bf40bf")), -}) - -export const meta: Meta = { - name, - author: "atelierbram", - license: { - SPDX: "MIT", - license_text: { - https_url: "https://atelierbram.mit-license.org/license.txt", - license_checksum: - "f95ce526ef4e7eecf7a832bba0e3451cc1000f9ce63eb01ed6f64f8109f5d0a5", - } - }, - url: "https://atelierbram.github.io/syntax-highlighting/atelier-schemes/cave/", -} diff --git a/styles/src/themes/atelier-dune-dark.ts b/styles/src/themes/atelier-dune-dark.ts new file mode 100644 index 0000000000000000000000000000000000000000..0ab402a99d4adf8f1e0ed41979d5cc9973696404 --- /dev/null +++ b/styles/src/themes/atelier-dune-dark.ts @@ -0,0 +1,66 @@ +import chroma from "chroma-js" +import { Meta } from "./common/colorScheme" +import { colorRamp, createColorScheme } from "./common/ramps" +import { metaCommon, name, buildSyntax, Variant } from "./common/atelier-common" + +const variant: Variant = { + meta: { + name: `${name} Dune Dark`, + ...metaCommon, + url: "https://atelierbram.github.io/syntax-highlighting/atelier-schemes/dune/", + }, + colors: { + base00: "#20201d", + base01: "#292824", + base02: "#6e6b5e", + base03: "#7d7a68", + base04: "#999580", + base05: "#a6a28c", + base06: "#e8e4cf", + base07: "#fefbec", + base08: "#d73737", + base09: "#b65611", + base0A: "#ae9513", + base0B: "#60ac39", + base0C: "#1fad83", + base0D: "#6684e1", + base0E: "#b854d4", + base0F: "#d43552", + }, +} + +const syntax = buildSyntax(variant) + +const theme = (variant: Variant) => { + const { meta, colors } = variant + + return createColorScheme( + meta.name, + false, + { + neutral: chroma.scale([ + colors.base00, + colors.base01, + colors.base02, + colors.base03, + colors.base04, + colors.base05, + 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)), + }, + syntax + ) +} + +export const dark = theme(variant) + +export const meta: Meta = variant.meta diff --git a/styles/src/themes/atelier-dune-light.ts b/styles/src/themes/atelier-dune-light.ts new file mode 100644 index 0000000000000000000000000000000000000000..e6a09985d5e09f8612d0a41f7b3a898991b9b720 --- /dev/null +++ b/styles/src/themes/atelier-dune-light.ts @@ -0,0 +1,68 @@ +import chroma from "chroma-js" +import { Meta } from "./common/colorScheme" +import { colorRamp, createColorScheme } from "./common/ramps" +import { metaCommon, name, buildSyntax, Variant } from "./common/atelier-common" + +const variant: Variant = { + meta: { + name: `${name} Dune Light`, + ...metaCommon, + url: "https://atelierbram.github.io/syntax-highlighting/atelier-schemes/dune/", + }, + colors: { + base00: "#fefbec", + base01: "#e8e4cf", + base02: "#a6a28c", + base03: "#999580", + base04: "#7d7a68", + base05: "#6e6b5e", + base06: "#292824", + base07: "#20201d", + base08: "#d73737", + base09: "#b65611", + base0A: "#ae9513", + base0B: "#60ac39", + base0C: "#1fad83", + base0D: "#6684e1", + base0E: "#b854d4", + base0F: "#d43552", + }, +} + +const syntax = buildSyntax(variant) + +const theme = (variant: Variant) => { + const { meta, colors } = variant + + return createColorScheme( + meta.name, + true, + { + neutral: chroma.scale( + [ + colors.base00, + colors.base01, + colors.base02, + colors.base03, + colors.base04, + colors.base05, + colors.base06, + 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)), + }, + syntax + ) +} + +export const dark = theme(variant) + +export const meta: Meta = variant.meta diff --git a/styles/src/themes/atelier-estuary-dark.ts b/styles/src/themes/atelier-estuary-dark.ts new file mode 100644 index 0000000000000000000000000000000000000000..c4ec50c1e00ed7731d069ed3f559204b9030b10c --- /dev/null +++ b/styles/src/themes/atelier-estuary-dark.ts @@ -0,0 +1,66 @@ +import chroma from "chroma-js" +import { Meta } from "./common/colorScheme" +import { colorRamp, createColorScheme } from "./common/ramps" +import { metaCommon, name, buildSyntax, Variant } from "./common/atelier-common" + +const variant: Variant = { + meta: { + name: `${name} Estuary Dark`, + ...metaCommon, + url: "https://atelierbram.github.io/syntax-highlighting/atelier-schemes/estuary/", + }, + colors: { + base00: "#22221b", + base01: "#302f27", + base02: "#5f5e4e", + base03: "#6c6b5a", + base04: "#878573", + base05: "#929181", + base06: "#e7e6df", + base07: "#f4f3ec", + base08: "#ba6236", + base09: "#ae7313", + base0A: "#a5980d", + base0B: "#7d9726", + base0C: "#5b9d48", + base0D: "#36a166", + base0E: "#5f9182", + base0F: "#9d6c7c", + }, +} + +const syntax = buildSyntax(variant) + +const theme = (variant: Variant) => { + const { meta, colors } = variant + + return createColorScheme( + meta.name, + false, + { + neutral: chroma.scale([ + colors.base00, + colors.base01, + colors.base02, + colors.base03, + colors.base04, + colors.base05, + 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)), + }, + syntax + ) +} + +export const dark = theme(variant) + +export const meta: Meta = variant.meta diff --git a/styles/src/themes/atelier-estuary-light.ts b/styles/src/themes/atelier-estuary-light.ts new file mode 100644 index 0000000000000000000000000000000000000000..6fce0e44833d2d0bf8657a3465bcfb601b262644 --- /dev/null +++ b/styles/src/themes/atelier-estuary-light.ts @@ -0,0 +1,68 @@ +import chroma from "chroma-js" +import { Meta } from "./common/colorScheme" +import { colorRamp, createColorScheme } from "./common/ramps" +import { metaCommon, name, buildSyntax, Variant } from "./common/atelier-common" + +const variant: Variant = { + meta: { + name: `${name} Estuary Light`, + ...metaCommon, + url: "https://atelierbram.github.io/syntax-highlighting/atelier-schemes/estuary/", + }, + colors: { + base00: "#f4f3ec", + base01: "#e7e6df", + base02: "#929181", + base03: "#878573", + base04: "#6c6b5a", + base05: "#5f5e4e", + base06: "#302f27", + base07: "#22221b", + base08: "#ba6236", + base09: "#ae7313", + base0A: "#a5980d", + base0B: "#7d9726", + base0C: "#5b9d48", + base0D: "#36a166", + base0E: "#5f9182", + base0F: "#9d6c7c", + }, +} + +const syntax = buildSyntax(variant) + +const theme = (variant: Variant) => { + const { meta, colors } = variant + + return createColorScheme( + meta.name, + true, + { + neutral: chroma.scale( + [ + colors.base00, + colors.base01, + colors.base02, + colors.base03, + colors.base04, + colors.base05, + colors.base06, + 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)), + }, + syntax + ) +} + +export const dark = theme(variant) + +export const meta: Meta = variant.meta diff --git a/styles/src/themes/atelier-forest-dark.ts b/styles/src/themes/atelier-forest-dark.ts new file mode 100644 index 0000000000000000000000000000000000000000..7c47c55a830ed780f662d83d9f768d26207dc31e --- /dev/null +++ b/styles/src/themes/atelier-forest-dark.ts @@ -0,0 +1,66 @@ +import chroma from "chroma-js" +import { Meta } from "./common/colorScheme" +import { colorRamp, createColorScheme } from "./common/ramps" +import { metaCommon, name, buildSyntax, Variant } from "./common/atelier-common" + +const variant: Variant = { + meta: { + name: `${name} Forest Dark`, + ...metaCommon, + url: "https://atelierbram.github.io/syntax-highlighting/atelier-schemes/forest/", + }, + colors: { + base00: "#1b1918", + base01: "#2c2421", + base02: "#68615e", + base03: "#766e6b", + base04: "#9c9491", + base05: "#a8a19f", + base06: "#e6e2e0", + base07: "#f1efee", + base08: "#f22c40", + base09: "#df5320", + base0A: "#c38418", + base0B: "#7b9726", + base0C: "#3d97b8", + base0D: "#407ee7", + base0E: "#6666ea", + base0F: "#c33ff3", + }, +} + +const syntax = buildSyntax(variant) + +const theme = (variant: Variant) => { + const { meta, colors } = variant + + return createColorScheme( + meta.name, + false, + { + neutral: chroma.scale([ + colors.base00, + colors.base01, + colors.base02, + colors.base03, + colors.base04, + colors.base05, + 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)), + }, + syntax + ) +} + +export const dark = theme(variant) + +export const meta: Meta = variant.meta diff --git a/styles/src/themes/atelier-forest-light.ts b/styles/src/themes/atelier-forest-light.ts new file mode 100644 index 0000000000000000000000000000000000000000..8ce06164769a7a8dfa15fc74a77b6aa0f3c9e3af --- /dev/null +++ b/styles/src/themes/atelier-forest-light.ts @@ -0,0 +1,68 @@ +import chroma from "chroma-js" +import { Meta } from "./common/colorScheme" +import { colorRamp, createColorScheme } from "./common/ramps" +import { metaCommon, name, buildSyntax, Variant } from "./common/atelier-common" + +const variant: Variant = { + meta: { + name: `${name} Forest Light`, + ...metaCommon, + url: "https://atelierbram.github.io/syntax-highlighting/atelier-schemes/forest/", + }, + colors: { + base00: "#f1efee", + base01: "#e6e2e0", + base02: "#a8a19f", + base03: "#9c9491", + base04: "#766e6b", + base05: "#68615e", + base06: "#2c2421", + base07: "#1b1918", + base08: "#f22c40", + base09: "#df5320", + base0A: "#c38418", + base0B: "#7b9726", + base0C: "#3d97b8", + base0D: "#407ee7", + base0E: "#6666ea", + base0F: "#c33ff3", + }, +} + +const syntax = buildSyntax(variant) + +const theme = (variant: Variant) => { + const { meta, colors } = variant + + return createColorScheme( + meta.name, + true, + { + neutral: chroma.scale( + [ + colors.base00, + colors.base01, + colors.base02, + colors.base03, + colors.base04, + colors.base05, + colors.base06, + 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)), + }, + syntax + ) +} + +export const dark = theme(variant) + +export const meta: Meta = variant.meta diff --git a/styles/src/themes/atelier-heath-dark.ts b/styles/src/themes/atelier-heath-dark.ts new file mode 100644 index 0000000000000000000000000000000000000000..87458ab8f5ab68a30e602de9b62edbe1e4cf5c18 --- /dev/null +++ b/styles/src/themes/atelier-heath-dark.ts @@ -0,0 +1,66 @@ +import chroma from "chroma-js" +import { Meta } from "./common/colorScheme" +import { colorRamp, createColorScheme } from "./common/ramps" +import { metaCommon, name, buildSyntax, Variant } from "./common/atelier-common" + +const variant: Variant = { + meta: { + name: `${name} Heath Dark`, + ...metaCommon, + url: "https://atelierbram.github.io/syntax-highlighting/atelier-schemes/heath/", + }, + colors: { + base00: "#1b181b", + base01: "#292329", + base02: "#695d69", + base03: "#776977", + base04: "#9e8f9e", + base05: "#ab9bab", + base06: "#d8cad8", + base07: "#f7f3f7", + base08: "#ca402b", + base09: "#a65926", + base0A: "#bb8a35", + base0B: "#918b3b", + base0C: "#159393", + base0D: "#516aec", + base0E: "#7b59c0", + base0F: "#cc33cc", + }, +} + +const syntax = buildSyntax(variant) + +const theme = (variant: Variant) => { + const { meta, colors } = variant + + return createColorScheme( + meta.name, + false, + { + neutral: chroma.scale([ + colors.base00, + colors.base01, + colors.base02, + colors.base03, + colors.base04, + colors.base05, + 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)), + }, + syntax + ) +} + +export const dark = theme(variant) + +export const meta: Meta = variant.meta diff --git a/styles/src/themes/atelier-heath-light.ts b/styles/src/themes/atelier-heath-light.ts new file mode 100644 index 0000000000000000000000000000000000000000..3db34370418f1cd27e305a3175e045e591e36ae9 --- /dev/null +++ b/styles/src/themes/atelier-heath-light.ts @@ -0,0 +1,68 @@ +import chroma from "chroma-js" +import { Meta } from "./common/colorScheme" +import { colorRamp, createColorScheme } from "./common/ramps" +import { metaCommon, name, buildSyntax, Variant } from "./common/atelier-common" + +const variant: Variant = { + meta: { + name: `${name} Heath Light`, + ...metaCommon, + url: "https://atelierbram.github.io/syntax-highlighting/atelier-schemes/heath/", + }, + colors: { + base00: "#f7f3f7", + base01: "#d8cad8", + base02: "#ab9bab", + base03: "#9e8f9e", + base04: "#776977", + base05: "#695d69", + base06: "#292329", + base07: "#1b181b", + base08: "#ca402b", + base09: "#a65926", + base0A: "#bb8a35", + base0B: "#918b3b", + base0C: "#159393", + base0D: "#516aec", + base0E: "#7b59c0", + base0F: "#cc33cc", + }, +} + +const syntax = buildSyntax(variant) + +const theme = (variant: Variant) => { + const { meta, colors } = variant + + return createColorScheme( + meta.name, + true, + { + neutral: chroma.scale( + [ + colors.base00, + colors.base01, + colors.base02, + colors.base03, + colors.base04, + colors.base05, + colors.base06, + 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)), + }, + syntax + ) +} + +export const dark = theme(variant) + +export const meta: Meta = variant.meta diff --git a/styles/src/themes/atelier-lakeside-dark.ts b/styles/src/themes/atelier-lakeside-dark.ts new file mode 100644 index 0000000000000000000000000000000000000000..a8297ef9fd307243f1c9c84f3caa1e627120a19e --- /dev/null +++ b/styles/src/themes/atelier-lakeside-dark.ts @@ -0,0 +1,66 @@ +import chroma from "chroma-js" +import { Meta } from "./common/colorScheme" +import { colorRamp, createColorScheme } from "./common/ramps" +import { metaCommon, name, buildSyntax, Variant } from "./common/atelier-common" + +const variant: Variant = { + meta: { + name: `${name} Lakeside Dark`, + ...metaCommon, + url: "https://atelierbram.github.io/syntax-highlighting/atelier-schemes/lakeside/", + }, + colors: { + base00: "#161b1d", + base01: "#1f292e", + base02: "#516d7b", + base03: "#5a7b8c", + base04: "#7195a8", + base05: "#7ea2b4", + base06: "#c1e4f6", + base07: "#ebf8ff", + base08: "#d22d72", + base09: "#935c25", + base0A: "#8a8a0f", + base0B: "#568c3b", + base0C: "#2d8f6f", + base0D: "#257fad", + base0E: "#6b6bb8", + base0F: "#b72dd2", + }, +} + +const syntax = buildSyntax(variant) + +const theme = (variant: Variant) => { + const { meta, colors } = variant + + return createColorScheme( + meta.name, + false, + { + neutral: chroma.scale([ + colors.base00, + colors.base01, + colors.base02, + colors.base03, + colors.base04, + colors.base05, + 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)), + }, + syntax + ) +} + +export const dark = theme(variant) + +export const meta: Meta = variant.meta diff --git a/styles/src/themes/atelier-lakeside-light.ts b/styles/src/themes/atelier-lakeside-light.ts new file mode 100644 index 0000000000000000000000000000000000000000..408fabcaf36a50df5229ea0034d12c67bd3b3562 --- /dev/null +++ b/styles/src/themes/atelier-lakeside-light.ts @@ -0,0 +1,68 @@ +import chroma from "chroma-js" +import { Meta } from "./common/colorScheme" +import { colorRamp, createColorScheme } from "./common/ramps" +import { metaCommon, name, buildSyntax, Variant } from "./common/atelier-common" + +const variant: Variant = { + meta: { + name: `${name} Lakeside Light`, + ...metaCommon, + url: "https://atelierbram.github.io/syntax-highlighting/atelier-schemes/lakeside/", + }, + colors: { + base00: "#ebf8ff", + base01: "#c1e4f6", + base02: "#7ea2b4", + base03: "#7195a8", + base04: "#5a7b8c", + base05: "#516d7b", + base06: "#1f292e", + base07: "#161b1d", + base08: "#d22d72", + base09: "#935c25", + base0A: "#8a8a0f", + base0B: "#568c3b", + base0C: "#2d8f6f", + base0D: "#257fad", + base0E: "#6b6bb8", + base0F: "#b72dd2", + }, +} + +const syntax = buildSyntax(variant) + +const theme = (variant: Variant) => { + const { meta, colors } = variant + + return createColorScheme( + meta.name, + true, + { + neutral: chroma.scale( + [ + colors.base00, + colors.base01, + colors.base02, + colors.base03, + colors.base04, + colors.base05, + colors.base06, + 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)), + }, + syntax + ) +} + +export const dark = theme(variant) + +export const meta: Meta = variant.meta diff --git a/styles/src/themes/atelier-plateau-dark.ts b/styles/src/themes/atelier-plateau-dark.ts new file mode 100644 index 0000000000000000000000000000000000000000..731cb824e44eba0590c5f1dc4804121aa7f97cd9 --- /dev/null +++ b/styles/src/themes/atelier-plateau-dark.ts @@ -0,0 +1,66 @@ +import chroma from "chroma-js" +import { Meta } from "./common/colorScheme" +import { colorRamp, createColorScheme } from "./common/ramps" +import { metaCommon, name, buildSyntax, Variant } from "./common/atelier-common" + +const variant: Variant = { + meta: { + name: `${name} Plateau Dark`, + ...metaCommon, + url: "https://atelierbram.github.io/syntax-highlighting/atelier-schemes/plateau/", + }, + colors: { + base00: "#1b1818", + base01: "#292424", + base02: "#585050", + base03: "#655d5d", + base04: "#7e7777", + base05: "#8a8585", + base06: "#e7dfdf", + base07: "#f4ecec", + base08: "#ca4949", + base09: "#b45a3c", + base0A: "#a06e3b", + base0B: "#4b8b8b", + base0C: "#5485b6", + base0D: "#7272ca", + base0E: "#8464c4", + base0F: "#bd5187", + }, +} + +const syntax = buildSyntax(variant) + +const theme = (variant: Variant) => { + const { meta, colors } = variant + + return createColorScheme( + meta.name, + false, + { + neutral: chroma.scale([ + colors.base00, + colors.base01, + colors.base02, + colors.base03, + colors.base04, + colors.base05, + 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)), + }, + syntax + ) +} + +export const dark = theme(variant) + +export const meta: Meta = variant.meta diff --git a/styles/src/themes/atelier-plateau-light.ts b/styles/src/themes/atelier-plateau-light.ts new file mode 100644 index 0000000000000000000000000000000000000000..96f295a69518b1216482776e44d5b36912f526da --- /dev/null +++ b/styles/src/themes/atelier-plateau-light.ts @@ -0,0 +1,68 @@ +import chroma from "chroma-js" +import { Meta } from "./common/colorScheme" +import { colorRamp, createColorScheme } from "./common/ramps" +import { metaCommon, name, buildSyntax, Variant } from "./common/atelier-common" + +const variant: Variant = { + meta: { + name: `${name} Plateau Light`, + ...metaCommon, + url: "https://atelierbram.github.io/syntax-highlighting/atelier-schemes/plateau/", + }, + colors: { + base00: "#f4ecec", + base01: "#e7dfdf", + base02: "#8a8585", + base03: "#7e7777", + base04: "#655d5d", + base05: "#585050", + base06: "#292424", + base07: "#1b1818", + base08: "#ca4949", + base09: "#b45a3c", + base0A: "#a06e3b", + base0B: "#4b8b8b", + base0C: "#5485b6", + base0D: "#7272ca", + base0E: "#8464c4", + base0F: "#bd5187", + }, +} + +const syntax = buildSyntax(variant) + +const theme = (variant: Variant) => { + const { meta, colors } = variant + + return createColorScheme( + meta.name, + true, + { + neutral: chroma.scale( + [ + colors.base00, + colors.base01, + colors.base02, + colors.base03, + colors.base04, + colors.base05, + colors.base06, + 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)), + }, + syntax + ) +} + +export const dark = theme(variant) + +export const meta: Meta = variant.meta diff --git a/styles/src/themes/atelier-savanna-dark.ts b/styles/src/themes/atelier-savanna-dark.ts new file mode 100644 index 0000000000000000000000000000000000000000..dfcb4f27cbedd41608b941da97172d083e7a8e76 --- /dev/null +++ b/styles/src/themes/atelier-savanna-dark.ts @@ -0,0 +1,66 @@ +import chroma from "chroma-js" +import { Meta } from "./common/colorScheme" +import { colorRamp, createColorScheme } from "./common/ramps" +import { metaCommon, name, buildSyntax, Variant } from "./common/atelier-common" + +const variant: Variant = { + meta: { + name: `${name} Savanna Dark`, + ...metaCommon, + url: "https://atelierbram.github.io/syntax-highlighting/atelier-schemes/savanna/", + }, + colors: { + base00: "#171c19", + base01: "#232a25", + base02: "#526057", + base03: "#5f6d64", + base04: "#78877d", + base05: "#87928a", + base06: "#dfe7e2", + base07: "#ecf4ee", + base08: "#b16139", + base09: "#9f713c", + base0A: "#a07e3b", + base0B: "#489963", + base0C: "#1c9aa0", + base0D: "#478c90", + base0E: "#55859b", + base0F: "#867469", + }, +} + +const syntax = buildSyntax(variant) + +const theme = (variant: Variant) => { + const { meta, colors } = variant + + return createColorScheme( + meta.name, + false, + { + neutral: chroma.scale([ + colors.base00, + colors.base01, + colors.base02, + colors.base03, + colors.base04, + colors.base05, + 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)), + }, + syntax + ) +} + +export const dark = theme(variant) + +export const meta: Meta = variant.meta diff --git a/styles/src/themes/atelier-savanna-light.ts b/styles/src/themes/atelier-savanna-light.ts new file mode 100644 index 0000000000000000000000000000000000000000..4bc1389fc9cf3cb3b4870a58665a510a65d4c9e2 --- /dev/null +++ b/styles/src/themes/atelier-savanna-light.ts @@ -0,0 +1,68 @@ +import chroma from "chroma-js" +import { Meta } from "./common/colorScheme" +import { colorRamp, createColorScheme } from "./common/ramps" +import { metaCommon, name, buildSyntax, Variant } from "./common/atelier-common" + +const variant: Variant = { + meta: { + name: `${name} Savanna Light`, + ...metaCommon, + url: "https://atelierbram.github.io/syntax-highlighting/atelier-schemes/savanna/", + }, + colors: { + base00: "#ecf4ee", + base01: "#dfe7e2", + base02: "#87928a", + base03: "#78877d", + base04: "#5f6d64", + base05: "#526057", + base06: "#232a25", + base07: "#171c19", + base08: "#b16139", + base09: "#9f713c", + base0A: "#a07e3b", + base0B: "#489963", + base0C: "#1c9aa0", + base0D: "#478c90", + base0E: "#55859b", + base0F: "#867469", + }, +} + +const syntax = buildSyntax(variant) + +const theme = (variant: Variant) => { + const { meta, colors } = variant + + return createColorScheme( + meta.name, + true, + { + neutral: chroma.scale( + [ + colors.base00, + colors.base01, + colors.base02, + colors.base03, + colors.base04, + colors.base05, + colors.base06, + 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)), + }, + syntax + ) +} + +export const dark = theme(variant) + +export const meta: Meta = variant.meta diff --git a/styles/src/themes/atelier-seaside-dark.ts b/styles/src/themes/atelier-seaside-dark.ts new file mode 100644 index 0000000000000000000000000000000000000000..1326a277861e417711c3ea30d39230b227c9ac51 --- /dev/null +++ b/styles/src/themes/atelier-seaside-dark.ts @@ -0,0 +1,66 @@ +import chroma from "chroma-js" +import { Meta } from "./common/colorScheme" +import { colorRamp, createColorScheme } from "./common/ramps" +import { metaCommon, name, buildSyntax, Variant } from "./common/atelier-common" + +const variant: Variant = { + meta: { + name: `${name} Seaside Dark`, + ...metaCommon, + url: "https://atelierbram.github.io/syntax-highlighting/atelier-schemes/seaside/", + }, + colors: { + base00: "#131513", + base01: "#242924", + base02: "#5e6e5e", + base03: "#687d68", + base04: "#809980", + base05: "#8ca68c", + base06: "#cfe8cf", + base07: "#f4fbf4", + base08: "#e6193c", + base09: "#87711d", + base0A: "#98981b", + base0B: "#29a329", + base0C: "#1999b3", + base0D: "#3d62f5", + base0E: "#ad2bee", + base0F: "#e619c3", + }, +} + +const syntax = buildSyntax(variant) + +const theme = (variant: Variant) => { + const { meta, colors } = variant + + return createColorScheme( + meta.name, + false, + { + neutral: chroma.scale([ + colors.base00, + colors.base01, + colors.base02, + colors.base03, + colors.base04, + colors.base05, + 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)), + }, + syntax + ) +} + +export const dark = theme(variant) + +export const meta: Meta = variant.meta diff --git a/styles/src/themes/atelier-seaside-light.ts b/styles/src/themes/atelier-seaside-light.ts new file mode 100644 index 0000000000000000000000000000000000000000..6f6823718aed9ba4e3d7325b9968b11ba5472b40 --- /dev/null +++ b/styles/src/themes/atelier-seaside-light.ts @@ -0,0 +1,68 @@ +import chroma from "chroma-js" +import { Meta } from "./common/colorScheme" +import { colorRamp, createColorScheme } from "./common/ramps" +import { metaCommon, name, buildSyntax, Variant } from "./common/atelier-common" + +const variant: Variant = { + meta: { + name: `${name} Seaside Light`, + ...metaCommon, + url: "https://atelierbram.github.io/syntax-highlighting/atelier-schemes/seaside/", + }, + colors: { + base00: "#f4fbf4", + base01: "#cfe8cf", + base02: "#8ca68c", + base03: "#809980", + base04: "#687d68", + base05: "#5e6e5e", + base06: "#242924", + base07: "#131513", + base08: "#e6193c", + base09: "#87711d", + base0A: "#98981b", + base0B: "#29a329", + base0C: "#1999b3", + base0D: "#3d62f5", + base0E: "#ad2bee", + base0F: "#e619c3", + }, +} + +const syntax = buildSyntax(variant) + +const theme = (variant: Variant) => { + const { meta, colors } = variant + + return createColorScheme( + meta.name, + true, + { + neutral: chroma.scale( + [ + colors.base00, + colors.base01, + colors.base02, + colors.base03, + colors.base04, + colors.base05, + colors.base06, + 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)), + }, + syntax + ) +} + +export const dark = theme(variant) + +export const meta: Meta = variant.meta diff --git a/styles/src/themes/atelier-sulphurpool-dark.ts b/styles/src/themes/atelier-sulphurpool-dark.ts new file mode 100644 index 0000000000000000000000000000000000000000..dcfc0d932a67d7081f5bcdb195f6fc57cfc2d79f --- /dev/null +++ b/styles/src/themes/atelier-sulphurpool-dark.ts @@ -0,0 +1,66 @@ +import chroma from "chroma-js" +import { Meta } from "./common/colorScheme" +import { colorRamp, createColorScheme } from "./common/ramps" +import { metaCommon, name, buildSyntax, Variant } from "./common/atelier-common" + +const variant: Variant = { + meta: { + name: `${name} Sulphurpool Dark`, + ...metaCommon, + url: "https://atelierbram.github.io/syntax-highlighting/atelier-schemes/sulphurpool/", + }, + colors: { + base00: "#202746", + base01: "#293256", + base02: "#5e6687", + base03: "#6b7394", + base04: "#898ea4", + base05: "#979db4", + base06: "#dfe2f1", + base07: "#f5f7ff", + base08: "#c94922", + base09: "#c76b29", + base0A: "#c08b30", + base0B: "#ac9739", + base0C: "#22a2c9", + base0D: "#3d8fd1", + base0E: "#6679cc", + base0F: "#9c637a", + }, +} + +const syntax = buildSyntax(variant) + +const theme = (variant: Variant) => { + const { meta, colors } = variant + + return createColorScheme( + meta.name, + false, + { + neutral: chroma.scale([ + colors.base00, + colors.base01, + colors.base02, + colors.base03, + colors.base04, + colors.base05, + 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)), + }, + syntax + ) +} + +export const dark = theme(variant) + +export const meta: Meta = variant.meta diff --git a/styles/src/themes/atelier-sulphurpool-light.ts b/styles/src/themes/atelier-sulphurpool-light.ts new file mode 100644 index 0000000000000000000000000000000000000000..b2b5f7c328ba86be296be0fb938bf850571dbef9 --- /dev/null +++ b/styles/src/themes/atelier-sulphurpool-light.ts @@ -0,0 +1,68 @@ +import chroma from "chroma-js" +import { Meta } from "./common/colorScheme" +import { colorRamp, createColorScheme } from "./common/ramps" +import { metaCommon, name, buildSyntax, Variant } from "./common/atelier-common" + +const variant: Variant = { + meta: { + name: `${name} Sulphurpool Light`, + ...metaCommon, + url: "https://atelierbram.github.io/syntax-highlighting/atelier-schemes/sulphurpool/", + }, + colors: { + base00: "#f5f7ff", + base01: "#dfe2f1", + base02: "#979db4", + base03: "#898ea4", + base04: "#6b7394", + base05: "#5e6687", + base06: "#293256", + base07: "#202746", + base08: "#c94922", + base09: "#c76b29", + base0A: "#c08b30", + base0B: "#ac9739", + base0C: "#22a2c9", + base0D: "#3d8fd1", + base0E: "#6679cc", + base0F: "#9c637a", + }, +} + +const syntax = buildSyntax(variant) + +const theme = (variant: Variant) => { + const { meta, colors } = variant + + return createColorScheme( + meta.name, + true, + { + neutral: chroma.scale( + [ + colors.base00, + colors.base01, + colors.base02, + colors.base03, + colors.base04, + colors.base05, + colors.base06, + 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)), + }, + syntax + ) +} + +export const dark = theme(variant) + +export const meta: Meta = variant.meta diff --git a/styles/src/themes/atelier-sulphurpool.ts b/styles/src/themes/atelier-sulphurpool.ts deleted file mode 100644 index e34bc802389a20d7cf6a4cc1cb8dfbff80ed1cd8..0000000000000000000000000000000000000000 --- a/styles/src/themes/atelier-sulphurpool.ts +++ /dev/null @@ -1,45 +0,0 @@ -import chroma from "chroma-js" -import { Meta } from "./common/colorScheme" -import { colorRamp, createColorScheme } from "./common/ramps" - -const name = "Atelier Sulphurpool" - -const ramps = { - neutral: chroma - .scale([ - "#202746", - "#293256", - "#5e6687", - "#6b7394", - "#898ea4", - "#979db4", - "#dfe2f1", - "#f5f7ff", - ]) - .domain([0, 0.2, 0.38, 0.45, 0.65, 0.7, 0.85, 1]), - red: colorRamp(chroma("#c94922")), - orange: colorRamp(chroma("#c76b29")), - yellow: colorRamp(chroma("#c08b30")), - green: colorRamp(chroma("#ac9739")), - cyan: colorRamp(chroma("#22a2c9")), - blue: colorRamp(chroma("#3d8fd1")), - violet: colorRamp(chroma("#6679cc")), - magenta: colorRamp(chroma("#9c637a")), -} - -export const dark = createColorScheme(`${name} Dark`, false, ramps) -export const light = createColorScheme(`${name} Light`, true, ramps) - -export const meta: Meta = { - name, - author: "atelierbram", - license: { - SPDX: "MIT", - license_text: { - https_url: "https://atelierbram.mit-license.org/license.txt", - license_checksum: - "f95ce526ef4e7eecf7a832bba0e3451cc1000f9ce63eb01ed6f64f8109f5d0a5", - } - }, - url: "https://atelierbram.github.io/syntax-highlighting/atelier-schemes/sulphurpool/", -} diff --git a/styles/src/themes/common/atelier-common.ts b/styles/src/themes/common/atelier-common.ts new file mode 100644 index 0000000000000000000000000000000000000000..08a915d01948f300441d0904cbff2fd406e3dcba --- /dev/null +++ b/styles/src/themes/common/atelier-common.ts @@ -0,0 +1,66 @@ +import { License, Meta, ThemeSyntax } from "./colorScheme" + +export interface Variant { + meta: Meta + colors: { + base00: string + base01: string + base02: string + base03: string + base04: string + base05: string + base06: string + base07: string + base08: string + base09: string + base0A: string + base0B: string + base0C: string + base0D: string + base0E: string + base0F: string + } +} + +export const metaCommon: { + author: string + license: License +} = { + author: "Bram de Haan (http://atelierbramdehaan.nl)", + license: { + SPDX: "MIT", + license_text: { + https_url: "https://atelierbram.mit-license.org/license.txt", + license_checksum: + "f95ce526ef4e7eecf7a832bba0e3451cc1000f9ce63eb01ed6f64f8109f5d0a5", + }, + }, +} + +export const buildSyntax = (variant: Variant): ThemeSyntax => { + const { colors } = variant + return { + primary: { color: colors.base06 }, + comment: { color: colors.base03 }, + "punctuation.delimiter": { color: colors.base05 }, + "punctuation.bracket": { color: colors.base05 }, + "punctuation.special": { color: colors.base0F }, + "string.special.symbol": { color: colors.base0B }, + operator: { color: colors.base05 }, + function: { color: colors.base0D }, + "function.method": { color: colors.base0D }, + "function.special.definition": { color: colors.base0A }, + string: { color: colors.base0B }, + "string.special": { color: colors.base0F }, + "string.regex": { color: colors.base0C }, + type: { color: colors.base0A }, + number: { color: colors.base09 }, + property: { color: colors.base08 }, + variable: { color: colors.base06 }, + "variable.special": { color: colors.base0E }, + variant: { color: colors.base0A }, + keyword: { color: colors.base0E }, + } +} + +export const name = "Atelier" diff --git a/styles/src/themes/gruvbox-common.ts b/styles/src/themes/gruvbox-common.ts index c3e63a31cf12ca19419548677a598661cafae39c..c42362c11c007e5371ad7134c7300667fe04304e 100644 --- a/styles/src/themes/gruvbox-common.ts +++ b/styles/src/themes/gruvbox-common.ts @@ -248,7 +248,8 @@ export const meta: Meta = { name, license: { SPDX: "MIT", // "MIT/X11" - license_text: "Copyright \n\nPermission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files(the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/ or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 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." + license_text: + "Copyright \n\nPermission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files(the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/ or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 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.", }, author: "morhetz ", url: "https://github.com/morhetz/gruvbox", diff --git a/styles/src/themes/gruvbox-dark-hard.ts b/styles/src/themes/gruvbox-dark-hard.ts index a6e20e1020945474320537b332884d123fbaa543..3723de49015ab0cfe0f975a15943c4d4e7f35548 100644 --- a/styles/src/themes/gruvbox-dark-hard.ts +++ b/styles/src/themes/gruvbox-dark-hard.ts @@ -1,6 +1,6 @@ -import { darkHard as dark, meta as commonMeta } from "./gruvbox-common"; +import { darkHard as dark, meta as commonMeta } from "./gruvbox-common" -let meta = { ...commonMeta }; +let meta = { ...commonMeta } meta.name = `${commonMeta.name} Dark Hard` -export { dark, meta } \ No newline at end of file +export { dark, meta } diff --git a/styles/src/themes/gruvbox-dark-soft.ts b/styles/src/themes/gruvbox-dark-soft.ts index 7eaefaa99ac3df5190506820215a581036ae1eb7..2887572eadb6e757e0611b00301c3edb790fda59 100644 --- a/styles/src/themes/gruvbox-dark-soft.ts +++ b/styles/src/themes/gruvbox-dark-soft.ts @@ -1,7 +1,6 @@ -import { darkSoft as dark, meta as commonMeta } from "./gruvbox-common"; +import { darkSoft as dark, meta as commonMeta } from "./gruvbox-common" - -let meta = { ...commonMeta }; +let meta = { ...commonMeta } meta.name = `${commonMeta.name} Dark Soft` -export { dark, meta } \ No newline at end of file +export { dark, meta } diff --git a/styles/src/themes/gruvbox-dark.ts b/styles/src/themes/gruvbox-dark.ts index ea21933714e2bcc33e0eed28bb46f2998ebb0a16..cff7bd8bf94af1a839c043bb93165d7077912c6f 100644 --- a/styles/src/themes/gruvbox-dark.ts +++ b/styles/src/themes/gruvbox-dark.ts @@ -1,7 +1,6 @@ -import { darkDefault as dark, meta as commonMeta } from "./gruvbox-common"; +import { darkDefault as dark, meta as commonMeta } from "./gruvbox-common" - -let meta = { ...commonMeta }; +let meta = { ...commonMeta } meta.name = `${commonMeta.name} Dark` -export { dark, meta } \ No newline at end of file +export { dark, meta } diff --git a/styles/src/themes/gruvbox-light-hard.ts b/styles/src/themes/gruvbox-light-hard.ts index b0f42ca4b7aea8fea65e1565cf61388045c8371f..cf998ce588bfc56b168e05a3c03cd2c9a568dadd 100644 --- a/styles/src/themes/gruvbox-light-hard.ts +++ b/styles/src/themes/gruvbox-light-hard.ts @@ -1,6 +1,6 @@ -import { lightHard as light, meta as commonMeta } from "./gruvbox-common"; +import { lightHard as light, meta as commonMeta } from "./gruvbox-common" -let meta = { ...commonMeta }; +let meta = { ...commonMeta } meta.name = `${commonMeta.name} Dark Soft` -export { light, meta } \ No newline at end of file +export { light, meta } diff --git a/styles/src/themes/gruvbox-light-soft.ts b/styles/src/themes/gruvbox-light-soft.ts index 6743d29232931de6b5041977ba0c87cbec7a1e1a..90ec82e965e6ef5f36c173fee837162e0b1aefaf 100644 --- a/styles/src/themes/gruvbox-light-soft.ts +++ b/styles/src/themes/gruvbox-light-soft.ts @@ -1,6 +1,6 @@ -import { lightSoft as light, meta as commonMeta } from "./gruvbox-common"; +import { lightSoft as light, meta as commonMeta } from "./gruvbox-common" -let meta = { ...commonMeta }; +let meta = { ...commonMeta } meta.name = `${commonMeta.name} Light Soft` -export { light, meta } \ No newline at end of file +export { light, meta } diff --git a/styles/src/themes/gruvbox-light.ts b/styles/src/themes/gruvbox-light.ts index f64778d8564dbdcfd4e56b64d338ae1d7f3e3b2a..e8f355cd11482c3ca82462c5ca1fa239071e6b84 100644 --- a/styles/src/themes/gruvbox-light.ts +++ b/styles/src/themes/gruvbox-light.ts @@ -1,6 +1,6 @@ -import { lightDefault as light, meta as commonMeta } from "./gruvbox-common"; +import { lightDefault as light, meta as commonMeta } from "./gruvbox-common" -let meta = { ...commonMeta }; +let meta = { ...commonMeta } meta.name = `${commonMeta.name} Light` -export { light, meta } \ No newline at end of file +export { light, meta } diff --git a/styles/src/themes/one-dark.ts b/styles/src/themes/one-dark.ts index f99b4114f3ec9fa35f97705cb708f3c300e460fe..85417a0e68651341edded4689425b15a0d8f29b1 100644 --- a/styles/src/themes/one-dark.ts +++ b/styles/src/themes/one-dark.ts @@ -79,7 +79,7 @@ export const meta: Meta = { "https://raw.githubusercontent.com/atom/atom/master/packages/one-light-ui/LICENSE.md", license_checksum: "d5af8fc171f6f600c0ab4e7597dca398dda80dbe6821ce01cef78e859e7a00f8", - } + }, }, url: "https://github.com/atom/atom/tree/master/packages/one-dark-ui", } diff --git a/styles/src/themes/one-light.ts b/styles/src/themes/one-light.ts index 534e4c16481a6dac6635f74084a2fcea1b36bb8d..7bf21aee17d4871681b33174e3b20bea06ced9fe 100644 --- a/styles/src/themes/one-light.ts +++ b/styles/src/themes/one-light.ts @@ -78,7 +78,7 @@ export const meta: Meta = { "https://raw.githubusercontent.com/atom/atom/master/packages/one-light-ui/LICENSE.md", license_checksum: "d5af8fc171f6f600c0ab4e7597dca398dda80dbe6821ce01cef78e859e7a00f8", - } + }, }, url: "https://github.com/atom/atom/tree/master/packages/one-light-ui", } diff --git a/styles/src/themes/rose-pine-dawn.ts b/styles/src/themes/rose-pine-dawn.ts index 7f7d52079d4b2ea0977c04e8b2e121b6e7f14bf3..427b05f72b8e5672897b7457738d4c43e2c6f603 100644 --- a/styles/src/themes/rose-pine-dawn.ts +++ b/styles/src/themes/rose-pine-dawn.ts @@ -39,7 +39,7 @@ export const meta: Meta = { "https://raw.githubusercontent.com/edunfelt/base16-rose-pine-scheme/main/LICENSE", license_checksum: "6ca1b9da8c78c8441c5aa43d024a4e4a7bf59d1ecca1480196e94fda0f91ee4a", - } + }, }, url: "https://github.com/edunfelt/base16-rose-pine-scheme", } diff --git a/styles/src/themes/rose-pine-moon.ts b/styles/src/themes/rose-pine-moon.ts index a89d44487e04a9e48836b619d78967ea1576953f..be2f5a8dafd366803f13737c75bdb48d7e101aa9 100644 --- a/styles/src/themes/rose-pine-moon.ts +++ b/styles/src/themes/rose-pine-moon.ts @@ -39,7 +39,7 @@ export const meta: Meta = { "https://raw.githubusercontent.com/edunfelt/base16-rose-pine-scheme/main/LICENSE", license_checksum: "6ca1b9da8c78c8441c5aa43d024a4e4a7bf59d1ecca1480196e94fda0f91ee4a", - } + }, }, url: "https://github.com/edunfelt/base16-rose-pine-scheme", } diff --git a/styles/src/themes/rose-pine.ts b/styles/src/themes/rose-pine.ts index 87c9d34ffefe5d2e12bb1ce8cdfbfd3e6f058b6a..944550f1250ad01146f8622a8199cf027f4658ab 100644 --- a/styles/src/themes/rose-pine.ts +++ b/styles/src/themes/rose-pine.ts @@ -37,7 +37,7 @@ export const meta: Meta = { "https://raw.githubusercontent.com/edunfelt/base16-rose-pine-scheme/main/LICENSE", license_checksum: "6ca1b9da8c78c8441c5aa43d024a4e4a7bf59d1ecca1480196e94fda0f91ee4a", - } + }, }, url: "https://github.com/edunfelt/base16-rose-pine-scheme", } diff --git a/styles/src/themes/sandcastle.ts b/styles/src/themes/sandcastle.ts index 86179db3dbacd91f42a38739acb42efb55e7c745..483f01b27a1850463348e0a9c575086a014d5f1b 100644 --- a/styles/src/themes/sandcastle.ts +++ b/styles/src/themes/sandcastle.ts @@ -37,7 +37,7 @@ export const meta: Meta = { "https://raw.githubusercontent.com/gessig/base16-sandcastle-scheme/master/LICENSE", license_checksum: "8399d44b4d935b60be9fee0a76d7cc9a817b4f3f11574c9d6d1e8fd57e72ffdc", - } + }, }, url: "https://github.com/gessig/base16-sandcastle-scheme", } diff --git a/styles/src/themes/solarized.ts b/styles/src/themes/solarized.ts index 42306f68c3a6c4deea12006b88a320b596713ec1..1210c4380608e812d39dec0f36d6ec69dab37e9b 100644 --- a/styles/src/themes/solarized.ts +++ b/styles/src/themes/solarized.ts @@ -40,7 +40,7 @@ export const meta: Metadata = { "https://raw.githubusercontent.com/altercation/solarized/master/LICENSE", license_checksum: "494aefdabf86acce06bd63001ad8aedad4ee38da23509d3f917d95aa3368b9a6", - } + }, }, url: "https://github.com/altercation/solarized", } diff --git a/styles/src/themes/staff/abruzzo.ts b/styles/src/themes/staff/abruzzo.ts deleted file mode 100644 index 43b75f26133c2aeca4d1e4c1369f31ee25a0358b..0000000000000000000000000000000000000000 --- a/styles/src/themes/staff/abruzzo.ts +++ /dev/null @@ -1,31 +0,0 @@ -import chroma from "chroma-js" -import { colorRamp, createColorScheme } from "../common/ramps" - -const name = "Abruzzo" -const author = "slightknack " -const url = "https://github.com/slightknack" -const license = { - type: "", - url: "", -} - -export const dark = createColorScheme(name, false, { - neutral: chroma.scale([ - "#1b0d05", - "#2c1e18", - "#654035", - "#9d5e4a", - "#b37354", - "#c1825a", - "#dda66e", - "#fbf3e2", - ]), - red: colorRamp(chroma("#e594c4")), - orange: colorRamp(chroma("#d9e87e")), - yellow: colorRamp(chroma("#fd9d83")), - green: colorRamp(chroma("#96adf7")), - cyan: colorRamp(chroma("#fc798f")), - blue: colorRamp(chroma("#BCD0F5")), - violet: colorRamp(chroma("#dac5eb")), - magenta: colorRamp(chroma("#c1a3ef")), -}) diff --git a/styles/src/themes/staff/atelier-dune.ts b/styles/src/themes/staff/atelier-dune.ts deleted file mode 100644 index bd39098dc3575ce8a0a4c515e6a33d151a521658..0000000000000000000000000000000000000000 --- a/styles/src/themes/staff/atelier-dune.ts +++ /dev/null @@ -1,35 +0,0 @@ -import chroma from "chroma-js" -import { colorRamp, createColorScheme } from "../common/ramps" - -const name = "Atelier Dune" -const author = "atelierbram" -const url = - "https://atelierbram.github.io/syntax-highlighting/atelier-schemes/dune/" -const license = { - type: "MIT", - url: "https://github.com/atelierbram/syntax-highlighting/blob/master/LICENSE", -} - -const ramps = { - neutral: chroma.scale([ - "#20201d", - "#292824", - "#6e6b5e", - "#7d7a68", - "#999580", - "#a6a28c", - "#e8e4cf", - "#fefbec", - ]), - red: colorRamp(chroma("#d73737")), - orange: colorRamp(chroma("#b65611")), - yellow: colorRamp(chroma("#ae9513")), - green: colorRamp(chroma("#60ac39")), - cyan: colorRamp(chroma("#1fad83")), - blue: colorRamp(chroma("#6684e1")), - violet: colorRamp(chroma("#b854d4")), - magenta: colorRamp(chroma("#d43552")), -} - -export const dark = createColorScheme(`${name} Dark`, false, ramps) -export const light = createColorScheme(`${name} Light`, true, ramps) diff --git a/styles/src/themes/staff/atelier-heath.ts b/styles/src/themes/staff/atelier-heath.ts deleted file mode 100644 index cd4eb2a1ac597c127b384c6f83f478c78737ed4d..0000000000000000000000000000000000000000 --- a/styles/src/themes/staff/atelier-heath.ts +++ /dev/null @@ -1,54 +0,0 @@ -import chroma from "chroma-js" -import { colorRamp, createColorScheme } from "../common/ramps" - -const name = "Atelier Heath" -const author = "atelierbram" -const url = - "https://atelierbram.github.io/syntax-highlighting/atelier-schemes/heath/" -const license = { - type: "MIT", - url: "https://github.com/atelierbram/syntax-highlighting/blob/master/LICENSE", -} - -// `name-[light|dark]`, isLight, color ramps -export const dark = createColorScheme(`${name} Dark`, false, { - neutral: chroma.scale([ - "#1b181b", - "#292329", - "#695d69", - "#776977", - "#9e8f9e", - "#ab9bab", - "#d8cad8", - "#f7f3f7", - ]), - red: colorRamp(chroma("#ca402b")), - orange: colorRamp(chroma("#a65926")), - yellow: colorRamp(chroma("#bb8a35")), - green: colorRamp(chroma("#918b3b")), - cyan: colorRamp(chroma("#159393")), - blue: colorRamp(chroma("#516aec")), - violet: colorRamp(chroma("#7b59c0")), - magenta: colorRamp(chroma("#cc33cc")), -}) - -export const light = createColorScheme(`${name} Light`, true, { - neutral: chroma.scale([ - "#161b1d", - "#1f292e", - "#516d7b", - "#5a7b8c", - "#7195a8", - "#7ea2b4", - "#c1e4f6", - "#ebf8ff", - ]), - red: colorRamp(chroma("#d22d72")), - orange: colorRamp(chroma("#935c25")), - yellow: colorRamp(chroma("#8a8a0f")), - green: colorRamp(chroma("#568c3b")), - cyan: colorRamp(chroma("#2d8f6f")), - blue: colorRamp(chroma("#257fad")), - violet: colorRamp(chroma("#6b6bb8")), - magenta: colorRamp(chroma("#b72dd2")), -}) diff --git a/styles/src/themes/staff/atelier-seaside.ts b/styles/src/themes/staff/atelier-seaside.ts deleted file mode 100644 index 13286e77f929b11ebce5c0a5405f505d9ee34669..0000000000000000000000000000000000000000 --- a/styles/src/themes/staff/atelier-seaside.ts +++ /dev/null @@ -1,35 +0,0 @@ -import chroma from "chroma-js" -import { colorRamp, createColorScheme } from "../common/ramps" - -const name = "Atelier Seaside" -const author = "atelierbram" -const url = - "https://atelierbram.github.io/syntax-highlighting/atelier-schemes/seaside/" -const license = { - type: "MIT", - url: "https://github.com/atelierbram/syntax-highlighting/blob/master/LICENSE", -} - -const ramps = { - neutral: chroma.scale([ - "#131513", - "#242924", - "#5e6e5e", - "#687d68", - "#809980", - "#8ca68c", - "#cfe8cf", - "#f4fbf4", - ]), - red: colorRamp(chroma("#e6193c")), - orange: colorRamp(chroma("#87711d")), - yellow: colorRamp(chroma("#98981b")), - green: colorRamp(chroma("#29a329")), - cyan: colorRamp(chroma("#1999b3")), - blue: colorRamp(chroma("#3d62f5")), - violet: colorRamp(chroma("#ad2bee")), - magenta: colorRamp(chroma("#e619c3")), -} - -export const dark = createColorScheme(`${name} Dark`, false, ramps) -export const light = createColorScheme(`${name} Light`, true, ramps) diff --git a/styles/src/themes/staff/brushtrees.ts b/styles/src/themes/staff/brushtrees.ts deleted file mode 100644 index a17cf92acb563b4ceea672f79436566062c86db8..0000000000000000000000000000000000000000 --- a/styles/src/themes/staff/brushtrees.ts +++ /dev/null @@ -1,73 +0,0 @@ -import chroma from "chroma-js" -import { colorRamp, createColorScheme } from "../common/ramps" - -const name = "Brush Trees" -const author = "Abraham White " -const url = "https://github.com/WhiteAbeLincoln/base16-brushtrees-scheme" -const license = { - type: "MIT", - url: "https://github.com/WhiteAbeLincoln/base16-brushtrees-scheme/blob/master/LICENSE", -} - -export const dark = createColorScheme(`${name} Dark`, false, { - neutral: chroma.scale([ - "#485867", - "#5A6D7A", - "#6D828E", - "#8299A1", - "#98AFB5", - "#B0C5C8", - "#C9DBDC", - "#E3EFEF", - ]), - red: colorRamp(chroma("#b38686")), - orange: colorRamp(chroma("#d8bba2")), - yellow: colorRamp(chroma("#aab386")), - green: colorRamp(chroma("#87b386")), - cyan: colorRamp(chroma("#86b3b3")), - blue: colorRamp(chroma("#868cb3")), - violet: colorRamp(chroma("#b386b2")), - magenta: colorRamp(chroma("#b39f9f")), -}) - -export const mirage = createColorScheme(`${name} Mirage`, false, { - neutral: chroma.scale([ - "#485867", - "#5A6D7A", - "#6D828E", - "#8299A1", - "#98AFB5", - "#B0C5C8", - "#C9DBDC", - "#E3EFEF", - ]), - red: colorRamp(chroma("#F28779")), - orange: colorRamp(chroma("#FFAD66")), - yellow: colorRamp(chroma("#FFD173")), - green: colorRamp(chroma("#D5FF80")), - cyan: colorRamp(chroma("#95E6CB")), - blue: colorRamp(chroma("#5CCFE6")), - violet: colorRamp(chroma("#D4BFFF")), - magenta: colorRamp(chroma("#F29E74")), -}) - -export const light = createColorScheme(`${name} Light`, true, { - neutral: chroma.scale([ - "#1A1F29", - "#242936", - "#5C6773", - "#828C99", - "#ABB0B6", - "#F8F9FA", - "#F3F4F5", - "#FAFAFA", - ]), - red: colorRamp(chroma("#b38686")), - orange: colorRamp(chroma("#d8bba2")), - yellow: colorRamp(chroma("#aab386")), - green: colorRamp(chroma("#87b386")), - cyan: colorRamp(chroma("#86b3b3")), - blue: colorRamp(chroma("#868cb3")), - violet: colorRamp(chroma("#b386b2")), - magenta: colorRamp(chroma("#b39f9f")), -}) diff --git a/styles/src/themes/staff/dracula.ts b/styles/src/themes/staff/dracula.ts deleted file mode 100644 index acff08d230378eae13522712a8b74419a33edf05..0000000000000000000000000000000000000000 --- a/styles/src/themes/staff/dracula.ts +++ /dev/null @@ -1,31 +0,0 @@ -import chroma from "chroma-js" -import { colorRamp, createColorScheme } from "../common/ramps" - -const name = "Dracula" -const author = "zenorocha" -const url = "https://github.com/dracula/dracula-theme" -const license = { - type: "MIT", - url: "https://github.com/dracula/dracula-theme/blob/master/LICENSE", -} - -export const dark = createColorScheme(name, false, { - neutral: chroma.scale([ - "#282A36", - "#3a3c4e", - "#4d4f68", - "#626483", - "#62d6e8", - "#e9e9f4", - "#f1f2f8", - "#f8f8f2", - ]), - red: colorRamp(chroma("#ff5555")), - orange: colorRamp(chroma("#ffb86c")), - yellow: colorRamp(chroma("#f1fa8c")), - green: colorRamp(chroma("#50fa7b")), - cyan: colorRamp(chroma("#8be9fd")), - blue: colorRamp(chroma("#6272a4")), - violet: colorRamp(chroma("#bd93f9")), - magenta: colorRamp(chroma("#00f769")), -}) diff --git a/styles/src/themes/staff/monokai.ts b/styles/src/themes/staff/monokai.ts deleted file mode 100644 index 75319e527a7d088721083e0e639e1ba66bc636cc..0000000000000000000000000000000000000000 --- a/styles/src/themes/staff/monokai.ts +++ /dev/null @@ -1,32 +0,0 @@ -import chroma from "chroma-js" -import { colorRamp, createColorScheme } from "../common/ramps" - -const name = "Monokai" -const author = "Wimer Hazenberg (http://www.monokai.nl)" -const url = "https://base16.netlify.app/previews/base16-monokai.html" -const license = { - type: "?", - url: "?", -} - -// `name-[light|dark]`, isLight, color ramps -export const dark = createColorScheme(name, false, { - neutral: chroma.scale([ - "#272822", - "#383830", - "#49483e", - "#75715e", - "#a59f85", - "#f8f8f2", - "#f5f4f1", - "#f9f8f5", - ]), - red: colorRamp(chroma("#f92672")), - orange: colorRamp(chroma("#fd971f")), - yellow: colorRamp(chroma("#f4bf75")), - green: colorRamp(chroma("#a6e22e")), - cyan: colorRamp(chroma("#a1efe4")), - blue: colorRamp(chroma("#66d9ef")), - violet: colorRamp(chroma("#ae81ff")), - magenta: colorRamp(chroma("#cc6633")), -}) diff --git a/styles/src/themes/staff/nord.ts b/styles/src/themes/staff/nord.ts deleted file mode 100644 index 48f924e7a403cb5118ea96797afca75b703ef152..0000000000000000000000000000000000000000 --- a/styles/src/themes/staff/nord.ts +++ /dev/null @@ -1,32 +0,0 @@ -import chroma from "chroma-js" -import { colorRamp, createColorScheme } from "../common/ramps" - -const name = "Nord" -const author = "arcticicestudio" -const url = "https://www.nordtheme.com/" -const license = { - type: "MIT", - url: "https://github.com/arcticicestudio/nord/blob/develop/LICENSE.md", -} - -// `name-[light|dark]`, isLight, color ramps -export const dark = createColorScheme(name, false, { - neutral: chroma.scale([ - "#2E3440", - "#3B4252", - "#434C5E", - "#4C566A", - "#D8DEE9", - "#E5E9F0", - "#ECEFF4", - "#8FBCBB", - ]), - red: colorRamp(chroma("#88C0D0")), - orange: colorRamp(chroma("#81A1C1")), - yellow: colorRamp(chroma("#5E81AC")), - green: colorRamp(chroma("#BF616A")), - cyan: colorRamp(chroma("#D08770")), - blue: colorRamp(chroma("#EBCB8B")), - violet: colorRamp(chroma("#A3BE8C")), - magenta: colorRamp(chroma("#B48EAD")), -}) diff --git a/styles/src/themes/staff/seti-ui.ts b/styles/src/themes/staff/seti-ui.ts deleted file mode 100644 index abf6624242ce48373a5675a717cf217ec0fdd360..0000000000000000000000000000000000000000 --- a/styles/src/themes/staff/seti-ui.ts +++ /dev/null @@ -1,32 +0,0 @@ -import chroma from "chroma-js" -import { colorRamp, createColorScheme } from "../common/ramps" - -const name = "Seti UI" -const author = "jesseweed" -const url = "https://github.com/jesseweed/seti-ui" -const license = { - type: "MIT", - url: "https://github.com/jesseweed/seti-ui/blob/master/LICENSE.md", -} - -// `name-[light|dark]`, isLight, color ramps -export const dark = createColorScheme(name, false, { - neutral: chroma.scale([ - "#151718", - "#262B30", - "#1E2326", - "#41535B", - "#43a5d5", - "#d6d6d6", - "#eeeeee", - "#ffffff", - ]), - red: colorRamp(chroma("#Cd3f45")), - orange: colorRamp(chroma("#db7b55")), - yellow: colorRamp(chroma("#e6cd69")), - green: colorRamp(chroma("#9fca56")), - cyan: colorRamp(chroma("#55dbbe")), - blue: colorRamp(chroma("#55b5db")), - violet: colorRamp(chroma("#a074c4")), - magenta: colorRamp(chroma("#8a553f")), -}) diff --git a/styles/src/themes/staff/tokyo-night-storm.ts b/styles/src/themes/staff/tokyo-night-storm.ts deleted file mode 100644 index 399c5268721c05e3696a8881e5eb97e0967e83fe..0000000000000000000000000000000000000000 --- a/styles/src/themes/staff/tokyo-night-storm.ts +++ /dev/null @@ -1,32 +0,0 @@ -import chroma from "chroma-js" -import { colorRamp, createColorScheme } from "../common/ramps" - -const name = "Tokyo Night Storm" -const author = "folke" -const url = "https://github.com/folke/tokyonight.nvim" -const license = { - type: "MIT", - url: "https://github.com/ghifarit53/tokyonight-vim/blob/master/LICENSE", -} - -// `name-[light|dark]`, isLight, color ramps -export const dark = createColorScheme(name, false, { - neutral: chroma.scale([ - "#24283B", - "#16161E", - "#343A52", - "#444B6A", - "#787C99", - "#A9B1D6", - "#CBCCD1", - "#D5D6DB", - ]), - red: colorRamp(chroma("#C0CAF5")), - orange: colorRamp(chroma("#A9B1D6")), - yellow: colorRamp(chroma("#0DB9D7")), - green: colorRamp(chroma("#9ECE6A")), - cyan: colorRamp(chroma("#B4F9F8")), - blue: colorRamp(chroma("#2AC3DE")), - violet: colorRamp(chroma("#BB9AF7")), - magenta: colorRamp(chroma("#F7768E")), -}) diff --git a/styles/src/themes/staff/tokyo-night.ts b/styles/src/themes/staff/tokyo-night.ts deleted file mode 100644 index 267b1fc031b311e3a57c479c9978041f9b7cbbeb..0000000000000000000000000000000000000000 --- a/styles/src/themes/staff/tokyo-night.ts +++ /dev/null @@ -1,53 +0,0 @@ -import chroma from "chroma-js" -import { colorRamp, createColorScheme } from "../common/ramps" - -const name = "Tokyo" -const author = "folke" -const url = "https://github.com/folke/tokyonight.nvim" -const license = { - type: "Apache License 2.0", - url: "https://github.com/folke/tokyonight.nvim/blob/main/LICENSE", -} - -// `name-[light|dark]`, isLight, color ramps -export const dark = createColorScheme(`${name} Night`, false, { - neutral: chroma.scale([ - "#1A1B26", - "#16161E", - "#2F3549", - "#444B6A", - "#787C99", - "#A9B1D6", - "#CBCCD1", - "#D5D6DB", - ]), - red: colorRamp(chroma("#C0CAF5")), - orange: colorRamp(chroma("#A9B1D6")), - yellow: colorRamp(chroma("#0DB9D7")), - green: colorRamp(chroma("#9ECE6A")), - cyan: colorRamp(chroma("#B4F9F8")), - blue: colorRamp(chroma("#2AC3DE")), - violet: colorRamp(chroma("#BB9AF7")), - magenta: colorRamp(chroma("#F7768E")), -}) - -export const light = createColorScheme(`${name} Day`, true, { - neutral: chroma.scale([ - "#1A1B26", - "#1A1B26", - "#343B59", - "#4C505E", - "#9699A3", - "#DFE0E5", - "#CBCCD1", - "#D5D6DB", - ]), - red: colorRamp(chroma("#343B58")), - orange: colorRamp(chroma("#965027")), - yellow: colorRamp(chroma("#166775")), - green: colorRamp(chroma("#485E30")), - cyan: colorRamp(chroma("#3E6968")), - blue: colorRamp(chroma("#34548A")), - violet: colorRamp(chroma("#5A4A78")), - magenta: colorRamp(chroma("#8C4351")), -}) diff --git a/styles/src/themes/staff/zed-pro.ts b/styles/src/themes/staff/zed-pro.ts deleted file mode 100644 index 9b748be2991fc4dd49785537f07bdb0056436840..0000000000000000000000000000000000000000 --- a/styles/src/themes/staff/zed-pro.ts +++ /dev/null @@ -1,36 +0,0 @@ -import chroma from "chroma-js" -import { colorRamp, createColorScheme } from "../common/ramps" - -const name = "Zed Pro" -const author = "Nate Butler" -const url = "https://github.com/iamnbutler" -const license = { - type: "?", - url: "?", -} - -const ramps = { - neutral: chroma - .scale([ - "#101010", - "#1C1C1C", - "#212121", - "#2D2D2D", - "#B9B9B9", - "#DADADA", - "#E6E6E6", - "#FFFFFF", - ]) - .domain([0, 0.1, 0.2, 0.3, 0.7, 0.8, 0.9, 1]), - red: colorRamp(chroma("#DC604F")), - orange: colorRamp(chroma("#DE782F")), - yellow: colorRamp(chroma("#E0B750")), - green: colorRamp(chroma("#2A643D")), - cyan: colorRamp(chroma("#215050")), - blue: colorRamp(chroma("#2F6DB7")), - violet: colorRamp(chroma("#5874C1")), - magenta: colorRamp(chroma("#DE9AB8")), -} - -export const dark = createColorScheme(`${name} Dark`, false, ramps) -export const light = createColorScheme(`${name} Light`, true, ramps) diff --git a/styles/src/themes/staff/zenburn.ts b/styles/src/themes/staff/zenburn.ts deleted file mode 100644 index 9457ef195795188d5f467a9d74dde441eec208b6..0000000000000000000000000000000000000000 --- a/styles/src/themes/staff/zenburn.ts +++ /dev/null @@ -1,31 +0,0 @@ -import chroma from "chroma-js" -import { colorRamp, createColorScheme } from "../common/ramps" - -const name = "Zenburn" -const author = "elnawe" -const url = "https://github.com/elnawe/base16-zenburn-scheme" -const license = { - type: "None", - url: "", -} - -export const dark = createColorScheme(name, false, { - neutral: chroma.scale([ - "#383838", - "#404040", - "#606060", - "#6f6f6f", - "#808080", - "#dcdccc", - "#c0c0c0", - "#ffffff", - ]), - red: colorRamp(chroma("#dca3a3")), - orange: colorRamp(chroma("#dfaf8f")), - yellow: colorRamp(chroma("#e0cf9f")), - green: colorRamp(chroma("#5f7f5f")), - cyan: colorRamp(chroma("#93e0e3")), - blue: colorRamp(chroma("#7cb8bb")), - violet: colorRamp(chroma("#dc8cc3")), - magenta: colorRamp(chroma("#000000")), -}) diff --git a/styles/src/themes/summercamp.ts b/styles/src/themes/summercamp.ts index e8d1bd40b2538b3904127e6699ef4236dd9d6b98..7df125e86606d299e52ffb07140f156744e086ce 100644 --- a/styles/src/themes/summercamp.ts +++ b/styles/src/themes/summercamp.ts @@ -39,6 +39,6 @@ export const meta: Meta = { "https://raw.githubusercontent.com/zoefiri/base16-sc/master/LICENSE", license_checksum: "fadcc834b7eaf2943800956600e8aeea4b495ecf6490f4c4b6c91556a90accaf", - } + }, }, }