diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 67a7ed31c9..2cd2050013 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -2,12 +2,11 @@ Release Notes: -Use `N/A` in this section if this item should be skipped in the release notes. +- N/A -Add release note lines here: +or -* (Added|Fixed|Improved) ... ([#](https://github.com/zed-industries/community/issues/)). -* ... +- (Added|Fixed|Improved) ... ([#](https://github.com/zed-industries/community/issues/)). If the release notes are only intended for a specific release channel only, add `(-only)` to the end of the release note line. These will be removed by the person making the release. diff --git a/Cargo.lock b/Cargo.lock index 35b3176749..9634cd2c8e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1251,7 +1251,7 @@ dependencies = [ [[package]] name = "collab" -version = "0.12.4" +version = "0.12.5" dependencies = [ "anyhow", "async-tungstenite", @@ -2241,6 +2241,7 @@ dependencies = [ "log", "postage", "project", + "regex", "search", "serde", "serde_derive", @@ -8781,7 +8782,7 @@ dependencies = [ [[package]] name = "zed" -version = "0.89.0" +version = "0.90.0" dependencies = [ "activity_indicator", "ai", diff --git a/assets/keymaps/default.json b/assets/keymaps/default.json index c6964b49e2..f58e3c7cce 100644 --- a/assets/keymaps/default.json +++ b/assets/keymaps/default.json @@ -375,42 +375,9 @@ "workspace::ActivatePane", 8 ], - "cmd-b": [ - "workspace::ToggleLeftDock", - { - "focus": true - } - ], - "cmd-shift-b": [ - "workspace::ToggleLeftDock", - { - "focus": false - } - ], - "cmd-r": [ - "workspace::ToggleRightDock", - { - "focus": true - } - ], - "cmd-shift-r": [ - "workspace::ToggleRightDock", - { - "focus": false - } - ], - "cmd-j": [ - "workspace::ToggleBottomDock", - { - "focus": true - } - ], - "cmd-shift-j": [ - "workspace::ToggleBottomDock", - { - "focus": false - } - ], + "cmd-b": "workspace::ToggleLeftDock", + "cmd-r": "workspace::ToggleRightDock", + "cmd-j": "workspace::ToggleBottomDock", "cmd-shift-f": "workspace::NewSearch", "cmd-k cmd-t": "theme_selector::Toggle", "cmd-k cmd-s": "zed::OpenKeymap", diff --git a/crates/collab/Cargo.toml b/crates/collab/Cargo.toml index cd06b9a70a..07d2cdc60a 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.12.4" +version = "0.12.5" publish = false [[bin]] diff --git a/crates/collab/migrations.sqlite/20221109000000_test_schema.sql b/crates/collab/migrations.sqlite/20221109000000_test_schema.sql index 7c6a49f179..b0b2a684d9 100644 --- a/crates/collab/migrations.sqlite/20221109000000_test_schema.sql +++ b/crates/collab/migrations.sqlite/20221109000000_test_schema.sql @@ -112,6 +112,16 @@ CREATE INDEX "index_worktree_repository_statuses_on_project_id" ON "worktree_rep CREATE INDEX "index_worktree_repository_statuses_on_project_id_and_worktree_id" ON "worktree_repository_statuses" ("project_id", "worktree_id"); CREATE INDEX "index_worktree_repository_statuses_on_project_id_and_worktree_id_and_work_directory_id" ON "worktree_repository_statuses" ("project_id", "worktree_id", "work_directory_id"); +CREATE TABLE "worktree_settings_files" ( + "project_id" INTEGER NOT NULL, + "worktree_id" INTEGER NOT NULL, + "path" VARCHAR NOT NULL, + "content" TEXT, + PRIMARY KEY(project_id, worktree_id, path), + FOREIGN KEY(project_id, worktree_id) REFERENCES worktrees (project_id, id) ON DELETE CASCADE +); +CREATE INDEX "index_worktree_settings_files_on_project_id" ON "worktree_settings_files" ("project_id"); +CREATE INDEX "index_worktree_settings_files_on_project_id_and_worktree_id" ON "worktree_settings_files" ("project_id", "worktree_id"); CREATE TABLE "worktree_diagnostic_summaries" ( "project_id" INTEGER NOT NULL, diff --git a/crates/collab/migrations/20230529164700_add_worktree_settings_files.sql b/crates/collab/migrations/20230529164700_add_worktree_settings_files.sql new file mode 100644 index 0000000000..973a40af0f --- /dev/null +++ b/crates/collab/migrations/20230529164700_add_worktree_settings_files.sql @@ -0,0 +1,10 @@ +CREATE TABLE "worktree_settings_files" ( + "project_id" INTEGER NOT NULL, + "worktree_id" INT8 NOT NULL, + "path" VARCHAR NOT NULL, + "content" TEXT NOT NULL, + PRIMARY KEY(project_id, worktree_id, path), + FOREIGN KEY(project_id, worktree_id) REFERENCES worktrees (project_id, id) ON DELETE CASCADE +); +CREATE INDEX "index_settings_files_on_project_id" ON "worktree_settings_files" ("project_id"); +CREATE INDEX "index_settings_files_on_project_id_and_wt_id" ON "worktree_settings_files" ("project_id", "worktree_id"); diff --git a/crates/collab/src/db.rs b/crates/collab/src/db.rs index fd28fb9101..1deca1baa8 100644 --- a/crates/collab/src/db.rs +++ b/crates/collab/src/db.rs @@ -16,6 +16,7 @@ mod worktree_diagnostic_summary; mod worktree_entry; mod worktree_repository; mod worktree_repository_statuses; +mod worktree_settings_file; use crate::executor::Executor; use crate::{Error, Result}; @@ -1494,6 +1495,7 @@ impl Database { updated_repositories: Default::default(), removed_repositories: Default::default(), diagnostic_summaries: Default::default(), + settings_files: Default::default(), scan_id: db_worktree.scan_id as u64, completed_scan_id: db_worktree.completed_scan_id as u64, }; @@ -1638,6 +1640,25 @@ impl Database { }) .collect::>(); + { + let mut db_settings_files = worktree_settings_file::Entity::find() + .filter(worktree_settings_file::Column::ProjectId.eq(project_id)) + .stream(&*tx) + .await?; + while let Some(db_settings_file) = db_settings_files.next().await { + let db_settings_file = db_settings_file?; + if let Some(worktree) = worktrees + .iter_mut() + .find(|w| w.id == db_settings_file.worktree_id as u64) + { + worktree.settings_files.push(WorktreeSettingsFile { + path: db_settings_file.path, + content: db_settings_file.content, + }); + } + } + } + let mut collaborators = project .find_related(project_collaborator::Entity) .all(&*tx) @@ -2637,6 +2658,58 @@ impl Database { .await } + pub async fn update_worktree_settings( + &self, + update: &proto::UpdateWorktreeSettings, + connection: ConnectionId, + ) -> Result>> { + let project_id = ProjectId::from_proto(update.project_id); + let room_id = self.room_id_for_project(project_id).await?; + self.room_transaction(room_id, |tx| async move { + // Ensure the update comes from the host. + let project = project::Entity::find_by_id(project_id) + .one(&*tx) + .await? + .ok_or_else(|| anyhow!("no such project"))?; + if project.host_connection()? != connection { + return Err(anyhow!("can't update a project hosted by someone else"))?; + } + + if let Some(content) = &update.content { + worktree_settings_file::Entity::insert(worktree_settings_file::ActiveModel { + project_id: ActiveValue::Set(project_id), + worktree_id: ActiveValue::Set(update.worktree_id as i64), + path: ActiveValue::Set(update.path.clone()), + content: ActiveValue::Set(content.clone()), + }) + .on_conflict( + OnConflict::columns([ + worktree_settings_file::Column::ProjectId, + worktree_settings_file::Column::WorktreeId, + worktree_settings_file::Column::Path, + ]) + .update_column(worktree_settings_file::Column::Content) + .to_owned(), + ) + .exec(&*tx) + .await?; + } else { + worktree_settings_file::Entity::delete(worktree_settings_file::ActiveModel { + project_id: ActiveValue::Set(project_id), + worktree_id: ActiveValue::Set(update.worktree_id as i64), + path: ActiveValue::Set(update.path.clone()), + ..Default::default() + }) + .exec(&*tx) + .await?; + } + + let connection_ids = self.project_guest_connection_ids(project_id, &tx).await?; + Ok(connection_ids) + }) + .await + } + pub async fn join_project( &self, project_id: ProjectId, @@ -2707,6 +2780,7 @@ impl Database { entries: Default::default(), repository_entries: Default::default(), diagnostic_summaries: Default::default(), + settings_files: Default::default(), scan_id: db_worktree.scan_id as u64, completed_scan_id: db_worktree.completed_scan_id as u64, }, @@ -2819,6 +2893,25 @@ impl Database { } } + // Populate worktree settings files + { + let mut db_settings_files = worktree_settings_file::Entity::find() + .filter(worktree_settings_file::Column::ProjectId.eq(project_id)) + .stream(&*tx) + .await?; + while let Some(db_settings_file) = db_settings_files.next().await { + let db_settings_file = db_settings_file?; + if let Some(worktree) = + worktrees.get_mut(&(db_settings_file.worktree_id as u64)) + { + worktree.settings_files.push(WorktreeSettingsFile { + path: db_settings_file.path, + content: db_settings_file.content, + }); + } + } + } + // Populate language servers. let language_servers = project .find_related(language_server::Entity) @@ -3482,6 +3575,7 @@ pub struct RejoinedWorktree { pub updated_repositories: Vec, pub removed_repositories: Vec, pub diagnostic_summaries: Vec, + pub settings_files: Vec, pub scan_id: u64, pub completed_scan_id: u64, } @@ -3537,10 +3631,17 @@ pub struct Worktree { pub entries: Vec, pub repository_entries: BTreeMap, pub diagnostic_summaries: Vec, + pub settings_files: Vec, pub scan_id: u64, pub completed_scan_id: u64, } +#[derive(Debug)] +pub struct WorktreeSettingsFile { + pub path: String, + pub content: String, +} + #[cfg(test)] pub use test::*; diff --git a/crates/collab/src/db/worktree_settings_file.rs b/crates/collab/src/db/worktree_settings_file.rs new file mode 100644 index 0000000000..f8e87f6e59 --- /dev/null +++ b/crates/collab/src/db/worktree_settings_file.rs @@ -0,0 +1,19 @@ +use super::ProjectId; +use sea_orm::entity::prelude::*; + +#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)] +#[sea_orm(table_name = "worktree_settings_files")] +pub struct Model { + #[sea_orm(primary_key)] + pub project_id: ProjectId, + #[sea_orm(primary_key)] + pub worktree_id: i64, + #[sea_orm(primary_key)] + pub path: String, + pub content: String, +} + +#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] +pub enum Relation {} + +impl ActiveModelBehavior for ActiveModel {} diff --git a/crates/collab/src/rpc.rs b/crates/collab/src/rpc.rs index 4c117b613d..61c6123a82 100644 --- a/crates/collab/src/rpc.rs +++ b/crates/collab/src/rpc.rs @@ -200,6 +200,7 @@ impl Server { .add_message_handler(start_language_server) .add_message_handler(update_language_server) .add_message_handler(update_diagnostic_summary) + .add_message_handler(update_worktree_settings) .add_request_handler(forward_project_request::) .add_request_handler(forward_project_request::) .add_request_handler(forward_project_request::) @@ -1088,6 +1089,18 @@ async fn rejoin_room( }, )?; } + + for settings_file in worktree.settings_files { + session.peer.send( + session.connection_id, + proto::UpdateWorktreeSettings { + project_id: project.id.to_proto(), + worktree_id: worktree.id, + path: settings_file.path, + content: Some(settings_file.content), + }, + )?; + } } for language_server in &project.language_servers { @@ -1410,6 +1423,18 @@ async fn join_project( }, )?; } + + for settings_file in dbg!(worktree.settings_files) { + session.peer.send( + session.connection_id, + proto::UpdateWorktreeSettings { + project_id: project_id.to_proto(), + worktree_id: worktree.id, + path: settings_file.path, + content: Some(settings_file.content), + }, + )?; + } } for language_server in &project.language_servers { @@ -1525,6 +1550,31 @@ async fn update_diagnostic_summary( Ok(()) } +async fn update_worktree_settings( + message: proto::UpdateWorktreeSettings, + session: Session, +) -> Result<()> { + dbg!(&message); + + let guest_connection_ids = session + .db() + .await + .update_worktree_settings(&message, session.connection_id) + .await?; + + broadcast( + Some(session.connection_id), + guest_connection_ids.iter().copied(), + |connection_id| { + session + .peer + .forward_send(session.connection_id, connection_id, message.clone()) + }, + ); + + Ok(()) +} + async fn start_language_server( request: proto::StartLanguageServer, session: Session, diff --git a/crates/collab/src/tests/integration_tests.rs b/crates/collab/src/tests/integration_tests.rs index 53726113db..5bc1a4e414 100644 --- a/crates/collab/src/tests/integration_tests.rs +++ b/crates/collab/src/tests/integration_tests.rs @@ -3114,6 +3114,135 @@ async fn test_fs_operations( }); } +#[gpui::test(iterations = 10)] +async fn test_local_settings( + deterministic: Arc, + cx_a: &mut TestAppContext, + cx_b: &mut TestAppContext, +) { + deterministic.forbid_parking(); + let mut server = TestServer::start(&deterministic).await; + let client_a = server.create_client(cx_a, "user_a").await; + let client_b = server.create_client(cx_b, "user_b").await; + server + .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)]) + .await; + let active_call_a = cx_a.read(ActiveCall::global); + + // As client A, open a project that contains some local settings files + client_a + .fs + .insert_tree( + "/dir", + json!({ + ".zed": { + "settings.json": r#"{ "tab_size": 2 }"# + }, + "a": { + ".zed": { + "settings.json": r#"{ "tab_size": 8 }"# + }, + "a.txt": "a-contents", + }, + "b": { + "b.txt": "b-contents", + } + }), + ) + .await; + let (project_a, _) = client_a.build_local_project("/dir", cx_a).await; + let project_id = active_call_a + .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx)) + .await + .unwrap(); + + // As client B, join that project and observe the local settings. + let project_b = client_b.build_remote_project(project_id, cx_b).await; + let worktree_b = project_b.read_with(cx_b, |project, cx| project.worktrees(cx).next().unwrap()); + deterministic.run_until_parked(); + cx_b.read(|cx| { + let store = cx.global::(); + assert_eq!( + store.local_settings(worktree_b.id()).collect::>(), + &[ + (Path::new("").into(), r#"{"tab_size":2}"#.to_string()), + (Path::new("a").into(), r#"{"tab_size":8}"#.to_string()), + ] + ) + }); + + // As client A, update a settings file. As Client B, see the changed settings. + client_a + .fs + .insert_file("/dir/.zed/settings.json", r#"{}"#.into()) + .await; + deterministic.run_until_parked(); + cx_b.read(|cx| { + let store = cx.global::(); + assert_eq!( + store.local_settings(worktree_b.id()).collect::>(), + &[ + (Path::new("").into(), r#"{}"#.to_string()), + (Path::new("a").into(), r#"{"tab_size":8}"#.to_string()), + ] + ) + }); + + // As client A, create and remove some settings files. As client B, see the changed settings. + client_a + .fs + .remove_file("/dir/.zed/settings.json".as_ref(), Default::default()) + .await + .unwrap(); + client_a + .fs + .create_dir("/dir/b/.zed".as_ref()) + .await + .unwrap(); + client_a + .fs + .insert_file("/dir/b/.zed/settings.json", r#"{"tab_size": 4}"#.into()) + .await; + deterministic.run_until_parked(); + cx_b.read(|cx| { + let store = cx.global::(); + assert_eq!( + store.local_settings(worktree_b.id()).collect::>(), + &[ + (Path::new("a").into(), r#"{"tab_size":8}"#.to_string()), + (Path::new("b").into(), r#"{"tab_size":4}"#.to_string()), + ] + ) + }); + + // As client B, disconnect. + server.forbid_connections(); + server.disconnect_client(client_b.peer_id().unwrap()); + + // As client A, change and remove settings files while client B is disconnected. + client_a + .fs + .insert_file("/dir/a/.zed/settings.json", r#"{"hard_tabs":true}"#.into()) + .await; + client_a + .fs + .remove_file("/dir/b/.zed/settings.json".as_ref(), Default::default()) + .await + .unwrap(); + deterministic.run_until_parked(); + + // As client B, reconnect and see the changed settings. + server.allow_connections(); + deterministic.advance_clock(RECEIVE_TIMEOUT); + cx_b.read(|cx| { + let store = cx.global::(); + assert_eq!( + store.local_settings(worktree_b.id()).collect::>(), + &[(Path::new("a").into(), r#"{"hard_tabs":true}"#.to_string()),] + ) + }); +} + #[gpui::test(iterations = 10)] async fn test_buffer_conflict_after_save( deterministic: Arc, diff --git a/crates/copilot/src/copilot.rs b/crates/copilot/src/copilot.rs index de9104a684..7834603552 100644 --- a/crates/copilot/src/copilot.rs +++ b/crates/copilot/src/copilot.rs @@ -318,7 +318,7 @@ impl Copilot { fn enable_or_disable_copilot(&mut self, cx: &mut ModelContext) { let http = self.http.clone(); let node_runtime = self.node_runtime.clone(); - if all_language_settings(cx).copilot_enabled(None, None) { + if all_language_settings(None, cx).copilot_enabled(None, None) { if matches!(self.server, CopilotServer::Disabled) { let start_task = cx .spawn({ @@ -785,10 +785,7 @@ impl Copilot { let buffer = buffer.read(cx); let uri = registered_buffer.uri.clone(); let position = position.to_point_utf16(buffer); - let settings = language_settings( - buffer.language_at(position).map(|l| l.name()).as_deref(), - cx, - ); + let settings = language_settings(buffer.language_at(position).as_ref(), buffer.file(), cx); let tab_size = settings.tab_size; let hard_tabs = settings.hard_tabs; let relative_path = buffer @@ -1175,6 +1172,10 @@ mod tests { fn to_proto(&self) -> rpc::proto::File { unimplemented!() } + + fn worktree_id(&self) -> usize { + 0 + } } impl language::LocalFile for File { diff --git a/crates/copilot_button/src/copilot_button.rs b/crates/copilot_button/src/copilot_button.rs index 17d27ca41f..e34fddd9b9 100644 --- a/crates/copilot_button/src/copilot_button.rs +++ b/crates/copilot_button/src/copilot_button.rs @@ -9,7 +9,10 @@ use gpui::{ AnyElement, AppContext, AsyncAppContext, Element, Entity, MouseState, Subscription, View, ViewContext, ViewHandle, WeakViewHandle, WindowContext, }; -use language::language_settings::{self, all_language_settings, AllLanguageSettings}; +use language::{ + language_settings::{self, all_language_settings, AllLanguageSettings}, + File, Language, +}; use settings::{update_settings_file, SettingsStore}; use std::{path::Path, sync::Arc}; use util::{paths, ResultExt}; @@ -26,8 +29,8 @@ pub struct CopilotButton { popup_menu: ViewHandle, editor_subscription: Option<(Subscription, usize)>, editor_enabled: Option, - language: Option>, - path: Option>, + language: Option>, + file: Option>, fs: Arc, } @@ -41,7 +44,7 @@ impl View for CopilotButton { } fn render(&mut self, cx: &mut ViewContext) -> AnyElement { - let all_language_settings = &all_language_settings(cx); + let all_language_settings = all_language_settings(None, cx); if !all_language_settings.copilot.feature_enabled { return Empty::new().into_any(); } @@ -165,7 +168,7 @@ impl CopilotButton { editor_subscription: None, editor_enabled: None, language: None, - path: None, + file: None, fs, } } @@ -197,14 +200,13 @@ impl CopilotButton { if let Some(language) = self.language.clone() { let fs = fs.clone(); - let language_enabled = - language_settings::language_settings(Some(language.as_ref()), cx) - .show_copilot_suggestions; + let language_enabled = language_settings::language_settings(Some(&language), None, cx) + .show_copilot_suggestions; menu_options.push(ContextMenuItem::handler( format!( "{} Suggestions for {}", if language_enabled { "Hide" } else { "Show" }, - language + language.name() ), move |cx| toggle_copilot_for_language(language.clone(), fs.clone(), cx), )); @@ -212,9 +214,9 @@ impl CopilotButton { let settings = settings::get::(cx); - if let Some(path) = self.path.as_ref() { - let path_enabled = settings.copilot_enabled_for_path(path); - let path = path.clone(); + if let Some(file) = &self.file { + let path = file.path().clone(); + let path_enabled = settings.copilot_enabled_for_path(&path); menu_options.push(ContextMenuItem::handler( format!( "{} Suggestions for This Path", @@ -276,17 +278,15 @@ impl CopilotButton { let editor = editor.read(cx); let snapshot = editor.buffer().read(cx).snapshot(cx); let suggestion_anchor = editor.selections.newest_anchor().start; - let language_name = snapshot - .language_at(suggestion_anchor) - .map(|language| language.name()); - let path = snapshot.file_at(suggestion_anchor).map(|file| file.path()); + let language = snapshot.language_at(suggestion_anchor); + let file = snapshot.file_at(suggestion_anchor).cloned(); self.editor_enabled = Some( - all_language_settings(cx) - .copilot_enabled(language_name.as_deref(), path.map(|p| p.as_ref())), + all_language_settings(self.file.as_ref(), cx) + .copilot_enabled(language, file.as_ref().map(|file| file.path().as_ref())), ); - self.language = language_name; - self.path = path.cloned(); + self.language = language.cloned(); + self.file = file; cx.notify() } @@ -363,17 +363,18 @@ async fn configure_disabled_globs( } fn toggle_copilot_globally(fs: Arc, cx: &mut AppContext) { - let show_copilot_suggestions = all_language_settings(cx).copilot_enabled(None, None); + let show_copilot_suggestions = all_language_settings(None, cx).copilot_enabled(None, None); update_settings_file::(fs, cx, move |file| { file.defaults.show_copilot_suggestions = Some((!show_copilot_suggestions).into()) }); } -fn toggle_copilot_for_language(language: Arc, fs: Arc, cx: &mut AppContext) { - let show_copilot_suggestions = all_language_settings(cx).copilot_enabled(Some(&language), None); +fn toggle_copilot_for_language(language: Arc, fs: Arc, cx: &mut AppContext) { + let show_copilot_suggestions = + all_language_settings(None, cx).copilot_enabled(Some(&language), None); update_settings_file::(fs, cx, move |file| { file.languages - .entry(language) + .entry(language.name()) .or_default() .show_copilot_suggestions = Some(!show_copilot_suggestions); }); diff --git a/crates/editor/src/display_map.rs b/crates/editor/src/display_map.rs index 366e47ddc6..b0483db68d 100644 --- a/crates/editor/src/display_map.rs +++ b/crates/editor/src/display_map.rs @@ -272,12 +272,11 @@ impl DisplayMap { } fn tab_size(buffer: &ModelHandle, cx: &mut ModelContext) -> NonZeroU32 { - let language_name = buffer + let language = buffer .read(cx) .as_singleton() - .and_then(|buffer| buffer.read(cx).language()) - .map(|language| language.name()); - language_settings(language_name.as_deref(), cx).tab_size + .and_then(|buffer| buffer.read(cx).language()); + language_settings(language.as_deref(), None, cx).tab_size } #[cfg(test)] diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 16fce156a4..a3601f3c58 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -31,7 +31,9 @@ use copilot::Copilot; pub use display_map::DisplayPoint; use display_map::*; pub use editor_settings::EditorSettings; -pub use element::*; +pub use element::{ + Cursor, EditorElement, HighlightedRange, HighlightedRangeLine, LineWithInvisibles, +}; use futures::FutureExt; use fuzzy::{StringMatch, StringMatchCandidate}; use gpui::{ @@ -3214,12 +3216,10 @@ impl Editor { snapshot: &MultiBufferSnapshot, cx: &mut ViewContext, ) -> bool { - let path = snapshot.file_at(location).map(|file| file.path().as_ref()); - let language_name = snapshot - .language_at(location) - .map(|language| language.name()); - let settings = all_language_settings(cx); - settings.copilot_enabled(language_name.as_deref(), path) + let file = snapshot.file_at(location); + let language = snapshot.language_at(location); + let settings = all_language_settings(file, cx); + settings.copilot_enabled(language, file.map(|f| f.path().as_ref())) } fn has_active_copilot_suggestion(&self, cx: &AppContext) -> bool { @@ -7102,11 +7102,13 @@ impl Editor { }; // If None, we are in a file without an extension - let file_extension = file_extension.or(self + let file = self .buffer .read(cx) .as_singleton() - .and_then(|b| b.read(cx).file()) + .and_then(|b| b.read(cx).file()); + let file_extension = file_extension.or(file + .as_ref() .and_then(|file| Path::new(file.file_name(cx)).extension()) .and_then(|e| e.to_str()) .map(|a| a.to_string())); @@ -7117,7 +7119,7 @@ impl Editor { .get("vim_mode") == Some(&serde_json::Value::Bool(true)); let telemetry_settings = *settings::get::(cx); - let copilot_enabled = all_language_settings(cx).copilot_enabled(None, None); + let copilot_enabled = all_language_settings(file, cx).copilot_enabled(None, None); let copilot_enabled_for_language = self .buffer .read(cx) diff --git a/crates/editor/src/items.rs b/crates/editor/src/items.rs index 40e7c89cb2..8da746075e 100644 --- a/crates/editor/src/items.rs +++ b/crates/editor/src/items.rs @@ -1231,6 +1231,10 @@ mod tests { unimplemented!() } + fn worktree_id(&self) -> usize { + 0 + } + fn is_deleted(&self) -> bool { unimplemented!() } diff --git a/crates/editor/src/multi_buffer.rs b/crates/editor/src/multi_buffer.rs index 4650dff38f..b7b2b89c8c 100644 --- a/crates/editor/src/multi_buffer.rs +++ b/crates/editor/src/multi_buffer.rs @@ -1377,8 +1377,14 @@ impl MultiBuffer { point: T, cx: &'a AppContext, ) -> &'a LanguageSettings { - let language = self.language_at(point, cx); - language_settings(language.map(|l| l.name()).as_deref(), cx) + let mut language = None; + let mut file = None; + if let Some((buffer, offset)) = self.point_to_buffer_offset(point, cx) { + let buffer = buffer.read(cx); + language = buffer.language_at(offset); + file = buffer.file(); + } + language_settings(language.as_ref(), file, cx) } pub fn for_each_buffer(&self, mut f: impl FnMut(&ModelHandle)) { @@ -2785,9 +2791,13 @@ impl MultiBufferSnapshot { point: T, cx: &'a AppContext, ) -> &'a LanguageSettings { - self.point_to_buffer_offset(point) - .map(|(buffer, offset)| buffer.settings_at(offset, cx)) - .unwrap_or_else(|| language_settings(None, cx)) + let mut language = None; + let mut file = None; + if let Some((buffer, offset)) = self.point_to_buffer_offset(point) { + language = buffer.language_at(offset); + file = buffer.file(); + } + language_settings(language, file, cx) } pub fn language_scope_at<'a, T: ToOffset>(&'a self, point: T) -> Option { diff --git a/crates/feedback/Cargo.toml b/crates/feedback/Cargo.toml index cd35afbda8..07b6ad790c 100644 --- a/crates/feedback/Cargo.toml +++ b/crates/feedback/Cargo.toml @@ -16,6 +16,7 @@ editor = { path = "../editor" } language = { path = "../language" } gpui = { path = "../gpui" } project = { path = "../project" } +regex.workspace = true search = { path = "../search" } settings = { path = "../settings" } theme = { path = "../theme" } diff --git a/crates/feedback/src/feedback_editor.rs b/crates/feedback/src/feedback_editor.rs index d5d20b069a..5a4f912e3a 100644 --- a/crates/feedback/src/feedback_editor.rs +++ b/crates/feedback/src/feedback_editor.rs @@ -14,6 +14,7 @@ use isahc::Request; use language::Buffer; use postage::prelude::Stream; use project::Project; +use regex::Regex; use serde::Serialize; use smallvec::SmallVec; use std::{ @@ -46,6 +47,7 @@ pub fn init(cx: &mut AppContext) { #[derive(Serialize)] struct FeedbackRequestBody<'a> { feedback_text: &'a str, + email: Option, metrics_id: Option>, installation_id: Option>, system_specs: SystemSpecs, @@ -157,8 +159,18 @@ impl FeedbackEditor { let is_staff = telemetry.is_staff(); let http_client = zed_client.http_client(); + let re = Regex::new(r"\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b").unwrap(); + + let emails: Vec<&str> = re + .captures_iter(feedback_text) + .map(|capture| capture.get(0).unwrap().as_str()) + .collect(); + + let email = emails.first().map(|e| e.to_string()); + let request = FeedbackRequestBody { feedback_text: &feedback_text, + email, metrics_id, installation_id, system_specs, diff --git a/crates/feedback/src/feedback_info_text.rs b/crates/feedback/src/feedback_info_text.rs index 5852cd9a61..6c55b7a713 100644 --- a/crates/feedback/src/feedback_info_text.rs +++ b/crates/feedback/src/feedback_info_text.rs @@ -34,7 +34,7 @@ impl View for FeedbackInfoText { Flex::row() .with_child( Text::new( - "We read whatever you submit here. For issues and discussions, visit the ", + "Share your feedback. Include your email for replies. For issues and discussions, visit the ", theme.feedback.info_text_default.text.clone(), ) .with_soft_wrap(false) @@ -60,7 +60,7 @@ impl View for FeedbackInfoText { }), ) .with_child( - Text::new(" on GitHub.", theme.feedback.info_text_default.text.clone()) + Text::new(".", theme.feedback.info_text_default.text.clone()) .with_soft_wrap(false) .aligned(), ) diff --git a/crates/gpui/src/elements/uniform_list.rs b/crates/gpui/src/elements/uniform_list.rs index 9ccd57b291..8344914da0 100644 --- a/crates/gpui/src/elements/uniform_list.rs +++ b/crates/gpui/src/elements/uniform_list.rs @@ -36,7 +36,7 @@ struct StateInner { scroll_to: Option, } -pub struct LayoutState { +pub struct UniformListLayoutState { scroll_max: f32, item_height: f32, items: Vec>, @@ -152,7 +152,7 @@ impl UniformList { } impl Element for UniformList { - type LayoutState = LayoutState; + type LayoutState = UniformListLayoutState; type PaintState = (); fn layout( @@ -169,7 +169,7 @@ impl Element for UniformList { let no_items = ( constraint.min, - LayoutState { + UniformListLayoutState { item_height: 0., scroll_max: 0., items: Default::default(), @@ -263,7 +263,7 @@ impl Element for UniformList { ( size, - LayoutState { + UniformListLayoutState { item_height, scroll_max, items, diff --git a/crates/gpui/src/font_cache.rs b/crates/gpui/src/font_cache.rs index 4388ad4bcb..57dad48e34 100644 --- a/crates/gpui/src/font_cache.rs +++ b/crates/gpui/src/font_cache.rs @@ -25,8 +25,9 @@ struct Family { pub struct FontCache(RwLock); pub struct FontCacheState { - fonts: Arc, + font_system: Arc, families: Vec, + default_family: Option, font_selections: HashMap>, metrics: HashMap, wrapper_pool: HashMap<(FontId, OrderedFloat), Vec>, @@ -42,8 +43,9 @@ unsafe impl Send for FontCache {} impl FontCache { pub fn new(fonts: Arc) -> Self { Self(RwLock::new(FontCacheState { - fonts, + font_system: fonts, families: Default::default(), + default_family: None, font_selections: Default::default(), metrics: Default::default(), wrapper_pool: Default::default(), @@ -73,14 +75,14 @@ impl FontCache { let mut state = RwLockUpgradableReadGuard::upgrade(state); - if let Ok(font_ids) = state.fonts.load_family(name, features) { + if let Ok(font_ids) = state.font_system.load_family(name, features) { if font_ids.is_empty() { continue; } let family_id = FamilyId(state.families.len()); for font_id in &font_ids { - if state.fonts.glyph_for_char(*font_id, 'm').is_none() { + if state.font_system.glyph_for_char(*font_id, 'm').is_none() { return Err(anyhow!("font must contain a glyph for the 'm' character")); } } @@ -99,6 +101,31 @@ impl FontCache { )) } + /// Returns an arbitrary font family that is available on the system. + pub fn known_existing_family(&self) -> FamilyId { + if let Some(family_id) = self.0.read().default_family { + return family_id; + } + + let default_family = self + .load_family( + &["Courier", "Helvetica", "Arial", "Verdana"], + &Default::default(), + ) + .unwrap_or_else(|_| { + let all_family_names = self.0.read().font_system.all_families(); + let all_family_names: Vec<_> = all_family_names + .iter() + .map(|string| string.as_str()) + .collect(); + self.load_family(&all_family_names, &Default::default()) + .expect("could not load any default font family") + }); + + self.0.write().default_family = Some(default_family); + default_family + } + pub fn default_font(&self, family_id: FamilyId) -> FontId { self.select_font(family_id, &Properties::default()).unwrap() } @@ -115,7 +142,7 @@ impl FontCache { let mut inner = RwLockUpgradableReadGuard::upgrade(inner); let family = &inner.families[family_id.0]; let font_id = inner - .fonts + .font_system .select_font(&family.font_ids, properties) .unwrap_or(family.font_ids[0]); @@ -137,7 +164,7 @@ impl FontCache { if let Some(metrics) = state.metrics.get(&font_id) { f(metrics) } else { - let metrics = state.fonts.font_metrics(font_id); + let metrics = state.font_system.font_metrics(font_id); let metric = f(&metrics); let mut state = RwLockUpgradableReadGuard::upgrade(state); state.metrics.insert(font_id, metrics); @@ -157,8 +184,11 @@ impl FontCache { let bounds; { let state = self.0.read(); - glyph_id = state.fonts.glyph_for_char(font_id, 'm').unwrap(); - bounds = state.fonts.typographic_bounds(font_id, glyph_id).unwrap(); + glyph_id = state.font_system.glyph_for_char(font_id, 'm').unwrap(); + bounds = state + .font_system + .typographic_bounds(font_id, glyph_id) + .unwrap(); } bounds.width() * self.em_scale(font_id, font_size) } @@ -168,8 +198,8 @@ impl FontCache { let advance; { let state = self.0.read(); - glyph_id = state.fonts.glyph_for_char(font_id, 'm').unwrap(); - advance = state.fonts.advance(font_id, glyph_id).unwrap(); + glyph_id = state.font_system.glyph_for_char(font_id, 'm').unwrap(); + advance = state.font_system.advance(font_id, glyph_id).unwrap(); } advance.x() * self.em_scale(font_id, font_size) } @@ -214,7 +244,7 @@ impl FontCache { .or_default(); let wrapper = wrappers .pop() - .unwrap_or_else(|| LineWrapper::new(font_id, font_size, state.fonts.clone())); + .unwrap_or_else(|| LineWrapper::new(font_id, font_size, state.font_system.clone())); LineWrapperHandle { wrapper: Some(wrapper), font_cache: self.clone(), diff --git a/crates/gpui/src/fonts.rs b/crates/gpui/src/fonts.rs index e0f037acb8..5e77593d05 100644 --- a/crates/gpui/src/fonts.rs +++ b/crates/gpui/src/fonts.rs @@ -295,13 +295,14 @@ impl Default for TextStyle { .as_ref() .expect("TextStyle::default can only be called within a call to with_font_cache"); - let font_family_name = Arc::from("Courier"); - let font_family_id = font_cache - .load_family(&[&font_family_name], &Default::default()) - .unwrap(); + let font_family_id = font_cache.known_existing_family(); let font_id = font_cache .select_font(font_family_id, &Default::default()) - .unwrap(); + .expect("did not have any font in system-provided family"); + let font_family_name = font_cache + .family_name(font_family_id) + .expect("we loaded this family from the font cache, so this should work"); + Self { color: Default::default(), font_family_name, diff --git a/crates/gpui/src/platform.rs b/crates/gpui/src/platform.rs index 9b4fd7ca51..7fc02b0548 100644 --- a/crates/gpui/src/platform.rs +++ b/crates/gpui/src/platform.rs @@ -343,6 +343,7 @@ pub enum RasterizationOptions { pub trait FontSystem: Send + Sync { fn add_fonts(&self, fonts: &[Arc>]) -> anyhow::Result<()>; + fn all_families(&self) -> Vec; fn load_family(&self, name: &str, features: &FontFeatures) -> anyhow::Result>; fn select_font( &self, diff --git a/crates/gpui/src/platform/mac/fonts.rs b/crates/gpui/src/platform/mac/fonts.rs index 96916698fc..0414cb8a6c 100644 --- a/crates/gpui/src/platform/mac/fonts.rs +++ b/crates/gpui/src/platform/mac/fonts.rs @@ -66,6 +66,14 @@ impl platform::FontSystem for FontSystem { self.0.write().add_fonts(fonts) } + fn all_families(&self) -> Vec { + self.0 + .read() + .system_source + .all_families() + .expect("core text should never return an error") + } + fn load_family(&self, name: &str, features: &Features) -> anyhow::Result> { self.0.write().load_family(name, features) } diff --git a/crates/language/src/buffer.rs b/crates/language/src/buffer.rs index 93b50cf597..7acb36a92f 100644 --- a/crates/language/src/buffer.rs +++ b/crates/language/src/buffer.rs @@ -216,6 +216,11 @@ pub trait File: Send + Sync { /// of its worktree, then this method will return the name of the worktree itself. fn file_name<'a>(&'a self, cx: &'a AppContext) -> &'a OsStr; + /// Returns the id of the worktree to which this file belongs. + /// + /// This is needed for looking up project-specific settings. + fn worktree_id(&self) -> usize; + fn is_deleted(&self) -> bool; fn as_any(&self) -> &dyn Any; @@ -1802,8 +1807,7 @@ impl BufferSnapshot { } pub fn language_indent_size_at(&self, position: T, cx: &AppContext) -> IndentSize { - let language_name = self.language_at(position).map(|language| language.name()); - let settings = language_settings(language_name.as_deref(), cx); + let settings = language_settings(self.language_at(position), self.file(), cx); if settings.hard_tabs { IndentSize::tab() } else { @@ -2127,8 +2131,7 @@ impl BufferSnapshot { position: D, cx: &'a AppContext, ) -> &'a LanguageSettings { - let language = self.language_at(position); - language_settings(language.map(|l| l.name()).as_deref(), cx) + language_settings(self.language_at(position), self.file.as_ref(), cx) } pub fn language_scope_at(&self, position: D) -> Option { diff --git a/crates/language/src/language_settings.rs b/crates/language/src/language_settings.rs index c98297c036..332e789b4d 100644 --- a/crates/language/src/language_settings.rs +++ b/crates/language/src/language_settings.rs @@ -1,3 +1,4 @@ +use crate::{File, Language}; use anyhow::Result; use collections::HashMap; use globset::GlobMatcher; @@ -13,12 +14,21 @@ pub fn init(cx: &mut AppContext) { settings::register::(cx); } -pub fn language_settings<'a>(language: Option<&str>, cx: &'a AppContext) -> &'a LanguageSettings { - settings::get::(cx).language(language) +pub fn language_settings<'a>( + language: Option<&Arc>, + file: Option<&Arc>, + cx: &'a AppContext, +) -> &'a LanguageSettings { + let language_name = language.map(|l| l.name()); + all_language_settings(file, cx).language(language_name.as_deref()) } -pub fn all_language_settings<'a>(cx: &'a AppContext) -> &'a AllLanguageSettings { - settings::get::(cx) +pub fn all_language_settings<'a>( + file: Option<&Arc>, + cx: &'a AppContext, +) -> &'a AllLanguageSettings { + let location = file.map(|f| (f.worktree_id(), f.path().as_ref())); + settings::get_local(location, cx) } #[derive(Debug, Clone)] @@ -155,7 +165,7 @@ impl AllLanguageSettings { .any(|glob| glob.is_match(path)) } - pub fn copilot_enabled(&self, language_name: Option<&str>, path: Option<&Path>) -> bool { + pub fn copilot_enabled(&self, language: Option<&Arc>, path: Option<&Path>) -> bool { if !self.copilot.feature_enabled { return false; } @@ -166,7 +176,8 @@ impl AllLanguageSettings { } } - self.language(language_name).show_copilot_suggestions + self.language(language.map(|l| l.name()).as_deref()) + .show_copilot_suggestions } } diff --git a/crates/project/src/lsp_command.rs b/crates/project/src/lsp_command.rs index ce7004dd31..490f3bde17 100644 --- a/crates/project/src/lsp_command.rs +++ b/crates/project/src/lsp_command.rs @@ -1717,8 +1717,7 @@ impl LspCommand for OnTypeFormatting { .await?; let tab_size = buffer.read_with(&cx, |buffer, cx| { - let language_name = buffer.language().map(|language| language.name()); - language_settings(language_name.as_deref(), cx).tab_size + language_settings(buffer.language(), buffer.file(), cx).tab_size }); Ok(Self { diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index fd522c5061..7ce07959d7 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -28,7 +28,7 @@ use gpui::{ ModelHandle, Task, WeakModelHandle, }; use language::{ - language_settings::{all_language_settings, language_settings, FormatOnSave, Formatter}, + language_settings::{language_settings, FormatOnSave, Formatter}, point_to_lsp, proto::{ deserialize_anchor, deserialize_fingerprint, deserialize_line_ending, deserialize_version, @@ -72,7 +72,10 @@ use std::{ time::{Duration, Instant, SystemTime}, }; use terminals::Terminals; -use util::{debug_panic, defer, merge_json_value_into, post_inc, ResultExt, TryFutureExt as _}; +use util::{ + debug_panic, defer, merge_json_value_into, paths::LOCAL_SETTINGS_RELATIVE_PATH, post_inc, + ResultExt, TryFutureExt as _, +}; pub use fs::*; pub use worktree::*; @@ -460,6 +463,7 @@ impl Project { client.add_model_request_handler(Self::handle_update_buffer); client.add_model_message_handler(Self::handle_update_diagnostic_summary); client.add_model_message_handler(Self::handle_update_worktree); + client.add_model_message_handler(Self::handle_update_worktree_settings); client.add_model_request_handler(Self::handle_create_project_entry); client.add_model_request_handler(Self::handle_rename_project_entry); client.add_model_request_handler(Self::handle_copy_project_entry); @@ -686,42 +690,37 @@ impl Project { } fn on_settings_changed(&mut self, cx: &mut ModelContext) { - let settings = all_language_settings(cx); - let mut language_servers_to_start = Vec::new(); for buffer in self.opened_buffers.values() { if let Some(buffer) = buffer.upgrade(cx) { let buffer = buffer.read(cx); - if let Some((file, language)) = File::from_dyn(buffer.file()).zip(buffer.language()) - { - if settings - .language(Some(&language.name())) - .enable_language_server - { - let worktree = file.worktree.read(cx); - language_servers_to_start.push(( - worktree.id(), - worktree.as_local().unwrap().abs_path().clone(), - language.clone(), - )); + if let Some((file, language)) = buffer.file().zip(buffer.language()) { + let settings = language_settings(Some(language), Some(file), cx); + if settings.enable_language_server { + if let Some(file) = File::from_dyn(Some(file)) { + language_servers_to_start + .push((file.worktree.clone(), language.clone())); + } } } } } let mut language_servers_to_stop = Vec::new(); - for language in self.languages.to_vec() { - for lsp_adapter in language.lsp_adapters() { - if !settings - .language(Some(&language.name())) - .enable_language_server - { - let lsp_name = &lsp_adapter.name; - for (worktree_id, started_lsp_name) in self.language_server_ids.keys() { - if lsp_name == started_lsp_name { - language_servers_to_stop.push((*worktree_id, started_lsp_name.clone())); - } - } + let languages = self.languages.to_vec(); + for (worktree_id, started_lsp_name) in self.language_server_ids.keys() { + let language = languages.iter().find(|l| { + l.lsp_adapters() + .iter() + .any(|adapter| &adapter.name == started_lsp_name) + }); + if let Some(language) = language { + let worktree = self.worktree_for_id(*worktree_id, cx); + let file = worktree.and_then(|tree| { + tree.update(cx, |tree, cx| tree.root_file(cx).map(|f| f as _)) + }); + if !language_settings(Some(language), file.as_ref(), cx).enable_language_server { + language_servers_to_stop.push((*worktree_id, started_lsp_name.clone())); } } } @@ -733,8 +732,9 @@ impl Project { } // Start all the newly-enabled language servers. - for (worktree_id, worktree_path, language) in language_servers_to_start { - self.start_language_servers(worktree_id, worktree_path, language, cx); + for (worktree, language) in language_servers_to_start { + let worktree_path = worktree.read(cx).abs_path(); + self.start_language_servers(&worktree, worktree_path, language, cx); } if !self.copilot_enabled && Copilot::global(cx).is_some() { @@ -1107,6 +1107,21 @@ impl Project { .log_err(); } + let store = cx.global::(); + for worktree in self.worktrees(cx) { + let worktree_id = worktree.read(cx).id().to_proto(); + for (path, content) in store.local_settings(worktree.id()) { + self.client + .send(proto::UpdateWorktreeSettings { + project_id, + worktree_id, + path: path.to_string_lossy().into(), + content: Some(content), + }) + .log_err(); + } + } + let (updates_tx, mut updates_rx) = mpsc::unbounded(); let client = self.client.clone(); self.client_state = Some(ProjectClientState::Local { @@ -1219,6 +1234,14 @@ impl Project { message_id: u32, cx: &mut ModelContext, ) -> Result<()> { + cx.update_global::(|store, cx| { + for worktree in &self.worktrees { + store + .clear_local_settings(worktree.handle_id(), cx) + .log_err(); + } + }); + self.join_project_response_message_id = message_id; self.set_worktrees_from_proto(message.worktrees, cx)?; self.set_collaborators_from_proto(message.collaborators, cx)?; @@ -2321,25 +2344,34 @@ impl Project { }); 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_servers(worktree_id, worktree_abs_path, new_language, cx); + let worktree = file.worktree.clone(); + if let Some(tree) = worktree.read(cx).as_local() { + self.start_language_servers(&worktree, tree.abs_path().clone(), new_language, cx); } } } fn start_language_servers( &mut self, - worktree_id: WorktreeId, + worktree: &ModelHandle, worktree_path: Arc, language: Arc, cx: &mut ModelContext, ) { - if !language_settings(Some(&language.name()), cx).enable_language_server { + if !language_settings( + Some(&language), + worktree + .update(cx, |tree, cx| tree.root_file(cx)) + .map(|f| f as _) + .as_ref(), + cx, + ) + .enable_language_server + { return; } + let worktree_id = worktree.read(cx).id(); for adapter in language.lsp_adapters() { let key = (worktree_id, adapter.name.clone()); if self.language_server_ids.contains_key(&key) { @@ -2748,23 +2780,22 @@ impl Project { buffers: impl IntoIterator>, cx: &mut ModelContext, ) -> Option<()> { - let language_server_lookup_info: HashSet<(WorktreeId, Arc, Arc)> = buffers + let language_server_lookup_info: HashSet<(ModelHandle, Arc)> = buffers .into_iter() .filter_map(|buffer| { let buffer = buffer.read(cx); let file = File::from_dyn(buffer.file())?; - let worktree = file.worktree.read(cx).as_local()?; let full_path = file.full_path(cx); let language = self .languages .language_for_file(&full_path, Some(buffer.as_rope())) .now_or_never()? .ok()?; - Some((worktree.id(), worktree.abs_path().clone(), language)) + Some((file.worktree.clone(), language)) }) .collect(); - for (worktree_id, worktree_abs_path, language) in language_server_lookup_info { - self.restart_language_servers(worktree_id, worktree_abs_path, language, cx); + for (worktree, language) in language_server_lookup_info { + self.restart_language_servers(worktree, language, cx); } None @@ -2773,11 +2804,13 @@ impl Project { // TODO This will break in the case where the adapter's root paths and worktrees are not equal fn restart_language_servers( &mut self, - worktree_id: WorktreeId, - fallback_path: Arc, + worktree: ModelHandle, language: Arc, cx: &mut ModelContext, ) { + let worktree_id = worktree.read(cx).id(); + let fallback_path = worktree.read(cx).abs_path(); + let mut stops = Vec::new(); for adapter in language.lsp_adapters() { stops.push(self.stop_language_server(worktree_id, adapter.name.clone(), cx)); @@ -2807,7 +2840,7 @@ impl Project { .map(|path_buf| Arc::from(path_buf.as_path())) .unwrap_or(fallback_path); - this.start_language_servers(worktree_id, root_path, language.clone(), cx); + this.start_language_servers(&worktree, root_path, language.clone(), cx); // Lookup new server ids and set them for each of the orphaned worktrees for adapter in language.lsp_adapters() { @@ -3432,8 +3465,7 @@ impl Project { let mut project_transaction = ProjectTransaction::default(); for (buffer, buffer_abs_path, language_server) in &buffers_with_paths_and_servers { let settings = buffer.read_with(&cx, |buffer, cx| { - let language_name = buffer.language().map(|language| language.name()); - language_settings(language_name.as_deref(), cx).clone() + language_settings(buffer.language(), buffer.file(), cx).clone() }); let remove_trailing_whitespace = settings.remove_trailing_whitespace_on_save; @@ -4463,11 +4495,14 @@ impl Project { push_to_history: bool, cx: &mut ModelContext, ) -> Task>> { - let tab_size = buffer.read_with(cx, |buffer, cx| { - let language_name = buffer.language().map(|language| language.name()); - language_settings(language_name.as_deref(), cx).tab_size + let (position, tab_size) = buffer.read_with(cx, |buffer, cx| { + let position = position.to_point_utf16(buffer); + ( + position, + language_settings(buffer.language_at(position).as_ref(), buffer.file(), cx) + .tab_size, + ) }); - let position = position.to_point_utf16(buffer.read(cx)); self.request_lsp( buffer.clone(), OnTypeFormatting { @@ -4873,6 +4908,7 @@ impl Project { worktree::Event::UpdatedEntries(changes) => { this.update_local_worktree_buffers(&worktree, changes, cx); this.update_local_worktree_language_servers(&worktree, changes, cx); + this.update_local_worktree_settings(&worktree, changes, cx); } worktree::Event::UpdatedGitRepositories(updated_repos) => { this.update_local_worktree_buffers_git_repos(worktree, updated_repos, cx) @@ -4893,8 +4929,12 @@ impl Project { .push(WorktreeHandle::Weak(worktree.downgrade())); } - cx.observe_release(worktree, |this, worktree, cx| { + let handle_id = worktree.id(); + cx.observe_release(worktree, move |this, worktree, cx| { let _ = this.remove_worktree(worktree.id(), cx); + cx.update_global::(|store, cx| { + store.clear_local_settings(handle_id, cx).log_err() + }); }) .detach(); @@ -5179,6 +5219,71 @@ impl Project { .detach(); } + fn update_local_worktree_settings( + &mut self, + worktree: &ModelHandle, + changes: &UpdatedEntriesSet, + cx: &mut ModelContext, + ) { + let project_id = self.remote_id(); + let worktree_id = worktree.id(); + let worktree = worktree.read(cx).as_local().unwrap(); + let remote_worktree_id = worktree.id(); + + let mut settings_contents = Vec::new(); + for (path, _, change) in changes.iter() { + if path.ends_with(&*LOCAL_SETTINGS_RELATIVE_PATH) { + let settings_dir = Arc::from( + path.ancestors() + .nth(LOCAL_SETTINGS_RELATIVE_PATH.components().count()) + .unwrap(), + ); + let fs = self.fs.clone(); + let removed = *change == PathChange::Removed; + let abs_path = worktree.absolutize(path); + settings_contents.push(async move { + (settings_dir, (!removed).then_some(fs.load(&abs_path).await)) + }); + } + } + + if settings_contents.is_empty() { + return; + } + + let client = self.client.clone(); + cx.spawn_weak(move |_, mut cx| async move { + let settings_contents: Vec<(Arc, _)> = + futures::future::join_all(settings_contents).await; + cx.update(|cx| { + cx.update_global::(|store, cx| { + for (directory, file_content) in settings_contents { + let file_content = file_content.and_then(|content| content.log_err()); + store + .set_local_settings( + worktree_id, + directory.clone(), + file_content.as_ref().map(String::as_str), + cx, + ) + .log_err(); + if let Some(remote_id) = project_id { + client + .send(proto::UpdateWorktreeSettings { + project_id: remote_id, + worktree_id: remote_worktree_id.to_proto(), + path: directory.to_string_lossy().into_owned(), + content: file_content, + }) + .log_err(); + } + } + }); + }); + }) + .detach(); + } + pub fn set_active_path(&mut self, entry: Option, cx: &mut ModelContext) { let new_active_entry = entry.and_then(|project_path| { let worktree = self.worktree_for_id(project_path.worktree_id, cx)?; @@ -5431,6 +5536,30 @@ impl Project { }) } + async fn handle_update_worktree_settings( + this: ModelHandle, + envelope: TypedEnvelope, + _: Arc, + mut cx: AsyncAppContext, + ) -> Result<()> { + this.update(&mut cx, |this, cx| { + let worktree_id = WorktreeId::from_proto(envelope.payload.worktree_id); + if let Some(worktree) = this.worktree_for_id(worktree_id, cx) { + cx.update_global::(|store, cx| { + store + .set_local_settings( + worktree.id(), + PathBuf::from(&envelope.payload.path).into(), + envelope.payload.content.as_ref().map(String::as_str), + cx, + ) + .log_err(); + }); + } + Ok(()) + }) + } + async fn handle_create_project_entry( this: ModelHandle, envelope: TypedEnvelope, @@ -6521,8 +6650,8 @@ impl Project { } self.metadata_changed(cx); - for (id, _) in old_worktrees_by_id { - cx.emit(Event::WorktreeRemoved(id)); + for id in old_worktrees_by_id.keys() { + cx.emit(Event::WorktreeRemoved(*id)); } Ok(()) @@ -6892,6 +7021,13 @@ impl WorktreeHandle { WorktreeHandle::Weak(handle) => handle.upgrade(cx), } } + + pub fn handle_id(&self) -> usize { + match self { + WorktreeHandle::Strong(handle) => handle.id(), + WorktreeHandle::Weak(handle) => handle.id(), + } + } } impl OpenBuffer { diff --git a/crates/project/src/project_tests.rs b/crates/project/src/project_tests.rs index 656fdaf25d..a67e38893b 100644 --- a/crates/project/src/project_tests.rs +++ b/crates/project/src/project_tests.rs @@ -63,6 +63,66 @@ async fn test_symlinks(cx: &mut gpui::TestAppContext) { }); } +#[gpui::test] +async fn test_managing_project_specific_settings( + deterministic: Arc, + cx: &mut gpui::TestAppContext, +) { + init_test(cx); + + let fs = FakeFs::new(cx.background()); + fs.insert_tree( + "/the-root", + json!({ + ".zed": { + "settings.json": r#"{ "tab_size": 8 }"# + }, + "a": { + "a.rs": "fn a() {\n A\n}" + }, + "b": { + ".zed": { + "settings.json": r#"{ "tab_size": 2 }"# + }, + "b.rs": "fn b() {\n B\n}" + } + }), + ) + .await; + + let project = Project::test(fs.clone(), ["/the-root".as_ref()], cx).await; + let worktree = project.read_with(cx, |project, cx| project.worktrees(cx).next().unwrap()); + + deterministic.run_until_parked(); + cx.read(|cx| { + let tree = worktree.read(cx); + + let settings_a = language_settings( + None, + Some( + &(File::for_entry( + tree.entry_for_path("a/a.rs").unwrap().clone(), + worktree.clone(), + ) as _), + ), + cx, + ); + let settings_b = language_settings( + None, + Some( + &(File::for_entry( + tree.entry_for_path("b/b.rs").unwrap().clone(), + worktree.clone(), + ) as _), + ), + cx, + ); + + assert_eq!(settings_a.tab_size.get(), 8); + assert_eq!(settings_b.tab_size.get(), 2); + }); +} + #[gpui::test] async fn test_managing_language_servers( deterministic: Arc, diff --git a/crates/project/src/worktree.rs b/crates/project/src/worktree.rs index dc3c172775..a33a0fc050 100644 --- a/crates/project/src/worktree.rs +++ b/crates/project/src/worktree.rs @@ -677,6 +677,11 @@ impl Worktree { Worktree::Remote(worktree) => worktree.abs_path.clone(), } } + + pub fn root_file(&self, cx: &mut ModelContext) -> Option> { + let entry = self.root_entry()?; + Some(File::for_entry(entry.clone(), cx.handle())) + } } impl LocalWorktree { @@ -684,14 +689,6 @@ impl LocalWorktree { path.starts_with(&self.abs_path) } - fn absolutize(&self, path: &Path) -> PathBuf { - if path.file_name().is_some() { - self.abs_path.join(path) - } else { - self.abs_path.to_path_buf() - } - } - pub(crate) fn load_buffer( &mut self, id: u64, @@ -1544,6 +1541,14 @@ impl Snapshot { &self.abs_path } + pub fn absolutize(&self, path: &Path) -> PathBuf { + if path.file_name().is_some() { + self.abs_path.join(path) + } else { + self.abs_path.to_path_buf() + } + } + pub fn contains_entry(&self, entry_id: ProjectEntryId) -> bool { self.entries_by_id.get(&entry_id, &()).is_some() } @@ -2383,6 +2388,10 @@ impl language::File for File { .unwrap_or_else(|| OsStr::new(&self.worktree.read(cx).root_name)) } + fn worktree_id(&self) -> usize { + self.worktree.id() + } + fn is_deleted(&self) -> bool { self.is_deleted } @@ -2447,6 +2456,17 @@ impl language::LocalFile for File { } impl File { + pub fn for_entry(entry: Entry, worktree: ModelHandle) -> Arc { + Arc::new(Self { + worktree, + path: entry.path.clone(), + mtime: entry.mtime, + entry_id: entry.id, + is_local: true, + is_deleted: false, + }) + } + pub fn from_proto( proto: rpc::proto::File, worktree: ModelHandle, @@ -2507,7 +2527,7 @@ pub enum EntryKind { File(CharBag), } -#[derive(Clone, Copy, Debug)] +#[derive(Clone, Copy, Debug, PartialEq)] pub enum PathChange { /// A filesystem entry was was created. Added, diff --git a/crates/rpc/proto/zed.proto b/crates/rpc/proto/zed.proto index 848cc1c2fa..8351cdfd2d 100644 --- a/crates/rpc/proto/zed.proto +++ b/crates/rpc/proto/zed.proto @@ -132,6 +132,8 @@ message Envelope { OnTypeFormatting on_type_formatting = 111; OnTypeFormattingResponse on_type_formatting_response = 112; + + UpdateWorktreeSettings update_worktree_settings = 113; } } @@ -339,6 +341,13 @@ message UpdateWorktree { string abs_path = 10; } +message UpdateWorktreeSettings { + uint64 project_id = 1; + uint64 worktree_id = 2; + string path = 3; + optional string content = 4; +} + message CreateProjectEntry { uint64 project_id = 1; uint64 worktree_id = 2; diff --git a/crates/rpc/src/proto.rs b/crates/rpc/src/proto.rs index 07925a0486..13794ea64d 100644 --- a/crates/rpc/src/proto.rs +++ b/crates/rpc/src/proto.rs @@ -236,6 +236,7 @@ messages!( (UpdateProject, Foreground), (UpdateProjectCollaborator, Foreground), (UpdateWorktree, Foreground), + (UpdateWorktreeSettings, Foreground), (UpdateDiffBase, Foreground), (GetPrivateUserInfo, Foreground), (GetPrivateUserInfoResponse, Foreground), @@ -345,6 +346,7 @@ entity_messages!( UpdateProject, UpdateProjectCollaborator, UpdateWorktree, + UpdateWorktreeSettings, UpdateDiffBase ); diff --git a/crates/rpc/src/rpc.rs b/crates/rpc/src/rpc.rs index b929de9596..bef6efa529 100644 --- a/crates/rpc/src/rpc.rs +++ b/crates/rpc/src/rpc.rs @@ -6,4 +6,4 @@ pub use conn::Connection; pub use peer::*; mod macros; -pub const PROTOCOL_VERSION: u32 = 56; +pub const PROTOCOL_VERSION: u32 = 57; diff --git a/crates/settings/src/settings_file.rs b/crates/settings/src/settings_file.rs index cca2909da2..3505330eda 100644 --- a/crates/settings/src/settings_file.rs +++ b/crates/settings/src/settings_file.rs @@ -4,7 +4,14 @@ use assets::Assets; use fs::Fs; use futures::{channel::mpsc, StreamExt}; use gpui::{executor::Background, AppContext, AssetSource}; -use std::{borrow::Cow, io::ErrorKind, path::PathBuf, str, sync::Arc, time::Duration}; +use std::{ + borrow::Cow, + io::ErrorKind, + path::{Path, PathBuf}, + str, + sync::Arc, + time::Duration, +}; use util::{paths, ResultExt}; pub fn register(cx: &mut AppContext) { @@ -17,6 +24,10 @@ pub fn get<'a, T: Setting>(cx: &'a AppContext) -> &'a T { cx.global::().get(None) } +pub fn get_local<'a, T: Setting>(location: Option<(usize, &Path)>, cx: &'a AppContext) -> &'a T { + cx.global::().get(location) +} + pub fn default_settings() -> Cow<'static, str> { match Assets.load(DEFAULT_SETTINGS_ASSET_PATH).unwrap() { Cow::Borrowed(s) => Cow::Borrowed(str::from_utf8(s).unwrap()), @@ -55,15 +66,22 @@ pub fn watch_config_file( .spawn(async move { let events = fs.watch(&path, Duration::from_millis(100)).await; futures::pin_mut!(events); + + let contents = fs.load(&path).await.unwrap_or_default(); + if tx.unbounded_send(contents).is_err() { + return; + } + loop { + if events.next().await.is_none() { + break; + } + if let Ok(contents) = fs.load(&path).await { if !tx.unbounded_send(contents).is_ok() { break; } } - if events.next().await.is_none() { - break; - } } }) .detach(); diff --git a/crates/settings/src/settings_store.rs b/crates/settings/src/settings_store.rs index 71b3cc635f..3fdb81dd45 100644 --- a/crates/settings/src/settings_store.rs +++ b/crates/settings/src/settings_store.rs @@ -1,4 +1,4 @@ -use anyhow::Result; +use anyhow::{anyhow, Result}; use collections::{btree_map, hash_map, BTreeMap, HashMap}; use gpui::AppContext; use lazy_static::lazy_static; @@ -84,19 +84,30 @@ pub struct SettingsJsonSchemaParams<'a> { } /// A set of strongly-typed setting values defined via multiple JSON files. -#[derive(Default)] pub struct SettingsStore { setting_values: HashMap>, - default_deserialized_settings: Option, - user_deserialized_settings: Option, - local_deserialized_settings: BTreeMap, serde_json::Value>, + default_deserialized_settings: serde_json::Value, + user_deserialized_settings: serde_json::Value, + local_deserialized_settings: BTreeMap<(usize, Arc), serde_json::Value>, tab_size_callback: Option<(TypeId, Box Option>)>, } +impl Default for SettingsStore { + fn default() -> Self { + SettingsStore { + setting_values: Default::default(), + default_deserialized_settings: serde_json::json!({}), + user_deserialized_settings: serde_json::json!({}), + local_deserialized_settings: Default::default(), + tab_size_callback: Default::default(), + } + } +} + #[derive(Debug)] struct SettingValue { global_value: Option, - local_values: Vec<(Arc, T)>, + local_values: Vec<(usize, Arc, T)>, } trait AnySettingValue { @@ -109,9 +120,9 @@ trait AnySettingValue { custom: &[DeserializedSetting], cx: &AppContext, ) -> Result>; - fn value_for_path(&self, path: Option<&Path>) -> &dyn Any; + fn value_for_path(&self, path: Option<(usize, &Path)>) -> &dyn Any; fn set_global_value(&mut self, value: Box); - fn set_local_value(&mut self, path: Arc, value: Box); + fn set_local_value(&mut self, root_id: usize, path: Arc, value: Box); fn json_schema( &self, generator: &mut SchemaGenerator, @@ -136,27 +147,24 @@ impl SettingsStore { local_values: Vec::new(), })); - if let Some(default_settings) = &self.default_deserialized_settings { - if let Some(default_settings) = setting_value - .deserialize_setting(default_settings) + if let Some(default_settings) = setting_value + .deserialize_setting(&self.default_deserialized_settings) + .log_err() + { + let mut user_values_stack = Vec::new(); + + if let Some(user_settings) = setting_value + .deserialize_setting(&self.user_deserialized_settings) .log_err() { - let mut user_values_stack = Vec::new(); + user_values_stack = vec![user_settings]; + } - if let Some(user_settings) = &self.user_deserialized_settings { - if let Some(user_settings) = - setting_value.deserialize_setting(user_settings).log_err() - { - user_values_stack = vec![user_settings]; - } - } - - if let Some(setting) = setting_value - .load_setting(&default_settings, &user_values_stack, cx) - .log_err() - { - setting_value.set_global_value(setting); - } + if let Some(setting) = setting_value + .load_setting(&default_settings, &user_values_stack, cx) + .log_err() + { + setting_value.set_global_value(setting); } } } @@ -165,7 +173,7 @@ impl SettingsStore { /// /// Panics if the given setting type has not been registered, or if there is no /// value for this setting. - pub fn get(&self, path: Option<&Path>) -> &T { + pub fn get(&self, path: Option<(usize, &Path)>) -> &T { self.setting_values .get(&TypeId::of::()) .unwrap_or_else(|| panic!("unregistered setting type {}", type_name::())) @@ -189,9 +197,7 @@ impl SettingsStore { /// This is only for debugging and reporting. For user-facing functionality, /// use the typed setting interface. pub fn untyped_user_settings(&self) -> &serde_json::Value { - self.user_deserialized_settings - .as_ref() - .unwrap_or(&serde_json::Value::Null) + &self.user_deserialized_settings } #[cfg(any(test, feature = "test-support"))] @@ -213,11 +219,7 @@ impl SettingsStore { cx: &AppContext, update: impl FnOnce(&mut T::FileContent), ) { - if self.user_deserialized_settings.is_none() { - self.set_user_settings("{}", cx).unwrap(); - } - let old_text = - serde_json::to_string(self.user_deserialized_settings.as_ref().unwrap()).unwrap(); + let old_text = serde_json::to_string(&self.user_deserialized_settings).unwrap(); let new_text = self.new_text_for_update::(old_text, update); self.set_user_settings(&new_text, cx).unwrap(); } @@ -250,11 +252,7 @@ impl SettingsStore { .setting_values .get(&setting_type_id) .unwrap_or_else(|| panic!("unregistered setting type {}", type_name::())) - .deserialize_setting( - self.user_deserialized_settings - .as_ref() - .expect("no user settings loaded"), - ) + .deserialize_setting(&self.user_deserialized_settings) .unwrap_or_else(|e| { panic!( "could not deserialize setting type {} from user settings: {}", @@ -323,10 +321,14 @@ impl SettingsStore { default_settings_content: &str, cx: &AppContext, ) -> Result<()> { - self.default_deserialized_settings = - Some(parse_json_with_comments(default_settings_content)?); - self.recompute_values(None, cx)?; - Ok(()) + let settings: serde_json::Value = parse_json_with_comments(default_settings_content)?; + if settings.is_object() { + self.default_deserialized_settings = settings; + self.recompute_values(None, cx)?; + Ok(()) + } else { + Err(anyhow!("settings must be an object")) + } } /// Set the user settings via a JSON string. @@ -335,28 +337,49 @@ impl SettingsStore { user_settings_content: &str, cx: &AppContext, ) -> Result<()> { - self.user_deserialized_settings = Some(parse_json_with_comments(user_settings_content)?); - self.recompute_values(None, cx)?; - Ok(()) + let settings: serde_json::Value = parse_json_with_comments(user_settings_content)?; + if settings.is_object() { + self.user_deserialized_settings = settings; + self.recompute_values(None, cx)?; + Ok(()) + } else { + Err(anyhow!("settings must be an object")) + } } /// Add or remove a set of local settings via a JSON string. pub fn set_local_settings( &mut self, + root_id: usize, path: Arc, settings_content: Option<&str>, cx: &AppContext, ) -> Result<()> { if let Some(content) = settings_content { self.local_deserialized_settings - .insert(path.clone(), parse_json_with_comments(content)?); + .insert((root_id, path.clone()), parse_json_with_comments(content)?); } else { - self.local_deserialized_settings.remove(&path); + self.local_deserialized_settings + .remove(&(root_id, path.clone())); } - self.recompute_values(Some(&path), cx)?; + self.recompute_values(Some((root_id, &path)), cx)?; Ok(()) } + /// Add or remove a set of local settings via a JSON string. + pub fn clear_local_settings(&mut self, root_id: usize, cx: &AppContext) -> Result<()> { + self.local_deserialized_settings + .retain(|k, _| k.0 != root_id); + self.recompute_values(Some((root_id, "".as_ref())), cx)?; + Ok(()) + } + + pub fn local_settings(&self, root_id: usize) -> impl '_ + Iterator, String)> { + self.local_deserialized_settings + .range((root_id, Path::new("").into())..(root_id + 1, Path::new("").into())) + .map(|((_, path), content)| (path.clone(), serde_json::to_string(content).unwrap())) + } + pub fn json_schema( &self, schema_params: &SettingsJsonSchemaParams, @@ -436,72 +459,70 @@ impl SettingsStore { fn recompute_values( &mut self, - changed_local_path: Option<&Path>, + changed_local_path: Option<(usize, &Path)>, cx: &AppContext, ) -> Result<()> { // Reload the global and local values for every setting. let mut user_settings_stack = Vec::::new(); - let mut paths_stack = Vec::>::new(); + let mut paths_stack = Vec::>::new(); for setting_value in self.setting_values.values_mut() { - if let Some(default_settings) = &self.default_deserialized_settings { - let default_settings = setting_value.deserialize_setting(default_settings)?; + let default_settings = + setting_value.deserialize_setting(&self.default_deserialized_settings)?; - user_settings_stack.clear(); - paths_stack.clear(); + user_settings_stack.clear(); + paths_stack.clear(); - if let Some(user_settings) = &self.user_deserialized_settings { - if let Some(user_settings) = - setting_value.deserialize_setting(user_settings).log_err() - { - user_settings_stack.push(user_settings); - paths_stack.push(None); + if let Some(user_settings) = setting_value + .deserialize_setting(&self.user_deserialized_settings) + .log_err() + { + user_settings_stack.push(user_settings); + paths_stack.push(None); + } + + // If the global settings file changed, reload the global value for the field. + if changed_local_path.is_none() { + if let Some(value) = setting_value + .load_setting(&default_settings, &user_settings_stack, cx) + .log_err() + { + setting_value.set_global_value(value); + } + } + + // Reload the local values for the setting. + for ((root_id, path), local_settings) in &self.local_deserialized_settings { + // Build a stack of all of the local values for that setting. + while let Some(prev_entry) = paths_stack.last() { + if let Some((prev_root_id, prev_path)) = prev_entry { + if root_id != prev_root_id || !path.starts_with(prev_path) { + paths_stack.pop(); + user_settings_stack.pop(); + continue; + } } + break; } - // If the global settings file changed, reload the global value for the field. - if changed_local_path.is_none() { + if let Some(local_settings) = + setting_value.deserialize_setting(&local_settings).log_err() + { + paths_stack.push(Some((*root_id, path.as_ref()))); + user_settings_stack.push(local_settings); + + // If a local settings file changed, then avoid recomputing local + // settings for any path outside of that directory. + if changed_local_path.map_or(false, |(changed_root_id, changed_local_path)| { + *root_id != changed_root_id || !path.starts_with(changed_local_path) + }) { + continue; + } + if let Some(value) = setting_value .load_setting(&default_settings, &user_settings_stack, cx) .log_err() { - setting_value.set_global_value(value); - } - } - - // Reload the local values for the setting. - for (path, local_settings) in &self.local_deserialized_settings { - // Build a stack of all of the local values for that setting. - while let Some(prev_path) = paths_stack.last() { - if let Some(prev_path) = prev_path { - if !path.starts_with(prev_path) { - paths_stack.pop(); - user_settings_stack.pop(); - continue; - } - } - break; - } - - if let Some(local_settings) = - setting_value.deserialize_setting(&local_settings).log_err() - { - paths_stack.push(Some(path.as_ref())); - user_settings_stack.push(local_settings); - - // If a local settings file changed, then avoid recomputing local - // settings for any path outside of that directory. - if changed_local_path.map_or(false, |changed_local_path| { - !path.starts_with(changed_local_path) - }) { - continue; - } - - if let Some(value) = setting_value - .load_setting(&default_settings, &user_settings_stack, cx) - .log_err() - { - setting_value.set_local_value(path.clone(), value); - } + setting_value.set_local_value(*root_id, path.clone(), value); } } } @@ -510,6 +531,24 @@ impl SettingsStore { } } +impl Debug for SettingsStore { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("SettingsStore") + .field( + "types", + &self + .setting_values + .values() + .map(|value| value.setting_type_name()) + .collect::>(), + ) + .field("default_settings", &self.default_deserialized_settings) + .field("user_settings", &self.user_deserialized_settings) + .field("local_settings", &self.local_deserialized_settings) + .finish_non_exhaustive() + } +} + impl AnySettingValue for SettingValue { fn key(&self) -> Option<&'static str> { T::KEY @@ -546,10 +585,10 @@ impl AnySettingValue for SettingValue { Ok(DeserializedSetting(Box::new(value))) } - fn value_for_path(&self, path: Option<&Path>) -> &dyn Any { - if let Some(path) = path { - for (settings_path, value) in self.local_values.iter().rev() { - if path.starts_with(&settings_path) { + fn value_for_path(&self, path: Option<(usize, &Path)>) -> &dyn Any { + if let Some((root_id, path)) = path { + for (settings_root_id, settings_path, value) in self.local_values.iter().rev() { + if root_id == *settings_root_id && path.starts_with(&settings_path) { return value; } } @@ -563,11 +602,14 @@ impl AnySettingValue for SettingValue { self.global_value = Some(*value.downcast().unwrap()); } - fn set_local_value(&mut self, path: Arc, value: Box) { + fn set_local_value(&mut self, root_id: usize, path: Arc, value: Box) { let value = *value.downcast().unwrap(); - match self.local_values.binary_search_by_key(&&path, |e| &e.0) { - Ok(ix) => self.local_values[ix].1 = value, - Err(ix) => self.local_values.insert(ix, (path, value)), + match self + .local_values + .binary_search_by_key(&(root_id, &path), |e| (e.0, &e.1)) + { + Ok(ix) => self.local_values[ix].2 = value, + Err(ix) => self.local_values.insert(ix, (root_id, path, value)), } } @@ -884,6 +926,7 @@ mod tests { store .set_local_settings( + 1, Path::new("/root1").into(), Some(r#"{ "user": { "staff": true } }"#), cx, @@ -891,6 +934,7 @@ mod tests { .unwrap(); store .set_local_settings( + 1, Path::new("/root1/subdir").into(), Some(r#"{ "user": { "name": "Jane Doe" } }"#), cx, @@ -899,6 +943,7 @@ mod tests { store .set_local_settings( + 1, Path::new("/root2").into(), Some(r#"{ "user": { "age": 42 }, "key2": "b" }"#), cx, @@ -906,7 +951,7 @@ mod tests { .unwrap(); assert_eq!( - store.get::(Some(Path::new("/root1/something"))), + store.get::(Some((1, Path::new("/root1/something")))), &UserSettings { name: "John Doe".to_string(), age: 31, @@ -914,7 +959,7 @@ mod tests { } ); assert_eq!( - store.get::(Some(Path::new("/root1/subdir/something"))), + store.get::(Some((1, Path::new("/root1/subdir/something")))), &UserSettings { name: "Jane Doe".to_string(), age: 31, @@ -922,7 +967,7 @@ mod tests { } ); assert_eq!( - store.get::(Some(Path::new("/root2/something"))), + store.get::(Some((1, Path::new("/root2/something")))), &UserSettings { name: "John Doe".to_string(), age: 42, @@ -930,7 +975,7 @@ mod tests { } ); assert_eq!( - store.get::(Some(Path::new("/root2/something"))), + store.get::(Some((1, Path::new("/root2/something")))), &MultiKeySettings { key1: "a".to_string(), key2: "b".to_string(), diff --git a/crates/terminal_view/src/terminal_panel.rs b/crates/terminal_view/src/terminal_panel.rs index d34a261bfe..ac3875af9e 100644 --- a/crates/terminal_view/src/terminal_panel.rs +++ b/crates/terminal_view/src/terminal_panel.rs @@ -22,7 +22,7 @@ const TERMINAL_PANEL_KEY: &'static str = "TerminalPanel"; actions!(terminal_panel, [ToggleFocus]); pub fn init(cx: &mut AppContext) { - cx.add_action(TerminalPanel::add_terminal); + cx.add_action(TerminalPanel::new_terminal); } pub enum Event { @@ -70,6 +70,7 @@ impl TerminalPanel { .with_child(Pane::render_tab_bar_button( 0, "icons/plus_12.svg", + false, Some(( "New Terminal".into(), Some(Box::new(workspace::NewTerminal)), @@ -80,7 +81,7 @@ impl TerminalPanel { cx.window_context().defer(move |cx| { if let Some(this) = this.upgrade(cx) { this.update(cx, |this, cx| { - this.add_terminal(&Default::default(), cx); + this.add_terminal(cx); }); } }) @@ -94,6 +95,7 @@ impl TerminalPanel { } else { "icons/maximize_8.svg" }, + pane.is_zoomed(), Some(("Toggle Zoom".into(), Some(Box::new(workspace::ToggleZoom)))), cx, move |pane, cx| pane.toggle_zoom(&Default::default(), cx), @@ -220,7 +222,19 @@ impl TerminalPanel { } } - fn add_terminal(&mut self, _: &workspace::NewTerminal, cx: &mut ViewContext) { + fn new_terminal( + workspace: &mut Workspace, + _: &workspace::NewTerminal, + cx: &mut ViewContext, + ) { + let Some(this) = workspace.focus_panel::(cx) else { + return; + }; + + this.update(cx, |this, cx| this.add_terminal(cx)) + } + + fn add_terminal(&mut self, cx: &mut ViewContext) { let workspace = self.workspace.clone(); cx.spawn(|this, mut cx| async move { let pane = this.read_with(&cx, |this, _| this.pane.clone())?; @@ -363,7 +377,7 @@ impl Panel for TerminalPanel { fn set_active(&mut self, active: bool, cx: &mut ViewContext) { if active && self.pane.read(cx).items_len() == 0 { - self.add_terminal(&Default::default(), cx) + self.add_terminal(cx) } } diff --git a/crates/terminal_view/src/terminal_view.rs b/crates/terminal_view/src/terminal_view.rs index 767e3bf4db..9478b3eef1 100644 --- a/crates/terminal_view/src/terminal_view.rs +++ b/crates/terminal_view/src/terminal_view.rs @@ -38,7 +38,7 @@ use workspace::{ notifications::NotifyResultExt, pane, register_deserializable_item, searchable::{SearchEvent, SearchOptions, SearchableItem, SearchableItemHandle}, - Pane, ToolbarItemLocation, Workspace, WorkspaceId, + NewCenterTerminal, Pane, ToolbarItemLocation, Workspace, WorkspaceId, }; pub use terminal::TerminalSettings; @@ -66,10 +66,10 @@ pub fn init(cx: &mut AppContext) { terminal_panel::init(cx); terminal::init(cx); - cx.add_action(TerminalView::deploy); - register_deserializable_item::(cx); + cx.add_action(TerminalView::deploy); + //Useful terminal views cx.add_action(TerminalView::send_text); cx.add_action(TerminalView::send_keystroke); @@ -101,7 +101,7 @@ impl TerminalView { ///Create a new Terminal in the current working directory or the user's home directory pub fn deploy( workspace: &mut Workspace, - _: &workspace::NewTerminal, + _: &NewCenterTerminal, cx: &mut ViewContext, ) { let strategy = settings::get::(cx); @@ -133,8 +133,8 @@ impl TerminalView { Event::Wakeup => { if !cx.is_self_focused() { this.has_new_content = true; - cx.notify(); } + cx.notify(); cx.emit(Event::Wakeup); } Event::Bell => { @@ -905,7 +905,10 @@ mod tests { cx: &mut TestAppContext, ) -> (ModelHandle, ViewHandle) { let params = cx.update(AppState::test); - cx.update(|cx| theme::init((), cx)); + cx.update(|cx| { + theme::init((), cx); + language::init(cx); + }); let project = Project::test(params.fs.clone(), [], cx).await; let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx)); diff --git a/crates/theme/src/theme.rs b/crates/theme/src/theme.rs index 1f99742cfe..f1e752b763 100644 --- a/crates/theme/src/theme.rs +++ b/crates/theme/src/theme.rs @@ -90,7 +90,8 @@ pub struct Workspace { pub breadcrumbs: Interactive, pub disconnected_overlay: ContainedText, pub modal: ContainerStyle, - pub zoomed_foreground: ContainerStyle, + pub zoomed_panel_foreground: ContainerStyle, + pub zoomed_pane_foreground: ContainerStyle, pub zoomed_background: ContainerStyle, pub notification: ContainerStyle, pub notifications: Notifications, diff --git a/crates/util/src/paths.rs b/crates/util/src/paths.rs index f998fc319f..e3397a1557 100644 --- a/crates/util/src/paths.rs +++ b/crates/util/src/paths.rs @@ -15,6 +15,7 @@ lazy_static::lazy_static! { pub static ref LAST_USERNAME: PathBuf = CONFIG_DIR.join("last-username.txt"); pub static ref LOG: PathBuf = LOGS_DIR.join("Zed.log"); pub static ref OLD_LOG: PathBuf = LOGS_DIR.join("Zed.log.old"); + pub static ref LOCAL_SETTINGS_RELATIVE_PATH: &'static Path = Path::new(".zed/settings.json"); } pub mod legacy { diff --git a/crates/welcome/src/welcome.rs b/crates/welcome/src/welcome.rs index cef6f53a6e..b7460c4c46 100644 --- a/crates/welcome/src/welcome.rs +++ b/crates/welcome/src/welcome.rs @@ -32,7 +32,7 @@ pub fn init(cx: &mut AppContext) { pub fn show_welcome_experience(app_state: &Arc, cx: &mut AppContext) { open_new(&app_state, cx, |workspace, cx| { - workspace.toggle_dock(DockPosition::Left, false, cx); + workspace.toggle_dock(DockPosition::Left, cx); let welcome_page = cx.add_view(|cx| WelcomePage::new(workspace, cx)); workspace.add_item_to_center(Box::new(welcome_page.clone()), cx); cx.focus(&welcome_page); diff --git a/crates/workspace/src/dock.rs b/crates/workspace/src/dock.rs index 49651486db..48f486381d 100644 --- a/crates/workspace/src/dock.rs +++ b/crates/workspace/src/dock.rs @@ -175,12 +175,16 @@ impl Dock { } } + pub fn position(&self) -> DockPosition { + self.position + } + pub fn is_open(&self) -> bool { self.is_open } pub fn has_focus(&self, cx: &WindowContext) -> bool { - self.active_panel() + self.visible_panel() .map_or(false, |panel| panel.has_focus(cx)) } @@ -207,7 +211,7 @@ impl Dock { self.active_panel_index } - pub fn set_open(&mut self, open: bool, cx: &mut ViewContext) { + pub(crate) fn set_open(&mut self, open: bool, cx: &mut ViewContext) { if open != self.is_open { self.is_open = open; if let Some(active_panel) = self.panel_entries.get(self.active_panel_index) { @@ -218,11 +222,6 @@ impl Dock { } } - pub fn toggle_open(&mut self, cx: &mut ViewContext) { - self.set_open(!self.is_open, cx); - cx.notify(); - } - pub fn set_panel_zoomed( &mut self, panel: &AnyViewHandle, @@ -265,7 +264,7 @@ impl Dock { cx.focus(&panel); } } else if T::should_close_on_event(event) - && this.active_panel().map_or(false, |p| p.id() == panel.id()) + && this.visible_panel().map_or(false, |p| p.id() == panel.id()) { this.set_open(false, cx); } @@ -321,12 +320,16 @@ impl Dock { } } - pub fn active_panel(&self) -> Option<&Rc> { - let entry = self.active_entry()?; + pub fn visible_panel(&self) -> Option<&Rc> { + let entry = self.visible_entry()?; Some(&entry.panel) } - fn active_entry(&self) -> Option<&PanelEntry> { + pub fn active_panel(&self) -> Option<&Rc> { + Some(&self.panel_entries.get(self.active_panel_index)?.panel) + } + + fn visible_entry(&self) -> Option<&PanelEntry> { if self.is_open { self.panel_entries.get(self.active_panel_index) } else { @@ -335,7 +338,7 @@ impl Dock { } pub fn zoomed_panel(&self, cx: &WindowContext) -> Option> { - let entry = self.active_entry()?; + let entry = self.visible_entry()?; if entry.panel.is_zoomed(cx) { Some(entry.panel.clone()) } else { @@ -368,7 +371,7 @@ impl Dock { } pub fn render_placeholder(&self, cx: &WindowContext) -> AnyElement { - if let Some(active_entry) = self.active_entry() { + if let Some(active_entry) = self.visible_entry() { Empty::new() .into_any() .contained() @@ -405,7 +408,7 @@ impl View for Dock { } fn render(&mut self, cx: &mut ViewContext) -> AnyElement { - if let Some(active_entry) = self.active_entry() { + if let Some(active_entry) = self.visible_entry() { let style = self.style(cx); ChildView::new(active_entry.panel.as_any(), cx) .contained() @@ -423,7 +426,7 @@ impl View for Dock { fn focus_in(&mut self, _: AnyViewHandle, cx: &mut ViewContext) { if cx.is_self_focused() { - if let Some(active_entry) = self.active_entry() { + if let Some(active_entry) = self.visible_entry() { cx.focus(active_entry.panel.as_any()); } else { cx.focus_parent(); @@ -479,11 +482,22 @@ impl View for PanelButtons { Flex::row() .with_children(panels.into_iter().enumerate().map( |(panel_ix, (view, context_menu))| { - let (tooltip, tooltip_action) = view.icon_tooltip(cx); + let is_active = is_open && panel_ix == active_ix; + let (tooltip, tooltip_action) = if is_active { + ( + format!("Close {} dock", dock_position.to_label()), + Some(match dock_position { + DockPosition::Left => crate::ToggleLeftDock.boxed_clone(), + DockPosition::Bottom => crate::ToggleBottomDock.boxed_clone(), + DockPosition::Right => crate::ToggleRightDock.boxed_clone(), + }), + ) + } else { + view.icon_tooltip(cx) + }; Stack::new() .with_child( MouseEventHandler::::new(panel_ix, cx, |state, cx| { - let is_active = is_open && panel_ix == active_ix; let style = button_style.style_for(state, is_active); Flex::row() .with_child( @@ -510,13 +524,22 @@ impl View for PanelButtons { }) .with_cursor_style(CursorStyle::PointingHand) .on_click(MouseButton::Left, { + let tooltip_action = + tooltip_action.as_ref().map(|action| action.boxed_clone()); move |_, this, cx| { - if let Some(workspace) = this.workspace.upgrade(cx) { - cx.window_context().defer(move |cx| { - workspace.update(cx, |workspace, cx| { - workspace.toggle_panel(dock_position, panel_ix, cx) - }); - }); + if let Some(tooltip_action) = &tooltip_action { + let window_id = cx.window_id(); + let view_id = this.workspace.id(); + let tooltip_action = tooltip_action.boxed_clone(); + cx.spawn(|_, mut cx| async move { + cx.dispatch_action( + window_id, + view_id, + &*tooltip_action, + ) + .ok(); + }) + .detach(); } } }) diff --git a/crates/workspace/src/notifications.rs b/crates/workspace/src/notifications.rs index 21b3be09d0..1e3c6044a1 100644 --- a/crates/workspace/src/notifications.rs +++ b/crates/workspace/src/notifications.rs @@ -1,5 +1,5 @@ use crate::{Toast, Workspace}; -use collections::HashSet; +use collections::HashMap; use gpui::{AnyViewHandle, AppContext, Entity, View, ViewContext, ViewHandle}; use std::{any::TypeId, ops::DerefMut}; @@ -33,12 +33,12 @@ impl From<&dyn NotificationHandle> for AnyViewHandle { } } -struct NotificationTracker { - notifications_sent: HashSet, +pub(crate) struct NotificationTracker { + notifications_sent: HashMap>, } impl std::ops::Deref for NotificationTracker { - type Target = HashSet; + type Target = HashMap>; fn deref(&self) -> &Self::Target { &self.notifications_sent @@ -54,24 +54,33 @@ impl DerefMut for NotificationTracker { impl NotificationTracker { fn new() -> Self { Self { - notifications_sent: HashSet::default(), + notifications_sent: Default::default(), } } } impl Workspace { + pub fn has_shown_notification_once( + &self, + id: usize, + cx: &ViewContext, + ) -> bool { + cx.global::() + .get(&TypeId::of::()) + .map(|ids| ids.contains(&id)) + .unwrap_or(false) + } + pub fn show_notification_once( &mut self, id: usize, cx: &mut ViewContext, build_notification: impl FnOnce(&mut ViewContext) -> ViewHandle, ) { - if !cx - .global::() - .contains(&TypeId::of::()) - { + if !self.has_shown_notification_once::(id, cx) { cx.update_global::(|tracker, _| { - tracker.insert(TypeId::of::()) + let entry = tracker.entry(TypeId::of::()).or_default(); + entry.push(id); }); self.show_notification::(id, cx, build_notification) @@ -154,9 +163,10 @@ pub mod simple_message_notification { use gpui::{ actions, elements::{Flex, MouseEventHandler, Padding, ParentElement, Svg, Text}, + fonts::TextStyle, impl_actions, platform::{CursorStyle, MouseButton}, - AppContext, Element, Entity, View, ViewContext, + AnyElement, AppContext, Element, Entity, View, ViewContext, }; use menu::Cancel; use serde::Deserialize; @@ -184,8 +194,13 @@ pub mod simple_message_notification { ) } + enum NotificationMessage { + Text(Cow<'static, str>), + Element(fn(TextStyle, &AppContext) -> AnyElement), + } + pub struct MessageNotification { - message: Cow<'static, str>, + message: NotificationMessage, on_click: Option)>>, click_message: Option>, } @@ -204,7 +219,17 @@ pub mod simple_message_notification { S: Into>, { Self { - message: message.into(), + message: NotificationMessage::Text(message.into()), + on_click: None, + click_message: None, + } + } + + pub fn new_element( + message: fn(TextStyle, &AppContext) -> AnyElement, + ) -> MessageNotification { + Self { + message: NotificationMessage::Element(message), on_click: None, click_message: None, } @@ -243,84 +268,90 @@ pub mod simple_message_notification { enum MessageNotificationTag {} let click_message = self.click_message.clone(); - let message = self.message.clone(); + let message = match &self.message { + NotificationMessage::Text(text) => { + Text::new(text.to_owned(), theme.message.text.clone()).into_any() + } + NotificationMessage::Element(e) => e(theme.message.text.clone(), cx), + }; let on_click = self.on_click.clone(); let has_click_action = on_click.is_some(); - MouseEventHandler::::new(0, cx, |state, cx| { - Flex::column() - .with_child( - Flex::row() - .with_child( - Text::new(message, theme.message.text.clone()) - .contained() - .with_style(theme.message.container) - .aligned() - .top() - .left() - .flex(1., true), - ) - .with_child( - MouseEventHandler::::new(0, cx, |state, _| { - let style = theme.dismiss_button.style_for(state, false); - Svg::new("icons/x_mark_8.svg") - .with_color(style.color) - .constrained() - .with_width(style.icon_width) - .aligned() - .contained() - .with_style(style.container) - .constrained() - .with_width(style.button_width) - .with_height(style.button_width) - }) - .with_padding(Padding::uniform(5.)) - .on_click(MouseButton::Left, move |_, this, cx| { - this.dismiss(&Default::default(), cx); - }) - .with_cursor_style(CursorStyle::PointingHand) - .aligned() - .constrained() - .with_height( - cx.font_cache().line_height(theme.message.text.font_size), - ) + Flex::column() + .with_child( + Flex::row() + .with_child( + message + .contained() + .with_style(theme.message.container) .aligned() .top() - .flex_float(), - ), - ) - .with_children({ - let style = theme.action_message.style_for(state, false); - if let Some(click_message) = click_message { - Some( - Flex::row().with_child( - Text::new(click_message, style.text.clone()) + .left() + .flex(1., true), + ) + .with_child( + MouseEventHandler::::new(0, cx, |state, _| { + let style = theme.dismiss_button.style_for(state, false); + Svg::new("icons/x_mark_8.svg") + .with_color(style.color) + .constrained() + .with_width(style.icon_width) + .aligned() + .contained() + .with_style(style.container) + .constrained() + .with_width(style.button_width) + .with_height(style.button_width) + }) + .with_padding(Padding::uniform(5.)) + .on_click(MouseButton::Left, move |_, this, cx| { + this.dismiss(&Default::default(), cx); + }) + .with_cursor_style(CursorStyle::PointingHand) + .aligned() + .constrained() + .with_height(cx.font_cache().line_height(theme.message.text.font_size)) + .aligned() + .top() + .flex_float(), + ), + ) + .with_children({ + click_message + .map(|click_message| { + MouseEventHandler::::new( + 0, + cx, + |state, _| { + let style = theme.action_message.style_for(state, false); + + Flex::row() + .with_child( + Text::new(click_message, style.text.clone()) + .contained() + .with_style(style.container), + ) .contained() - .with_style(style.container), - ), + }, ) - } else { - None - } + .on_click(MouseButton::Left, move |_, this, cx| { + if let Some(on_click) = on_click.as_ref() { + on_click(cx); + this.dismiss(&Default::default(), cx); + } + }) + // Since we're not using a proper overlay, we have to capture these extra events + .on_down(MouseButton::Left, |_, _, _| {}) + .on_up(MouseButton::Left, |_, _, _| {}) + .with_cursor_style(if has_click_action { + CursorStyle::PointingHand + } else { + CursorStyle::Arrow + }) + }) .into_iter() - }) - .contained() - }) - // 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 |_, this, cx| { - if let Some(on_click) = on_click.as_ref() { - on_click(cx); - this.dismiss(&Default::default(), cx); - } - }) - .with_cursor_style(if has_click_action { - CursorStyle::PointingHand - } else { - CursorStyle::Arrow - }) - .into_any() + }) + .into_any() } } diff --git a/crates/workspace/src/pane.rs b/crates/workspace/src/pane.rs index 2919bb7cd4..1fa8c15f9b 100644 --- a/crates/workspace/src/pane.rs +++ b/crates/workspace/src/pane.rs @@ -2,8 +2,8 @@ mod dragged_item_receiver; use super::{ItemHandle, SplitDirection}; use crate::{ - item::WeakItemHandle, toolbar::Toolbar, AutosaveSetting, Item, NewFile, NewSearch, NewTerminal, - ToggleZoom, Workspace, WorkspaceSettings, + item::WeakItemHandle, notify_of_new_dock, toolbar::Toolbar, AutosaveSetting, Item, + NewCenterTerminal, NewFile, NewSearch, ToggleZoom, Workspace, WorkspaceSettings, }; use anyhow::Result; use collections::{HashMap, HashSet, VecDeque}; @@ -131,7 +131,6 @@ pub enum Event { pub struct Pane { items: Vec>, activation_history: Vec, - is_active: bool, zoomed: bool, active_item_index: usize, last_focused_view_by_item: HashMap, @@ -238,7 +237,6 @@ impl Pane { Self { items: Vec::new(), activation_history: Vec::new(), - is_active: true, zoomed: false, active_item_index: 0, last_focused_view_by_item: Default::default(), @@ -270,6 +268,7 @@ impl Pane { .with_child(Self::render_tab_bar_button( 0, "icons/plus_12.svg", + false, Some(("New...".into(), None)), cx, |pane, cx| pane.deploy_new_menu(cx), @@ -279,6 +278,7 @@ impl Pane { .with_child(Self::render_tab_bar_button( 1, "icons/split_12.svg", + false, Some(("Split Pane".into(), None)), cx, |pane, cx| pane.deploy_split_menu(cx), @@ -292,6 +292,7 @@ impl Pane { } else { "icons/maximize_8.svg" }, + pane.is_zoomed(), Some(("Toggle Zoom".into(), Some(Box::new(ToggleZoom)))), cx, move |pane, cx| pane.toggle_zoom(&Default::default(), cx), @@ -306,15 +307,6 @@ impl Pane { &self.workspace } - pub fn is_active(&self) -> bool { - self.is_active - } - - pub fn set_active(&mut self, is_active: bool, cx: &mut ViewContext) { - self.is_active = is_active; - cx.notify(); - } - pub fn has_focus(&self) -> bool { self.has_focus } @@ -547,6 +539,11 @@ impl Pane { } pub fn toggle_zoom(&mut self, _: &ToggleZoom, cx: &mut ViewContext) { + // Potentially warn the user of the new keybinding + let workspace_handle = self.workspace().clone(); + cx.spawn(|_, mut cx| async move { notify_of_new_dock(&workspace_handle, &mut cx) }) + .detach(); + if self.zoomed { cx.emit(Event::ZoomOut); } else if !self.items.is_empty() { @@ -1005,7 +1002,7 @@ impl Pane { AnchorCorner::TopRight, vec![ ContextMenuItem::action("New File", NewFile), - ContextMenuItem::action("New Terminal", NewTerminal), + ContextMenuItem::action("New Terminal", NewCenterTerminal), ContextMenuItem::action("New Search", NewSearch), ], cx, @@ -1129,7 +1126,7 @@ impl Pane { None }; - let pane_active = self.is_active; + let pane_active = self.has_focus; enum Tabs {} let mut row = Flex::row().scrollable::(1, autoscroll, cx); @@ -1412,6 +1409,7 @@ impl Pane { pub fn render_tab_bar_button)>( index: usize, icon: &'static str, + active: bool, tooltip: Option<(String, Option>)>, cx: &mut ViewContext, on_click: F, @@ -1421,7 +1419,7 @@ impl Pane { let mut button = MouseEventHandler::::new(index, cx, |mouse_state, cx| { let theme = &settings::get::(cx).theme.workspace.tab_bar; - let style = theme.pane_button.style_for(mouse_state, false); + let style = theme.pane_button.style_for(mouse_state, active); Svg::new(icon) .with_color(style.color) .constrained() @@ -1508,7 +1506,7 @@ impl View for Pane { let mut tab_row = Flex::row() .with_child(self.render_tabs(cx).flex(1., true).into_any_named("tabs")); - if self.is_active { + if self.has_focus { let render_tab_bar_buttons = self.render_tab_bar_buttons.clone(); tab_row.add_child( (render_tab_bar_buttons)(self, cx) @@ -1599,6 +1597,7 @@ impl View for Pane { if !self.has_focus { self.has_focus = true; cx.emit(Event::Focus); + cx.notify(); } self.toolbar.update(cx, |toolbar, cx| { @@ -1633,6 +1632,7 @@ impl View for Pane { self.toolbar.update(cx, |toolbar, cx| { toolbar.pane_focus_update(false, cx); }); + cx.notify(); } fn update_keymap_context(&self, keymap: &mut KeymapContext, _: &AppContext) { diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 2aa937c9a9..9cc175ab4c 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -53,13 +53,14 @@ use std::{ cmp, env, future::Future, path::{Path, PathBuf}, + rc::Rc, str, sync::{atomic::AtomicUsize, Arc}, time::Duration, }; use crate::{ - notifications::simple_message_notification::MessageNotification, + notifications::{simple_message_notification::MessageNotification, NotificationTracker}, persistence::model::{ DockData, DockStructure, SerializedPane, SerializedPaneGroup, SerializedWorkspace, }, @@ -80,7 +81,7 @@ use serde::Deserialize; use shared_screen::SharedScreen; use status_bar::StatusBar; pub use status_bar::StatusItemView; -use theme::Theme; +use theme::{Theme, ThemeSettings}; pub use toolbar::{ToolbarItemLocation, ToolbarItemView}; use util::{async_iife, paths, ResultExt}; pub use workspace_settings::{AutosaveSetting, GitGutterSetting, WorkspaceSettings}; @@ -103,24 +104,6 @@ pub trait Modal: View { #[derive(Clone, PartialEq)] pub struct RemoveWorktreeFromProject(pub WorktreeId); -#[derive(Copy, Clone, Default, Deserialize, PartialEq)] -pub struct ToggleLeftDock { - #[serde(default = "default_true")] - pub focus: bool, -} - -#[derive(Copy, Clone, Default, Deserialize, PartialEq)] -pub struct ToggleBottomDock { - #[serde(default = "default_true")] - pub focus: bool, -} - -#[derive(Copy, Clone, Default, Deserialize, PartialEq)] -pub struct ToggleRightDock { - #[serde(default = "default_true")] - pub focus: bool, -} - actions!( workspace, [ @@ -137,22 +120,21 @@ actions!( ActivateNextPane, FollowNextCollaborator, NewTerminal, + NewCenterTerminal, ToggleTerminalFocus, NewSearch, Feedback, Restart, Welcome, ToggleZoom, + ToggleLeftDock, + ToggleRightDock, + ToggleBottomDock, ] ); actions!(zed, [OpenSettings]); -impl_actions!( - workspace, - [ToggleLeftDock, ToggleBottomDock, ToggleRightDock] -); - #[derive(Clone, PartialEq)] pub struct OpenPaths { pub paths: Vec, @@ -268,14 +250,14 @@ pub fn init(app_state: Arc, cx: &mut AppContext) { cx.add_action(|workspace: &mut Workspace, _: &ActivateNextPane, cx| { workspace.activate_next_pane(cx) }); - cx.add_action(|workspace: &mut Workspace, action: &ToggleLeftDock, cx| { - workspace.toggle_dock(DockPosition::Left, action.focus, cx); + cx.add_action(|workspace: &mut Workspace, _: &ToggleLeftDock, cx| { + workspace.toggle_dock(DockPosition::Left, cx); }); - cx.add_action(|workspace: &mut Workspace, action: &ToggleRightDock, cx| { - workspace.toggle_dock(DockPosition::Right, action.focus, cx); + cx.add_action(|workspace: &mut Workspace, _: &ToggleRightDock, cx| { + workspace.toggle_dock(DockPosition::Right, cx); }); - cx.add_action(|workspace: &mut Workspace, action: &ToggleBottomDock, cx| { - workspace.toggle_dock(DockPosition::Bottom, action.focus, cx); + cx.add_action(|workspace: &mut Workspace, _: &ToggleBottomDock, cx| { + workspace.toggle_dock(DockPosition::Bottom, cx); }); cx.add_action(Workspace::activate_pane_at_index); cx.add_action(|workspace: &mut Workspace, _: &ReopenClosedItem, cx| { @@ -498,6 +480,7 @@ pub struct Workspace { remote_entity_subscription: Option, modal: Option, zoomed: Option, + zoomed_position: Option, center: PaneGroup, left_dock: ViewHandle, bottom_dock: ViewHandle, @@ -703,6 +686,7 @@ impl Workspace { weak_self: weak_handle.clone(), modal: None, zoomed: None, + zoomed_position: None, center: PaneGroup::new(center_pane.clone()), panes: vec![center_pane.clone()], panes_by_item: Default::default(), @@ -901,10 +885,15 @@ impl Workspace { was_visible = dock.is_open() && dock - .active_panel() + .visible_panel() .map_or(false, |active_panel| active_panel.id() == panel.id()); dock.remove_panel(&panel, cx); }); + + if panel.is_zoomed(cx) { + this.zoomed_position = Some(new_position); + } + dock = match panel.read(cx).position(cx) { DockPosition::Left => &this.left_dock, DockPosition::Bottom => &this.bottom_dock, @@ -919,18 +908,27 @@ impl Workspace { } }); } else if T::should_zoom_in_on_event(event) { - this.zoom_out(cx); dock.update(cx, |dock, cx| dock.set_panel_zoomed(&panel, true, cx)); if panel.has_focus(cx) { this.zoomed = Some(panel.downgrade().into_any()); + this.zoomed_position = Some(panel.read(cx).position(cx)); } } else if T::should_zoom_out_on_event(event) { - this.zoom_out(cx); + dock.update(cx, |dock, cx| dock.set_panel_zoomed(&panel, false, cx)); + if this.zoomed_position == Some(prev_position) { + this.zoomed = None; + this.zoomed_position = None; + } + cx.notify(); } else if T::is_focus_event(event) { + let position = panel.read(cx).position(cx); + this.dismiss_zoomed_items_to_reveal(Some(position), cx); if panel.is_zoomed(cx) { this.zoomed = Some(panel.downgrade().into_any()); + this.zoomed_position = Some(position); } else { this.zoomed = None; + this.zoomed_position = None; } cx.notify(); } @@ -976,9 +974,8 @@ impl Workspace { let timestamp = entry.timestamp; match history.entry(project_path) { hash_map::Entry::Occupied(mut entry) => { - let (old_fs_path, old_timestamp) = entry.get(); + let (_, old_timestamp) = entry.get(); if ×tamp > old_timestamp { - assert_eq!(&fs_path, old_fs_path, "Inconsistent nav history"); entry.insert((fs_path, timestamp)); } } @@ -1593,89 +1590,98 @@ impl Workspace { } } - pub fn toggle_dock( - &mut self, - dock_side: DockPosition, - focus: bool, - cx: &mut ViewContext, - ) { + pub fn toggle_dock(&mut self, dock_side: DockPosition, cx: &mut ViewContext) { let dock = match dock_side { DockPosition::Left => &self.left_dock, DockPosition::Bottom => &self.bottom_dock, DockPosition::Right => &self.right_dock, }; + let mut focus_center = false; + let mut reveal_dock = false; dock.update(cx, |dock, cx| { - let open = !dock.is_open(); - dock.set_open(open, cx); + let other_is_zoomed = self.zoomed.is_some() && self.zoomed_position != Some(dock_side); + let was_visible = dock.is_open() && !other_is_zoomed; + dock.set_open(!was_visible, cx); + + if let Some(active_panel) = dock.active_panel() { + if was_visible { + if active_panel.has_focus(cx) { + focus_center = true; + } + } else { + if active_panel.is_zoomed(cx) { + cx.focus(active_panel.as_any()); + } + reveal_dock = true; + } + } }); - if dock.read(cx).is_open() && focus { - cx.focus(dock); - } else { + if reveal_dock { + self.dismiss_zoomed_items_to_reveal(Some(dock_side), cx); + } + + if focus_center { cx.focus_self(); } + cx.notify(); self.serialize_workspace(cx); } - pub fn toggle_panel( - &mut self, - position: DockPosition, - panel_index: usize, - cx: &mut ViewContext, - ) { - let dock = match position { - DockPosition::Left => &mut self.left_dock, - DockPosition::Bottom => &mut self.bottom_dock, - DockPosition::Right => &mut self.right_dock, - }; - let active_item = dock.update(cx, move |dock, cx| { - if dock.is_open() && dock.active_panel_index() == panel_index { - dock.set_open(false, cx); - None - } else { - dock.set_open(true, cx); - dock.activate_panel(panel_index, cx); - dock.active_panel().cloned() - } - }); - - if let Some(active_item) = active_item { - if active_item.has_focus(cx) { - cx.focus_self(); - } else { - cx.focus(active_item.as_any()); - } - } else { - cx.focus_self(); - } - - self.serialize_workspace(cx); - - cx.notify(); + /// Transfer focus to the panel of the given type. + pub fn focus_panel(&mut self, cx: &mut ViewContext) -> Option> { + self.focus_or_unfocus_panel::(cx, |_, _| true)? + .as_any() + .clone() + .downcast() } + /// Focus the panel of the given type if it isn't already focused. If it is + /// already focused, then transfer focus back to the workspace center. pub fn toggle_panel_focus(&mut self, cx: &mut ViewContext) { + self.focus_or_unfocus_panel::(cx, |panel, cx| !panel.has_focus(cx)); + } + + /// Focus or unfocus the given panel type, depending on the given callback. + fn focus_or_unfocus_panel( + &mut self, + cx: &mut ViewContext, + should_focus: impl Fn(&dyn PanelHandle, &mut ViewContext) -> bool, + ) -> Option> { for dock in [&self.left_dock, &self.bottom_dock, &self.right_dock] { if let Some(panel_index) = dock.read(cx).panel_index_for_type::() { - let active_item = dock.update(cx, |dock, cx| { - dock.set_open(true, cx); + let mut focus_center = false; + let mut reveal_dock = false; + let panel = dock.update(cx, |dock, cx| { dock.activate_panel(panel_index, cx); - dock.active_panel().cloned() - }); - if let Some(active_item) = active_item { - if active_item.has_focus(cx) { - cx.focus_self(); - } else { - cx.focus(active_item.as_any()); + + let panel = dock.active_panel().cloned(); + if let Some(panel) = panel.as_ref() { + if should_focus(&**panel, cx) { + dock.set_open(true, cx); + cx.focus(panel.as_any()); + reveal_dock = true; + } else { + // if panel.is_zoomed(cx) { + // dock.set_open(false, cx); + // } + focus_center = true; + } } + panel + }); + + if focus_center { + cx.focus_self(); } self.serialize_workspace(cx); cx.notify(); - break; + return panel; } } + None } pub fn panel(&self, cx: &WindowContext) -> Option> { @@ -1697,6 +1703,46 @@ impl Workspace { self.bottom_dock.update(cx, |dock, cx| dock.zoom_out(cx)); self.right_dock.update(cx, |dock, cx| dock.zoom_out(cx)); self.zoomed = None; + self.zoomed_position = None; + + cx.notify(); + } + + fn dismiss_zoomed_items_to_reveal( + &mut self, + dock_to_reveal: Option, + cx: &mut ViewContext, + ) { + // If a center pane is zoomed, unzoom it. + for pane in &self.panes { + if pane != &self.active_pane || dock_to_reveal.is_some() { + pane.update(cx, |pane, cx| pane.set_zoomed(false, cx)); + } + } + + // If another dock is zoomed, hide it. + let mut focus_center = false; + for dock in [&self.left_dock, &self.right_dock, &self.bottom_dock] { + dock.update(cx, |dock, cx| { + if Some(dock.position()) != dock_to_reveal { + if let Some(panel) = dock.active_panel() { + if panel.is_zoomed(cx) { + focus_center |= panel.has_focus(cx); + dock.set_open(false, cx); + } + } + } + }); + } + + if focus_center { + cx.focus_self(); + } + + if self.zoomed_position != dock_to_reveal { + self.zoomed = None; + self.zoomed_position = None; + } cx.notify(); } @@ -1896,11 +1942,7 @@ impl Workspace { fn handle_pane_focused(&mut self, pane: ViewHandle, cx: &mut ViewContext) { if self.active_pane != pane { - self.active_pane - .update(cx, |pane, cx| pane.set_active(false, cx)); self.active_pane = pane.clone(); - self.active_pane - .update(cx, |pane, cx| pane.set_active(true, cx)); self.status_bar.update(cx, |status_bar, cx| { status_bar.set_active_pane(&self.active_pane, cx); }); @@ -1908,11 +1950,13 @@ impl Workspace { self.last_active_center_pane = Some(pane.downgrade()); } + self.dismiss_zoomed_items_to_reveal(None, cx); if pane.read(cx).is_zoomed() { self.zoomed = Some(pane.downgrade().into_any()); } else { self.zoomed = None; } + self.zoomed_position = None; self.update_followers( proto::update_followers::Variant::UpdateActiveView(proto::UpdateActiveView { @@ -1968,15 +2012,21 @@ impl Workspace { } pane::Event::ZoomIn => { if pane == self.active_pane { - self.zoom_out(cx); pane.update(cx, |pane, cx| pane.set_zoomed(true, cx)); if pane.read(cx).has_focus() { self.zoomed = Some(pane.downgrade().into_any()); + self.zoomed_position = None; } cx.notify(); } } - pane::Event::ZoomOut => self.zoom_out(cx), + pane::Event::ZoomOut => { + pane.update(cx, |pane, cx| pane.set_zoomed(false, cx)); + if self.zoomed_position.is_none() { + self.zoomed = None; + } + cx.notify(); + } } self.serialize_workspace(cx); @@ -2817,7 +2867,7 @@ impl Workspace { }) }) .collect::>(), - pane.is_active(), + pane.has_focus(), ) }; @@ -2845,7 +2895,7 @@ impl Workspace { fn build_serialized_docks(this: &Workspace, cx: &AppContext) -> DockStructure { let left_dock = this.left_dock.read(cx); let left_visible = left_dock.is_open(); - let left_active_panel = left_dock.active_panel().and_then(|panel| { + let left_active_panel = left_dock.visible_panel().and_then(|panel| { Some( cx.view_ui_name(panel.as_any().window_id(), panel.id())? .to_string(), @@ -2854,7 +2904,7 @@ impl Workspace { let right_dock = this.right_dock.read(cx); let right_visible = right_dock.is_open(); - let right_active_panel = right_dock.active_panel().and_then(|panel| { + let right_active_panel = right_dock.visible_panel().and_then(|panel| { Some( cx.view_ui_name(panel.as_any().window_id(), panel.id())? .to_string(), @@ -2863,7 +2913,7 @@ impl Workspace { let bottom_dock = this.bottom_dock.read(cx); let bottom_visible = bottom_dock.is_open(); - let bottom_active_panel = bottom_dock.active_panel().and_then(|panel| { + let bottom_active_panel = bottom_dock.visible_panel().and_then(|panel| { Some( cx.view_ui_name(panel.as_any().window_id(), panel.id())? .to_string(), @@ -3045,7 +3095,7 @@ impl Workspace { DockPosition::Right => &self.right_dock, DockPosition::Bottom => &self.bottom_dock, }; - let active_panel = dock.read(cx).active_panel()?; + let active_panel = dock.read(cx).visible_panel()?; let element = if Some(active_panel.id()) == self.zoomed.as_ref().map(|zoomed| zoomed.id()) { dock.read(cx).render_placeholder(cx) } else { @@ -3159,6 +3209,87 @@ async fn open_items( opened_items } +fn notify_of_new_dock(workspace: &WeakViewHandle, cx: &mut AsyncAppContext) { + const NEW_PANEL_BLOG_POST: &str = "https://zed.dev/blog/new-panel-system"; + const NEW_DOCK_HINT_KEY: &str = "show_new_dock_key"; + const MESSAGE_ID: usize = 2; + + if workspace + .read_with(cx, |workspace, cx| { + workspace.has_shown_notification_once::(MESSAGE_ID, cx) + }) + .unwrap_or(false) + { + return; + } + + if db::kvp::KEY_VALUE_STORE + .read_kvp(NEW_DOCK_HINT_KEY) + .ok() + .flatten() + .is_some() + { + if !workspace + .read_with(cx, |workspace, cx| { + workspace.has_shown_notification_once::(MESSAGE_ID, cx) + }) + .unwrap_or(false) + { + cx.update(|cx| { + cx.update_global::(|tracker, _| { + let entry = tracker + .entry(TypeId::of::()) + .or_default(); + if !entry.contains(&MESSAGE_ID) { + entry.push(MESSAGE_ID); + } + }); + }); + } + + return; + } + + cx.spawn(|_| async move { + db::kvp::KEY_VALUE_STORE + .write_kvp(NEW_DOCK_HINT_KEY.to_string(), "seen".to_string()) + .await + .ok(); + }) + .detach(); + + workspace + .update(cx, |workspace, cx| { + workspace.show_notification_once(2, cx, |cx| { + cx.add_view(|_| { + MessageNotification::new_element(|text, _| { + Text::new( + "Looking for the dock? Try ctrl-`!\nshift-escape now zooms your pane.", + text, + ) + .with_custom_runs(vec![26..32, 34..46], |_, bounds, scene, cx| { + let code_span_background_color = settings::get::(cx) + .theme + .editor + .document_highlight_read_background; + + scene.push_quad(gpui::Quad { + bounds, + background: Some(code_span_background_color), + border: Default::default(), + corner_radius: 2.0, + }) + }) + .into_any() + }) + .with_click_message("Read more about the new panel system") + .on_click(|cx| cx.platform().open_url(NEW_PANEL_BLOG_POST)) + }) + }) + }) + .ok(); +} + fn notify_if_database_failed(workspace: &WeakViewHandle, cx: &mut AsyncAppContext) { const REPORT_ISSUE_URL: &str ="https://github.com/zed-industries/community/issues/new?assignees=&labels=defect%2Ctriage&template=2_bug_report.yml"; @@ -3175,7 +3306,7 @@ fn notify_if_database_failed(workspace: &WeakViewHandle, cx: &mut Asy } else { let backup_path = (*db::BACKUP_DB_PATH).read(); if let Some(backup_path) = backup_path.clone() { - workspace.show_notification_once(0, cx, move |cx| { + workspace.show_notification_once(1, cx, move |cx| { cx.add_view(move |_| { MessageNotification::new(format!( "Database file was corrupted. Old database backed up to {}", @@ -3246,10 +3377,44 @@ impl View for Workspace { .with_children(self.zoomed.as_ref().and_then(|zoomed| { enum ZoomBackground {} let zoomed = zoomed.upgrade(cx)?; + + let mut foreground_style = + theme.workspace.zoomed_pane_foreground; + if let Some(zoomed_dock_position) = self.zoomed_position { + foreground_style = + theme.workspace.zoomed_panel_foreground; + let margin = foreground_style.margin.top; + let border = foreground_style.border.top; + + // Only include a margin and border on the opposite side. + foreground_style.margin.top = 0.; + foreground_style.margin.left = 0.; + foreground_style.margin.bottom = 0.; + foreground_style.margin.right = 0.; + foreground_style.border.top = false; + foreground_style.border.left = false; + foreground_style.border.bottom = false; + foreground_style.border.right = false; + match zoomed_dock_position { + DockPosition::Left => { + foreground_style.margin.right = margin; + foreground_style.border.right = border; + } + DockPosition::Right => { + foreground_style.margin.left = margin; + foreground_style.border.left = border; + } + DockPosition::Bottom => { + foreground_style.margin.top = margin; + foreground_style.border.top = border; + } + } + } + Some( ChildView::new(&zoomed, cx) .contained() - .with_style(theme.workspace.zoomed_foreground) + .with_style(foreground_style) .aligned() .contained() .with_style(theme.workspace.zoomed_background) @@ -3599,10 +3764,6 @@ fn parse_pixel_position_env_var(value: &str) -> Option { Some(vec2f(width as f32, height as f32)) } -fn default_true() -> bool { - true -} - #[cfg(test)] mod tests { use super::*; @@ -4181,6 +4342,153 @@ mod tests { }); } + #[gpui::test] + async fn test_toggle_docks_and_panels(cx: &mut gpui::TestAppContext) { + init_test(cx); + let fs = FakeFs::new(cx.background()); + + let project = Project::test(fs, [], cx).await; + let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx)); + + let panel = workspace.update(cx, |workspace, cx| { + let panel = cx.add_view(|_| TestPanel::new(DockPosition::Right)); + workspace.add_panel(panel.clone(), cx); + + workspace + .right_dock() + .update(cx, |right_dock, cx| right_dock.set_open(true, cx)); + + panel + }); + + let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone()); + pane.update(cx, |pane, cx| { + let item = cx.add_view(|_| TestItem::new()); + pane.add_item(Box::new(item), true, true, None, cx); + }); + + // Transfer focus from center to panel + workspace.update(cx, |workspace, cx| { + workspace.toggle_panel_focus::(cx); + }); + + workspace.read_with(cx, |workspace, cx| { + assert!(workspace.right_dock().read(cx).is_open()); + assert!(!panel.is_zoomed(cx)); + assert!(panel.has_focus(cx)); + }); + + // Transfer focus from panel to center + workspace.update(cx, |workspace, cx| { + workspace.toggle_panel_focus::(cx); + }); + + workspace.read_with(cx, |workspace, cx| { + assert!(workspace.right_dock().read(cx).is_open()); + assert!(!panel.is_zoomed(cx)); + assert!(!panel.has_focus(cx)); + }); + + // Close the dock + workspace.update(cx, |workspace, cx| { + workspace.toggle_dock(DockPosition::Right, cx); + }); + + workspace.read_with(cx, |workspace, cx| { + assert!(!workspace.right_dock().read(cx).is_open()); + assert!(!panel.is_zoomed(cx)); + assert!(!panel.has_focus(cx)); + }); + + // Open the dock + workspace.update(cx, |workspace, cx| { + workspace.toggle_dock(DockPosition::Right, cx); + }); + + workspace.read_with(cx, |workspace, cx| { + assert!(workspace.right_dock().read(cx).is_open()); + assert!(!panel.is_zoomed(cx)); + assert!(!panel.has_focus(cx)); + }); + + // Focus and zoom panel + panel.update(cx, |panel, cx| { + cx.focus_self(); + panel.set_zoomed(true, cx) + }); + + workspace.read_with(cx, |workspace, cx| { + assert!(workspace.right_dock().read(cx).is_open()); + assert!(panel.is_zoomed(cx)); + assert!(panel.has_focus(cx)); + }); + + // Transfer focus to the center closes the dock + workspace.update(cx, |workspace, cx| { + workspace.toggle_panel_focus::(cx); + }); + + workspace.read_with(cx, |workspace, cx| { + assert!(!workspace.right_dock().read(cx).is_open()); + assert!(panel.is_zoomed(cx)); + assert!(!panel.has_focus(cx)); + }); + + // Transfering focus back to the panel keeps it zoomed + workspace.update(cx, |workspace, cx| { + workspace.toggle_panel_focus::(cx); + }); + + workspace.read_with(cx, |workspace, cx| { + assert!(workspace.right_dock().read(cx).is_open()); + assert!(panel.is_zoomed(cx)); + assert!(panel.has_focus(cx)); + }); + + // Close the dock while it is zoomed + workspace.update(cx, |workspace, cx| { + workspace.toggle_dock(DockPosition::Right, cx) + }); + + workspace.read_with(cx, |workspace, cx| { + assert!(!workspace.right_dock().read(cx).is_open()); + assert!(panel.is_zoomed(cx)); + assert!(workspace.zoomed.is_none()); + assert!(!panel.has_focus(cx)); + }); + + // Opening the dock, when it's zoomed, retains focus + workspace.update(cx, |workspace, cx| { + workspace.toggle_dock(DockPosition::Right, cx) + }); + + workspace.read_with(cx, |workspace, cx| { + assert!(workspace.right_dock().read(cx).is_open()); + assert!(panel.is_zoomed(cx)); + assert!(workspace.zoomed.is_some()); + assert!(panel.has_focus(cx)); + }); + + // Unzoom and close the panel, zoom the active pane. + panel.update(cx, |panel, cx| panel.set_zoomed(false, cx)); + workspace.update(cx, |workspace, cx| { + workspace.toggle_dock(DockPosition::Right, cx) + }); + pane.update(cx, |pane, cx| pane.toggle_zoom(&Default::default(), cx)); + + // Opening a dock unzooms the pane. + workspace.update(cx, |workspace, cx| { + workspace.toggle_dock(DockPosition::Right, cx) + }); + workspace.read_with(cx, |workspace, cx| { + let pane = pane.read(cx); + assert!(!pane.is_zoomed()); + assert!(pane.has_focus()); + assert!(workspace.right_dock().read(cx).is_open()); + assert!(workspace.zoomed.is_none()); + }); + } + #[gpui::test] async fn test_panels(cx: &mut gpui::TestAppContext) { init_test(cx); @@ -4204,7 +4512,7 @@ mod tests { let left_dock = workspace.left_dock(); assert_eq!( - left_dock.read(cx).active_panel().unwrap().id(), + left_dock.read(cx).visible_panel().unwrap().id(), panel_1.id() ); assert_eq!( @@ -4214,7 +4522,12 @@ mod tests { left_dock.update(cx, |left_dock, cx| left_dock.resize_active_panel(1337., cx)); assert_eq!( - workspace.right_dock().read(cx).active_panel().unwrap().id(), + workspace + .right_dock() + .read(cx) + .visible_panel() + .unwrap() + .id(), panel_2.id() ); @@ -4230,10 +4543,10 @@ mod tests { // Since panel_1 was visible on the left, it should now be visible now that it's been moved to the right. // Since it was the only panel on the left, the left dock should now be closed. assert!(!workspace.left_dock().read(cx).is_open()); - assert!(workspace.left_dock().read(cx).active_panel().is_none()); + assert!(workspace.left_dock().read(cx).visible_panel().is_none()); let right_dock = workspace.right_dock(); assert_eq!( - right_dock.read(cx).active_panel().unwrap().id(), + right_dock.read(cx).visible_panel().unwrap().id(), panel_1.id() ); assert_eq!(right_dock.read(cx).active_panel_size(cx).unwrap(), 1337.); @@ -4248,7 +4561,12 @@ mod tests { // And the right dock is unaffected in it's displaying of panel_1 assert!(workspace.right_dock().read(cx).is_open()); assert_eq!( - workspace.right_dock().read(cx).active_panel().unwrap().id(), + workspace + .right_dock() + .read(cx) + .visible_panel() + .unwrap() + .id(), panel_1.id() ); }); @@ -4263,7 +4581,7 @@ mod tests { let left_dock = workspace.left_dock(); assert!(left_dock.read(cx).is_open()); assert_eq!( - left_dock.read(cx).active_panel().unwrap().id(), + left_dock.read(cx).visible_panel().unwrap().id(), panel_1.id() ); assert_eq!(left_dock.read(cx).active_panel_size(cx).unwrap(), 1337.); @@ -4297,7 +4615,7 @@ mod tests { let left_dock = workspace.left_dock(); assert!(left_dock.read(cx).is_open()); assert_eq!( - left_dock.read(cx).active_panel().unwrap().id(), + left_dock.read(cx).visible_panel().unwrap().id(), panel_1.id() ); assert!(panel_1.is_focused(cx)); @@ -4311,7 +4629,7 @@ mod tests { let left_dock = workspace.left_dock(); assert!(left_dock.read(cx).is_open()); assert_eq!( - left_dock.read(cx).active_panel().unwrap().id(), + left_dock.read(cx).visible_panel().unwrap().id(), panel_1.id() ); }); @@ -4320,6 +4638,14 @@ mod tests { panel_1.update(cx, |_, cx| cx.emit(TestPanelEvent::ZoomIn)); workspace.read_with(cx, |workspace, _| { assert_eq!(workspace.zoomed, Some(panel_1.downgrade().into_any())); + assert_eq!(workspace.zoomed_position, Some(DockPosition::Left)); + }); + + // Move panel to another dock while it is zoomed + panel_1.update(cx, |panel, cx| panel.set_position(DockPosition::Right, cx)); + workspace.read_with(cx, |workspace, _| { + assert_eq!(workspace.zoomed, Some(panel_1.downgrade().into_any())); + assert_eq!(workspace.zoomed_position, Some(DockPosition::Right)); }); // If focus is transferred to another view that's not a panel or another pane, we still show @@ -4328,12 +4654,14 @@ mod tests { focus_receiver.update(cx, |_, cx| cx.focus_self()); workspace.read_with(cx, |workspace, _| { assert_eq!(workspace.zoomed, Some(panel_1.downgrade().into_any())); + assert_eq!(workspace.zoomed_position, Some(DockPosition::Right)); }); // If focus is transferred elsewhere in the workspace, the panel is no longer zoomed. workspace.update(cx, |_, cx| cx.focus_self()); workspace.read_with(cx, |workspace, _| { assert_eq!(workspace.zoomed, None); + assert_eq!(workspace.zoomed_position, None); }); // If focus is transferred again to another view that's not a panel or a pane, we won't @@ -4341,18 +4669,21 @@ mod tests { focus_receiver.update(cx, |_, cx| cx.focus_self()); workspace.read_with(cx, |workspace, _| { assert_eq!(workspace.zoomed, None); + assert_eq!(workspace.zoomed_position, None); }); // When focus is transferred back to the panel, it is zoomed again. panel_1.update(cx, |_, cx| cx.focus_self()); workspace.read_with(cx, |workspace, _| { assert_eq!(workspace.zoomed, Some(panel_1.downgrade().into_any())); + assert_eq!(workspace.zoomed_position, Some(DockPosition::Right)); }); // Emitting a ZoomOut event unzooms the panel. panel_1.update(cx, |_, cx| cx.emit(TestPanelEvent::ZoomOut)); workspace.read_with(cx, |workspace, _| { assert_eq!(workspace.zoomed, None); + assert_eq!(workspace.zoomed_position, None); }); // Emit closed event on panel 1, which is active @@ -4360,8 +4691,8 @@ mod tests { // Now the left dock is closed, because panel_1 was the active panel workspace.read_with(cx, |workspace, cx| { - let left_dock = workspace.left_dock(); - assert!(!left_dock.read(cx).is_open()); + let right_dock = workspace.right_dock(); + assert!(!right_dock.read(cx).is_open()); }); } diff --git a/crates/zed/Cargo.toml b/crates/zed/Cargo.toml index e96dff1221..c45991cf11 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.89.0" +version = "0.90.0" publish = false [lib] diff --git a/crates/zed/src/languages/json.rs b/crates/zed/src/languages/json.rs index 406d54cc03..3947c62a6d 100644 --- a/crates/zed/src/languages/json.rs +++ b/crates/zed/src/languages/json.rs @@ -135,7 +135,10 @@ impl LspAdapter for JsonLspAdapter { }, "schemas": [ { - "fileMatch": [schema_file_match(&paths::SETTINGS)], + "fileMatch": [ + schema_file_match(&paths::SETTINGS), + &*paths::LOCAL_SETTINGS_RELATIVE_PATH, + ], "schema": settings_schema, }, { diff --git a/crates/zed/src/languages/yaml.rs b/crates/zed/src/languages/yaml.rs index bd5f2b4c02..7f87a7caed 100644 --- a/crates/zed/src/languages/yaml.rs +++ b/crates/zed/src/languages/yaml.rs @@ -3,7 +3,7 @@ use async_trait::async_trait; use futures::{future::BoxFuture, FutureExt, StreamExt}; use gpui::AppContext; use language::{ - language_settings::language_settings, LanguageServerBinary, LanguageServerName, LspAdapter, + language_settings::all_language_settings, LanguageServerBinary, LanguageServerName, LspAdapter, }; use node_runtime::NodeRuntime; use serde_json::Value; @@ -101,13 +101,16 @@ impl LspAdapter for YamlLspAdapter { } fn workspace_configuration(&self, cx: &mut AppContext) -> Option> { + let tab_size = all_language_settings(None, cx) + .language(Some("YAML")) + .tab_size; Some( future::ready(serde_json::json!({ "yaml": { "keyOrdering": false }, "[yaml]": { - "editor.tabSize": language_settings(Some("YAML"), cx).tab_size, + "editor.tabSize": tab_size, } })) .boxed(), diff --git a/crates/zed/src/main.rs b/crates/zed/src/main.rs index 31f331ef93..9ec3e3d3f6 100644 --- a/crates/zed/src/main.rs +++ b/crates/zed/src/main.rs @@ -41,7 +41,7 @@ use std::{ Arc, Weak, }, thread, - time::Duration, + time::{Duration, SystemTime, UNIX_EPOCH}, }; use sum_tree::Bias; use terminal_view::{get_working_directory, TerminalSettings, TerminalView}; @@ -376,6 +376,7 @@ struct Panic { backtrace: Vec, // TODO // stripped_backtrace: String, + time: u128, } #[derive(Serialize)] @@ -413,6 +414,10 @@ fn init_panic_hook(app_version: String) { .map(|line| line.to_string()) .collect(), // modified_backtrace: None, + time: SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_millis(), }; if let Some(panic_data_json) = serde_json::to_string_pretty(&panic_data).log_err() { diff --git a/crates/zed/src/menus.rs b/crates/zed/src/menus.rs index adc1f81589..76e88325f5 100644 --- a/crates/zed/src/menus.rs +++ b/crates/zed/src/menus.rs @@ -89,18 +89,9 @@ pub fn menus() -> Vec> { MenuItem::action("Zoom Out", super::DecreaseBufferFontSize), MenuItem::action("Reset Zoom", super::ResetBufferFontSize), MenuItem::separator(), - MenuItem::action( - "Toggle Left Dock", - workspace::ToggleLeftDock { focus: false }, - ), - MenuItem::action( - "Toggle Right Dock", - workspace::ToggleRightDock { focus: false }, - ), - MenuItem::action( - "Toggle Bottom Dock", - workspace::ToggleBottomDock { focus: false }, - ), + MenuItem::action("Toggle Left Dock", workspace::ToggleLeftDock), + MenuItem::action("Toggle Right Dock", workspace::ToggleRightDock), + MenuItem::action("Toggle Bottom Dock", workspace::ToggleBottomDock), MenuItem::submenu(Menu { name: "Editor Layout", items: vec![ diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index dffdccdfe5..8379251485 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -357,7 +357,7 @@ pub fn initialize_workspace( .map_or(false, |entry| entry.is_dir()) }) { - workspace.toggle_dock(project_panel_position, false, cx); + workspace.toggle_dock(project_panel_position, cx); } workspace.add_panel(terminal_panel, cx); diff --git a/styles/src/buildLicenses.ts b/styles/src/buildLicenses.ts index 3367e50ff0..9a053e9c0c 100644 --- a/styles/src/buildLicenses.ts +++ b/styles/src/buildLicenses.ts @@ -1,11 +1,9 @@ import * as fs from "fs" import toml from "toml" import { schemeMeta } from "./colorSchemes" -import { Meta, Verification } from "./themes/common/colorScheme" -import https from "https" -import crypto from "crypto" +import { MetaAndLicense } from "./themes/common/colorScheme" -const accepted_licenses_file = `${__dirname}/../../script/licenses/zed-licenses.toml` +const ACCEPTED_LICENSES_FILE = `${__dirname}/../../script/licenses/zed-licenses.toml` // Use the cargo-about configuration file as the source of truth for supported licenses. function parseAcceptedToml(file: string): string[] { @@ -20,8 +18,11 @@ function parseAcceptedToml(file: string): string[] { return obj.accepted } -function checkLicenses(schemeMeta: Meta[], licenses: string[]) { - for (let meta of schemeMeta) { +function checkLicenses( + schemeMetaWithLicense: MetaAndLicense[], + licenses: string[] +) { + for (const { meta } of schemeMetaWithLicense) { // FIXME: Add support for conjuctions and conditions if (licenses.indexOf(meta.license.SPDX) < 0) { throw Error( @@ -31,62 +32,23 @@ function checkLicenses(schemeMeta: Meta[], licenses: string[]) { } } -function getLicenseText( - schemeMeta: Meta[], - callback: (meta: Meta, license_text: string) => void -) { - for (let meta of schemeMeta) { - if (typeof meta.license.license_text == "string") { - callback(meta, meta.license.license_text) - } else { - 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 - .get(license_text_obj.https_url, (res) => { - const { statusCode } = res - - if (statusCode < 200 || statusCode >= 300) { - throw new Error( - `Failed to fetch license for: ${meta.name}, Status Code: ${statusCode}` - ) - } - - res.setEncoding("utf8") - let rawData = "" - res.on("data", (chunk) => { - rawData += chunk - }) - res.on("end", () => { - const hash = crypto - .createHash("sha256") - .update(rawData) - .digest("hex") - if (license_text_obj.license_checksum == hash) { - callback(meta, rawData) - } else { - throw Error( - `Checksum for ${meta.name} did not match file downloaded from ${license_text_obj.https_url}` - ) - } - }) - }) - .on("error", (e) => { - throw e - }) - } +function generateLicenseFile(schemeMetaWithLicense: MetaAndLicense[]) { + for (const { meta, licenseFile } of schemeMetaWithLicense) { + const licenseText = fs.readFileSync(licenseFile).toString() + writeLicense(meta.name, meta.url, licenseText) } } -function writeLicense(schemeMeta: Meta, text: String) { +function writeLicense( + themeName: string, + themeUrl: string, + licenseText: String +) { process.stdout.write( - `## [${schemeMeta.name}](${schemeMeta.url})\n\n${text}\n********************************************************************************\n\n` + `## [${themeName}](${themeUrl})\n\n${licenseText}\n********************************************************************************\n\n` ) } -const accepted_licenses = parseAcceptedToml(accepted_licenses_file) -checkLicenses(schemeMeta, accepted_licenses) - -getLicenseText(schemeMeta, (meta, text) => { - writeLicense(meta, text) -}) +const acceptedLicenses = parseAcceptedToml(ACCEPTED_LICENSES_FILE) +checkLicenses(schemeMeta, acceptedLicenses) +generateLicenseFile(schemeMeta) diff --git a/styles/src/buildThemes.ts b/styles/src/buildThemes.ts index 2a63a407cc..4353765573 100644 --- a/styles/src/buildThemes.ts +++ b/styles/src/buildThemes.ts @@ -1,7 +1,7 @@ import * as fs from "fs" import { tmpdir } from "os" import * as path from "path" -import colorSchemes, { staffColorSchemes } from "./colorSchemes" +import { colorSchemes, staffColorSchemes } from "./colorSchemes" import app from "./styleTree/app" import { ColorScheme } from "./themes/common/colorScheme" import snakeCase from "./utils/snakeCase" diff --git a/styles/src/colorSchemes.ts b/styles/src/colorSchemes.ts index 4d2d7f6e02..74f1a2a03c 100644 --- a/styles/src/colorSchemes.ts +++ b/styles/src/colorSchemes.ts @@ -1,54 +1,79 @@ import fs from "fs" import path from "path" -import { ColorScheme, Meta } from "./themes/common/colorScheme" +import { ColorScheme, MetaAndLicense } from "./themes/common/colorScheme" -const colorSchemes: ColorScheme[] = [] -export default colorSchemes +const THEMES_DIRECTORY = path.resolve(`${__dirname}/themes`) +const STAFF_DIRECTORY = path.resolve(`${__dirname}/themes/staff`) +const IGNORE_ITEMS = ["staff", "common", "common.ts"] +const ACCEPT_EXTENSION = ".ts" +const LICENSE_FILE_NAME = "LICENSE" -const schemeMeta: Meta[] = [] -export { schemeMeta } +function getAllTsFiles(directoryPath: string) { + const files = fs.readdirSync(directoryPath) + const fileList: string[] = [] -const staffColorSchemes: ColorScheme[] = [] -export { staffColorSchemes } + for (const file of files) { + if (!IGNORE_ITEMS.includes(file)) { + const filePath = path.join(directoryPath, file) -const experimentalColorSchemes: ColorScheme[] = [] -export { experimentalColorSchemes } - -const themes_directory = path.resolve(`${__dirname}/themes`) - -function for_all_color_schemes_in( - themesPath: string, - callback: (module: any, path: string) => void -) { - for (const fileName of fs.readdirSync(themesPath)) { - if (fileName == "template.ts") continue - const filePath = path.join(themesPath, fileName) - - if (fs.statSync(filePath).isFile()) { - const colorScheme = require(filePath) - callback(colorScheme, path.basename(filePath)) + if (fs.statSync(filePath).isDirectory()) { + fileList.push(...getAllTsFiles(filePath)) + } else if (path.extname(file) === ACCEPT_EXTENSION) { + fileList.push(filePath) + } } } + + return fileList } -function fillColorSchemes(themesPath: string, colorSchemes: ColorScheme[]) { - for_all_color_schemes_in(themesPath, (colorScheme, _path) => { +function getAllColorSchemes(directoryPath: string) { + const files = getAllTsFiles(directoryPath) + return files.map((filePath) => ({ + colorScheme: require(filePath), + filePath, + fileName: path.basename(filePath), + licenseFile: `${path.dirname(filePath)}/${LICENSE_FILE_NAME}`, + })) +} + +function getColorSchemes(directoryPath: string) { + const colorSchemes: ColorScheme[] = [] + + for (const { colorScheme } of getAllColorSchemes(directoryPath)) { if (colorScheme.dark) colorSchemes.push(colorScheme.dark) - if (colorScheme.light) colorSchemes.push(colorScheme.light) - }) + else if (colorScheme.light) colorSchemes.push(colorScheme.light) + } + + return colorSchemes } -fillColorSchemes(themes_directory, colorSchemes) -fillColorSchemes(path.resolve(`${themes_directory}/staff`), staffColorSchemes) +function getMetaAndLicense(directoryPath: string) { + const meta: MetaAndLicense[] = [] -function fillMeta(themesPath: string, meta: Meta[]) { - for_all_color_schemes_in(themesPath, (colorScheme, path) => { - if (colorScheme.meta) { - meta.push(colorScheme.meta) - } else { - throw Error(`Public theme ${path} must have a meta field`) + for (const { colorScheme, filePath, licenseFile } of getAllColorSchemes( + directoryPath + )) { + const licenseExists = fs.existsSync(licenseFile) + if (!licenseExists) { + throw Error( + `Public theme should have a LICENSE file ${licenseFile}` + ) } - }) + + if (!colorScheme.meta) { + throw Error(`Public theme ${filePath} must have a meta field`) + } + + meta.push({ + meta: colorScheme.meta, + licenseFile, + }) + } + + return meta } -fillMeta(themes_directory, schemeMeta) +export const colorSchemes = getColorSchemes(THEMES_DIRECTORY) +export const staffColorSchemes = getColorSchemes(STAFF_DIRECTORY) +export const schemeMeta = getMetaAndLicense(THEMES_DIRECTORY) diff --git a/styles/src/styleTree/tabBar.ts b/styles/src/styleTree/tabBar.ts index 61d30be7e0..39a1ef0407 100644 --- a/styles/src/styleTree/tabBar.ts +++ b/styles/src/styleTree/tabBar.ts @@ -94,6 +94,9 @@ export default function tabBar(colorScheme: ColorScheme) { hover: { color: foreground(layer, "hovered"), }, + active: { + color: foreground(layer, "accent"), + } }, paneButtonContainer: { background: tab.background, diff --git a/styles/src/styleTree/workspace.ts b/styles/src/styleTree/workspace.ts index 737d225784..ec992c9c0b 100644 --- a/styles/src/styleTree/workspace.ts +++ b/styles/src/styleTree/workspace.ts @@ -13,6 +13,7 @@ import tabBar from "./tabBar" export default function workspace(colorScheme: ColorScheme) { const layer = colorScheme.lowest + const isLight = colorScheme.isLight const itemSpacing = 8 const titlebarButton = { cornerRadius: 6, @@ -119,13 +120,19 @@ export default function workspace(colorScheme: ColorScheme) { cursor: "Arrow", }, zoomedBackground: { - padding: 10, cursor: "Arrow", - background: withOpacity(background(colorScheme.lowest), 0.5) + background: isLight + ? withOpacity(background(colorScheme.lowest), 0.8) + : withOpacity(background(colorScheme.highest), 0.6) }, - zoomedForeground: { + zoomedPaneForeground: { + margin: 16, shadow: colorScheme.modalShadow, - border: border(colorScheme.highest, { overlay: true }), + border: border(colorScheme.lowest, { overlay: true }), + }, + zoomedPanelForeground: { + margin: 16, + border: border(colorScheme.lowest, { overlay: true }), }, dock: { left: { diff --git a/styles/src/themes/andromeda/LICENSE b/styles/src/themes/andromeda/LICENSE new file mode 100644 index 0000000000..bdd549491f --- /dev/null +++ b/styles/src/themes/andromeda/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2017 + +Permission 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: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE 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. \ No newline at end of file diff --git a/styles/src/themes/andromeda.ts b/styles/src/themes/andromeda/andromeda.ts similarity index 71% rename from styles/src/themes/andromeda.ts rename to styles/src/themes/andromeda/andromeda.ts index 7eba7b1481..d7f7f53b90 100644 --- a/styles/src/themes/andromeda.ts +++ b/styles/src/themes/andromeda/andromeda.ts @@ -1,6 +1,6 @@ import chroma from "chroma-js" -import { Meta } from "./common/colorScheme" -import { colorRamp, createColorScheme } from "./common/ramps" +import { Meta } from "../common/colorScheme" +import { colorRamp, createColorScheme } from "../common/ramps" const name = "Andromeda" @@ -34,12 +34,6 @@ export const meta: Meta = { author: "EliverLara", license: { SPDX: "MIT", - license_text: { - https_url: - "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/LICENSE b/styles/src/themes/atelier/LICENSE new file mode 100644 index 0000000000..9f92967a04 --- /dev/null +++ b/styles/src/themes/atelier/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2023 Bram de Haan, http://atelierbramdehaan.nl + +Permission 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: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE 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. \ No newline at end of file diff --git a/styles/src/themes/atelier-cave-dark.ts b/styles/src/themes/atelier/atelier-cave-dark.ts similarity index 93% rename from styles/src/themes/atelier-cave-dark.ts rename to styles/src/themes/atelier/atelier-cave-dark.ts index a56e22cd92..41ee6a745c 100644 --- a/styles/src/themes/atelier-cave-dark.ts +++ b/styles/src/themes/atelier/atelier-cave-dark.ts @@ -1,7 +1,7 @@ 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" +import { Meta } from "../common/colorScheme" +import { colorRamp, createColorScheme } from "../common/ramps" +import { metaCommon, name, buildSyntax, Variant } from "./common" const variant: Variant = { meta: { diff --git a/styles/src/themes/atelier-cave-light.ts b/styles/src/themes/atelier/atelier-cave-light.ts similarity index 93% rename from styles/src/themes/atelier-cave-light.ts rename to styles/src/themes/atelier/atelier-cave-light.ts index 3b180752cf..9e0473bbfa 100644 --- a/styles/src/themes/atelier-cave-light.ts +++ b/styles/src/themes/atelier/atelier-cave-light.ts @@ -1,7 +1,7 @@ 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" +import { Meta } from "../common/colorScheme" +import { colorRamp, createColorScheme } from "../common/ramps" +import { metaCommon, name, buildSyntax, Variant } from "./common" const variant: Variant = { meta: { diff --git a/styles/src/themes/atelier-dune-dark.ts b/styles/src/themes/atelier/atelier-dune-dark.ts similarity index 93% rename from styles/src/themes/atelier-dune-dark.ts rename to styles/src/themes/atelier/atelier-dune-dark.ts index 0ab402a99d..c21ee61a60 100644 --- a/styles/src/themes/atelier-dune-dark.ts +++ b/styles/src/themes/atelier/atelier-dune-dark.ts @@ -1,7 +1,7 @@ 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" +import { Meta } from "../common/colorScheme" +import { colorRamp, createColorScheme } from "../common/ramps" +import { metaCommon, name, buildSyntax, Variant } from "./common" const variant: Variant = { meta: { diff --git a/styles/src/themes/atelier-dune-light.ts b/styles/src/themes/atelier/atelier-dune-light.ts similarity index 93% rename from styles/src/themes/atelier-dune-light.ts rename to styles/src/themes/atelier/atelier-dune-light.ts index e6a09985d5..956ae72c60 100644 --- a/styles/src/themes/atelier-dune-light.ts +++ b/styles/src/themes/atelier/atelier-dune-light.ts @@ -1,7 +1,7 @@ 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" +import { Meta } from "../common/colorScheme" +import { colorRamp, createColorScheme } from "../common/ramps" +import { metaCommon, name, buildSyntax, Variant } from "./common" const variant: Variant = { meta: { diff --git a/styles/src/themes/atelier-estuary-dark.ts b/styles/src/themes/atelier/atelier-estuary-dark.ts similarity index 93% rename from styles/src/themes/atelier-estuary-dark.ts rename to styles/src/themes/atelier/atelier-estuary-dark.ts index c4ec50c1e0..b9f1880d68 100644 --- a/styles/src/themes/atelier-estuary-dark.ts +++ b/styles/src/themes/atelier/atelier-estuary-dark.ts @@ -1,7 +1,7 @@ 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" +import { Meta } from "../common/colorScheme" +import { colorRamp, createColorScheme } from "../common/ramps" +import { metaCommon, name, buildSyntax, Variant } from "./common" const variant: Variant = { meta: { diff --git a/styles/src/themes/atelier-estuary-light.ts b/styles/src/themes/atelier/atelier-estuary-light.ts similarity index 93% rename from styles/src/themes/atelier-estuary-light.ts rename to styles/src/themes/atelier/atelier-estuary-light.ts index 6fce0e4483..a9eeb612ed 100644 --- a/styles/src/themes/atelier-estuary-light.ts +++ b/styles/src/themes/atelier/atelier-estuary-light.ts @@ -1,7 +1,7 @@ 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" +import { Meta } from "../common/colorScheme" +import { colorRamp, createColorScheme } from "../common/ramps" +import { metaCommon, name, buildSyntax, Variant } from "./common" const variant: Variant = { meta: { diff --git a/styles/src/themes/atelier-forest-dark.ts b/styles/src/themes/atelier/atelier-forest-dark.ts similarity index 93% rename from styles/src/themes/atelier-forest-dark.ts rename to styles/src/themes/atelier/atelier-forest-dark.ts index 7c47c55a83..352f1ea43e 100644 --- a/styles/src/themes/atelier-forest-dark.ts +++ b/styles/src/themes/atelier/atelier-forest-dark.ts @@ -1,7 +1,7 @@ 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" +import { Meta } from "../common/colorScheme" +import { colorRamp, createColorScheme } from "../common/ramps" +import { metaCommon, name, buildSyntax, Variant } from "./common" const variant: Variant = { meta: { diff --git a/styles/src/themes/atelier-forest-light.ts b/styles/src/themes/atelier/atelier-forest-light.ts similarity index 93% rename from styles/src/themes/atelier-forest-light.ts rename to styles/src/themes/atelier/atelier-forest-light.ts index 8ce0616476..1378c9b061 100644 --- a/styles/src/themes/atelier-forest-light.ts +++ b/styles/src/themes/atelier/atelier-forest-light.ts @@ -1,7 +1,7 @@ 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" +import { Meta } from "../common/colorScheme" +import { colorRamp, createColorScheme } from "../common/ramps" +import { metaCommon, name, buildSyntax, Variant } from "./common" const variant: Variant = { meta: { diff --git a/styles/src/themes/atelier-heath-dark.ts b/styles/src/themes/atelier/atelier-heath-dark.ts similarity index 93% rename from styles/src/themes/atelier-heath-dark.ts rename to styles/src/themes/atelier/atelier-heath-dark.ts index 87458ab8f5..4c35194422 100644 --- a/styles/src/themes/atelier-heath-dark.ts +++ b/styles/src/themes/atelier/atelier-heath-dark.ts @@ -1,7 +1,7 @@ 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" +import { Meta } from "../common/colorScheme" +import { colorRamp, createColorScheme } from "../common/ramps" +import { metaCommon, name, buildSyntax, Variant } from "./common" const variant: Variant = { meta: { diff --git a/styles/src/themes/atelier-heath-light.ts b/styles/src/themes/atelier/atelier-heath-light.ts similarity index 93% rename from styles/src/themes/atelier-heath-light.ts rename to styles/src/themes/atelier/atelier-heath-light.ts index 3db3437041..51b9ef9398 100644 --- a/styles/src/themes/atelier-heath-light.ts +++ b/styles/src/themes/atelier/atelier-heath-light.ts @@ -1,7 +1,7 @@ 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" +import { Meta } from "../common/colorScheme" +import { colorRamp, createColorScheme } from "../common/ramps" +import { metaCommon, name, buildSyntax, Variant } from "./common" const variant: Variant = { meta: { diff --git a/styles/src/themes/atelier-lakeside-dark.ts b/styles/src/themes/atelier/atelier-lakeside-dark.ts similarity index 93% rename from styles/src/themes/atelier-lakeside-dark.ts rename to styles/src/themes/atelier/atelier-lakeside-dark.ts index a8297ef9fd..ece9179860 100644 --- a/styles/src/themes/atelier-lakeside-dark.ts +++ b/styles/src/themes/atelier/atelier-lakeside-dark.ts @@ -1,7 +1,7 @@ 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" +import { Meta } from "../common/colorScheme" +import { colorRamp, createColorScheme } from "../common/ramps" +import { metaCommon, name, buildSyntax, Variant } from "./common" const variant: Variant = { meta: { diff --git a/styles/src/themes/atelier-lakeside-light.ts b/styles/src/themes/atelier/atelier-lakeside-light.ts similarity index 93% rename from styles/src/themes/atelier-lakeside-light.ts rename to styles/src/themes/atelier/atelier-lakeside-light.ts index 408fabcaf3..fd265d10c8 100644 --- a/styles/src/themes/atelier-lakeside-light.ts +++ b/styles/src/themes/atelier/atelier-lakeside-light.ts @@ -1,7 +1,7 @@ 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" +import { Meta } from "../common/colorScheme" +import { colorRamp, createColorScheme } from "../common/ramps" +import { metaCommon, name, buildSyntax, Variant } from "./common" const variant: Variant = { meta: { diff --git a/styles/src/themes/atelier-plateau-dark.ts b/styles/src/themes/atelier/atelier-plateau-dark.ts similarity index 93% rename from styles/src/themes/atelier-plateau-dark.ts rename to styles/src/themes/atelier/atelier-plateau-dark.ts index 731cb824e4..ef64782bda 100644 --- a/styles/src/themes/atelier-plateau-dark.ts +++ b/styles/src/themes/atelier/atelier-plateau-dark.ts @@ -1,7 +1,7 @@ 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" +import { Meta } from "../common/colorScheme" +import { colorRamp, createColorScheme } from "../common/ramps" +import { metaCommon, name, buildSyntax, Variant } from "./common" const variant: Variant = { meta: { diff --git a/styles/src/themes/atelier-plateau-light.ts b/styles/src/themes/atelier/atelier-plateau-light.ts similarity index 93% rename from styles/src/themes/atelier-plateau-light.ts rename to styles/src/themes/atelier/atelier-plateau-light.ts index 96f295a695..9fe51f5b79 100644 --- a/styles/src/themes/atelier-plateau-light.ts +++ b/styles/src/themes/atelier/atelier-plateau-light.ts @@ -1,7 +1,7 @@ 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" +import { Meta } from "../common/colorScheme" +import { colorRamp, createColorScheme } from "../common/ramps" +import { metaCommon, name, buildSyntax, Variant } from "./common" const variant: Variant = { meta: { diff --git a/styles/src/themes/atelier-savanna-dark.ts b/styles/src/themes/atelier/atelier-savanna-dark.ts similarity index 93% rename from styles/src/themes/atelier-savanna-dark.ts rename to styles/src/themes/atelier/atelier-savanna-dark.ts index dfcb4f27cb..36de9b817f 100644 --- a/styles/src/themes/atelier-savanna-dark.ts +++ b/styles/src/themes/atelier/atelier-savanna-dark.ts @@ -1,7 +1,7 @@ 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" +import { Meta } from "../common/colorScheme" +import { colorRamp, createColorScheme } from "../common/ramps" +import { metaCommon, name, buildSyntax, Variant } from "./common" const variant: Variant = { meta: { diff --git a/styles/src/themes/atelier-savanna-light.ts b/styles/src/themes/atelier/atelier-savanna-light.ts similarity index 93% rename from styles/src/themes/atelier-savanna-light.ts rename to styles/src/themes/atelier/atelier-savanna-light.ts index 4bc1389fc9..d5d9cb369d 100644 --- a/styles/src/themes/atelier-savanna-light.ts +++ b/styles/src/themes/atelier/atelier-savanna-light.ts @@ -1,7 +1,7 @@ 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" +import { Meta } from "../common/colorScheme" +import { colorRamp, createColorScheme } from "../common/ramps" +import { metaCommon, name, buildSyntax, Variant } from "./common" const variant: Variant = { meta: { diff --git a/styles/src/themes/atelier-seaside-dark.ts b/styles/src/themes/atelier/atelier-seaside-dark.ts similarity index 93% rename from styles/src/themes/atelier-seaside-dark.ts rename to styles/src/themes/atelier/atelier-seaside-dark.ts index 1326a27786..f7c49ad71e 100644 --- a/styles/src/themes/atelier-seaside-dark.ts +++ b/styles/src/themes/atelier/atelier-seaside-dark.ts @@ -1,7 +1,7 @@ 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" +import { Meta } from "../common/colorScheme" +import { colorRamp, createColorScheme } from "../common/ramps" +import { metaCommon, name, buildSyntax, Variant } from "./common" const variant: Variant = { meta: { diff --git a/styles/src/themes/atelier-seaside-light.ts b/styles/src/themes/atelier/atelier-seaside-light.ts similarity index 93% rename from styles/src/themes/atelier-seaside-light.ts rename to styles/src/themes/atelier/atelier-seaside-light.ts index 6f6823718a..1cf6461446 100644 --- a/styles/src/themes/atelier-seaside-light.ts +++ b/styles/src/themes/atelier/atelier-seaside-light.ts @@ -1,7 +1,7 @@ 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" +import { Meta } from "../common/colorScheme" +import { colorRamp, createColorScheme } from "../common/ramps" +import { metaCommon, name, buildSyntax, Variant } from "./common" const variant: Variant = { meta: { diff --git a/styles/src/themes/atelier-sulphurpool-dark.ts b/styles/src/themes/atelier/atelier-sulphurpool-dark.ts similarity index 93% rename from styles/src/themes/atelier-sulphurpool-dark.ts rename to styles/src/themes/atelier/atelier-sulphurpool-dark.ts index dcfc0d932a..b4a4e2a651 100644 --- a/styles/src/themes/atelier-sulphurpool-dark.ts +++ b/styles/src/themes/atelier/atelier-sulphurpool-dark.ts @@ -1,7 +1,7 @@ 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" +import { Meta } from "../common/colorScheme" +import { colorRamp, createColorScheme } from "../common/ramps" +import { metaCommon, name, buildSyntax, Variant } from "./common" const variant: Variant = { meta: { diff --git a/styles/src/themes/atelier-sulphurpool-light.ts b/styles/src/themes/atelier/atelier-sulphurpool-light.ts similarity index 93% rename from styles/src/themes/atelier-sulphurpool-light.ts rename to styles/src/themes/atelier/atelier-sulphurpool-light.ts index b2b5f7c328..046adbdf43 100644 --- a/styles/src/themes/atelier-sulphurpool-light.ts +++ b/styles/src/themes/atelier/atelier-sulphurpool-light.ts @@ -1,7 +1,7 @@ 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" +import { Meta } from "../common/colorScheme" +import { colorRamp, createColorScheme } from "../common/ramps" +import { metaCommon, name, buildSyntax, Variant } from "./common" const variant: Variant = { meta: { diff --git a/styles/src/themes/common/atelier-common.ts b/styles/src/themes/atelier/common.ts similarity index 86% rename from styles/src/themes/common/atelier-common.ts rename to styles/src/themes/atelier/common.ts index 08a915d019..746834c88b 100644 --- a/styles/src/themes/common/atelier-common.ts +++ b/styles/src/themes/atelier/common.ts @@ -1,4 +1,4 @@ -import { License, Meta, ThemeSyntax } from "./colorScheme" +import { License, Meta, ThemeSyntax } from "../common/colorScheme" export interface Variant { meta: Meta @@ -29,11 +29,6 @@ export const metaCommon: { author: "Bram de Haan (http://atelierbramdehaan.nl)", license: { SPDX: "MIT", - license_text: { - https_url: "https://atelierbram.mit-license.org/license.txt", - license_checksum: - "f95ce526ef4e7eecf7a832bba0e3451cc1000f9ce63eb01ed6f64f8109f5d0a5", - }, }, } diff --git a/styles/src/themes/ayu/LICENSE b/styles/src/themes/ayu/LICENSE new file mode 100644 index 0000000000..6b83ef0582 --- /dev/null +++ b/styles/src/themes/ayu/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2016 Ike Ku + +Permission 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: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE 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. \ No newline at end of file diff --git a/styles/src/themes/ayu-dark.ts b/styles/src/themes/ayu/ayu-dark.ts similarity index 66% rename from styles/src/themes/ayu-dark.ts rename to styles/src/themes/ayu/ayu-dark.ts index c7e86994fe..1dc663f161 100644 --- a/styles/src/themes/ayu-dark.ts +++ b/styles/src/themes/ayu/ayu-dark.ts @@ -1,5 +1,5 @@ -import { createColorScheme } from "./common/ramps" -import { ayu, meta as themeMeta, buildTheme } from "./common/ayu-common" +import { createColorScheme } from "../common/ramps" +import { ayu, meta as themeMeta, buildTheme } from "./common" export const meta = { ...themeMeta, diff --git a/styles/src/themes/ayu-light.ts b/styles/src/themes/ayu/ayu-light.ts similarity index 66% rename from styles/src/themes/ayu-light.ts rename to styles/src/themes/ayu/ayu-light.ts index 9acabf6a39..2543521944 100644 --- a/styles/src/themes/ayu-light.ts +++ b/styles/src/themes/ayu/ayu-light.ts @@ -1,5 +1,5 @@ -import { createColorScheme } from "./common/ramps" -import { ayu, meta as themeMeta, buildTheme } from "./common/ayu-common" +import { createColorScheme } from "../common/ramps" +import { ayu, meta as themeMeta, buildTheme } from "./common" export const meta = { ...themeMeta, diff --git a/styles/src/themes/ayu-mirage.ts b/styles/src/themes/ayu/ayu-mirage.ts similarity index 66% rename from styles/src/themes/ayu-mirage.ts rename to styles/src/themes/ayu/ayu-mirage.ts index 2a01512673..2ada3678ee 100644 --- a/styles/src/themes/ayu-mirage.ts +++ b/styles/src/themes/ayu/ayu-mirage.ts @@ -1,5 +1,5 @@ -import { createColorScheme } from "./common/ramps" -import { ayu, meta as themeMeta, buildTheme } from "./common/ayu-common" +import { createColorScheme } from "../common/ramps" +import { ayu, meta as themeMeta, buildTheme } from "./common" export const meta = { ...themeMeta, diff --git a/styles/src/themes/common/ayu-common.ts b/styles/src/themes/ayu/common.ts similarity index 88% rename from styles/src/themes/common/ayu-common.ts rename to styles/src/themes/ayu/common.ts index f08817ef49..e586e2d22d 100644 --- a/styles/src/themes/common/ayu-common.ts +++ b/styles/src/themes/ayu/common.ts @@ -1,8 +1,8 @@ import { dark, light, mirage } from "ayu" -import { ThemeSyntax } from "./syntax" +import { ThemeSyntax } from "../common/syntax" import chroma from "chroma-js" -import { colorRamp } from "./ramps" -import { Meta } from "./colorScheme" +import { colorRamp } from "../common/ramps" +import { Meta } from "../common/colorScheme" export const ayu = { dark, @@ -79,12 +79,6 @@ export const meta: Meta = { author: "dempfi", license: { SPDX: "MIT", - license_text: { - https_url: - "https://raw.githubusercontent.com/dempfi/ayu/master/LICENSE", - license_checksum: - "e0af0e0d1754c18ca075649d42f5c6d9a60f8bdc03c20dfd97105f2253a94173", - }, }, url: "https://github.com/dempfi/ayu", } diff --git a/styles/src/themes/common/colorScheme.ts b/styles/src/themes/common/colorScheme.ts index 3b5b8f6932..5cf125ae8f 100644 --- a/styles/src/themes/common/colorScheme.ts +++ b/styles/src/themes/common/colorScheme.ts @@ -19,6 +19,11 @@ export interface ColorScheme { syntax?: Partial } +export interface MetaAndLicense { + meta: Meta + licenseFile: string +} + export interface Meta { name: string author: string @@ -28,13 +33,6 @@ export interface Meta { export interface License { SPDX: SPDXExpression - /// A url where we can download the license's text - license_text: Verification | string -} - -export interface Verification { - https_url: string - license_checksum: string } // License name -> License text diff --git a/styles/src/themes/gruvbox/LICENSE b/styles/src/themes/gruvbox/LICENSE new file mode 100644 index 0000000000..2a92306143 --- /dev/null +++ b/styles/src/themes/gruvbox/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) + +Permission 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: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE 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. \ No newline at end of file diff --git a/styles/src/themes/gruvbox-common.ts b/styles/src/themes/gruvbox/gruvbox-common.ts similarity index 83% rename from styles/src/themes/gruvbox-common.ts rename to styles/src/themes/gruvbox/gruvbox-common.ts index c42362c11c..b113ce68c6 100644 --- a/styles/src/themes/gruvbox-common.ts +++ b/styles/src/themes/gruvbox/gruvbox-common.ts @@ -1,6 +1,6 @@ import chroma from "chroma-js" -import { Meta, ThemeSyntax } from "./common/colorScheme" -import { colorRamp, createColorScheme } from "./common/ramps" +import { Meta, ThemeSyntax } from "../common/colorScheme" +import { colorRamp, createColorScheme } from "../common/ramps" const name = "Gruvbox" @@ -248,8 +248,6 @@ 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.", }, author: "morhetz ", url: "https://github.com/morhetz/gruvbox", diff --git a/styles/src/themes/gruvbox-dark-hard.ts b/styles/src/themes/gruvbox/gruvbox-dark-hard.ts similarity index 100% rename from styles/src/themes/gruvbox-dark-hard.ts rename to styles/src/themes/gruvbox/gruvbox-dark-hard.ts diff --git a/styles/src/themes/gruvbox-dark-soft.ts b/styles/src/themes/gruvbox/gruvbox-dark-soft.ts similarity index 100% rename from styles/src/themes/gruvbox-dark-soft.ts rename to styles/src/themes/gruvbox/gruvbox-dark-soft.ts diff --git a/styles/src/themes/gruvbox-dark.ts b/styles/src/themes/gruvbox/gruvbox-dark.ts similarity index 100% rename from styles/src/themes/gruvbox-dark.ts rename to styles/src/themes/gruvbox/gruvbox-dark.ts diff --git a/styles/src/themes/gruvbox-light-hard.ts b/styles/src/themes/gruvbox/gruvbox-light-hard.ts similarity index 100% rename from styles/src/themes/gruvbox-light-hard.ts rename to styles/src/themes/gruvbox/gruvbox-light-hard.ts diff --git a/styles/src/themes/gruvbox-light-soft.ts b/styles/src/themes/gruvbox/gruvbox-light-soft.ts similarity index 100% rename from styles/src/themes/gruvbox-light-soft.ts rename to styles/src/themes/gruvbox/gruvbox-light-soft.ts diff --git a/styles/src/themes/gruvbox-light.ts b/styles/src/themes/gruvbox/gruvbox-light.ts similarity index 100% rename from styles/src/themes/gruvbox-light.ts rename to styles/src/themes/gruvbox/gruvbox-light.ts diff --git a/styles/src/themes/one/LICENSE b/styles/src/themes/one/LICENSE new file mode 100644 index 0000000000..dc07dc10ad --- /dev/null +++ b/styles/src/themes/one/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2014 GitHub Inc. + +Permission 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: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE 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. \ No newline at end of file diff --git a/styles/src/themes/one-dark.ts b/styles/src/themes/one/one-dark.ts similarity index 83% rename from styles/src/themes/one-dark.ts rename to styles/src/themes/one/one-dark.ts index 85417a0e68..1a88ddf7dc 100644 --- a/styles/src/themes/one-dark.ts +++ b/styles/src/themes/one/one-dark.ts @@ -1,7 +1,7 @@ import chroma from "chroma-js" -import { fontWeights } from "../common" -import { Meta, ThemeSyntax } from "./common/colorScheme" -import { colorRamp, createColorScheme } from "./common/ramps" +import { fontWeights } from "../../common" +import { Meta, ThemeSyntax } from "../common/colorScheme" +import { colorRamp, createColorScheme } from "../common/ramps" const name = "One Dark" @@ -74,12 +74,6 @@ export const meta: Meta = { author: "simurai", license: { SPDX: "MIT", - license_text: { - https_url: - "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/one-light.ts similarity index 82% rename from styles/src/themes/one-light.ts rename to styles/src/themes/one/one-light.ts index 7bf21aee17..231ee3abb3 100644 --- a/styles/src/themes/one-light.ts +++ b/styles/src/themes/one/one-light.ts @@ -1,7 +1,7 @@ import chroma from "chroma-js" -import { fontWeights } from "../common" -import { Meta, ThemeSyntax } from "./common/colorScheme" -import { colorRamp, createColorScheme } from "./common/ramps" +import { fontWeights } from "../../common" +import { Meta, ThemeSyntax } from "../common/colorScheme" +import { colorRamp, createColorScheme } from "../common/ramps" const name = "One Light" @@ -73,12 +73,6 @@ export const meta: Meta = { author: "simurai", license: { SPDX: "MIT", - license_text: { - https_url: - "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/LICENSE b/styles/src/themes/rose-pine/LICENSE new file mode 100644 index 0000000000..dfd60136f9 --- /dev/null +++ b/styles/src/themes/rose-pine/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2021 Emilia Dunfelt + +Permission 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: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE 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. \ No newline at end of file diff --git a/styles/src/themes/rose-pine-dawn.ts b/styles/src/themes/rose-pine/rose-pine-dawn.ts similarity index 71% rename from styles/src/themes/rose-pine-dawn.ts rename to styles/src/themes/rose-pine/rose-pine-dawn.ts index 427b05f72b..cf953872e1 100644 --- a/styles/src/themes/rose-pine-dawn.ts +++ b/styles/src/themes/rose-pine/rose-pine-dawn.ts @@ -1,6 +1,6 @@ import chroma from "chroma-js" -import { Meta } from "./common/colorScheme" -import { colorRamp, createColorScheme } from "./common/ramps" +import { Meta } from "../common/colorScheme" +import { colorRamp, createColorScheme } from "../common/ramps" const name = "Rosé Pine Dawn" @@ -34,12 +34,6 @@ export const meta: Meta = { author: "edunfelt", license: { SPDX: "MIT", - license_text: { - https_url: - "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/rose-pine-moon.ts similarity index 70% rename from styles/src/themes/rose-pine-moon.ts rename to styles/src/themes/rose-pine/rose-pine-moon.ts index be2f5a8daf..85ac5fd73e 100644 --- a/styles/src/themes/rose-pine-moon.ts +++ b/styles/src/themes/rose-pine/rose-pine-moon.ts @@ -1,6 +1,6 @@ import chroma from "chroma-js" -import { Meta } from "./common/colorScheme" -import { colorRamp, createColorScheme } from "./common/ramps" +import { Meta } from "../common/colorScheme" +import { colorRamp, createColorScheme } from "../common/ramps" const name = "Rosé Pine Moon" @@ -34,12 +34,6 @@ export const meta: Meta = { author: "edunfelt", license: { SPDX: "MIT", - license_text: { - https_url: - "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/rose-pine.ts similarity index 68% rename from styles/src/themes/rose-pine.ts rename to styles/src/themes/rose-pine/rose-pine.ts index 944550f125..fe2817be13 100644 --- a/styles/src/themes/rose-pine.ts +++ b/styles/src/themes/rose-pine/rose-pine.ts @@ -1,6 +1,6 @@ import chroma from "chroma-js" -import { Meta } from "./common/colorScheme" -import { colorRamp, createColorScheme } from "./common/ramps" +import { Meta } from "../common/colorScheme" +import { colorRamp, createColorScheme } from "../common/ramps" const name = "Rosé Pine" @@ -32,12 +32,6 @@ export const meta: Meta = { author: "edunfelt", license: { SPDX: "MIT", - license_text: { - https_url: - "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/LICENSE b/styles/src/themes/sandcastle/LICENSE new file mode 100644 index 0000000000..c66a06c51b --- /dev/null +++ b/styles/src/themes/sandcastle/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2019 George Essig + +Permission 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: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE 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. \ No newline at end of file diff --git a/styles/src/themes/sandcastle.ts b/styles/src/themes/sandcastle/sandcastle.ts similarity index 68% rename from styles/src/themes/sandcastle.ts rename to styles/src/themes/sandcastle/sandcastle.ts index 483f01b27a..2544b6399a 100644 --- a/styles/src/themes/sandcastle.ts +++ b/styles/src/themes/sandcastle/sandcastle.ts @@ -1,6 +1,6 @@ import chroma from "chroma-js" -import { Meta } from "./common/colorScheme" -import { colorRamp, createColorScheme } from "./common/ramps" +import { Meta } from "../common/colorScheme" +import { colorRamp, createColorScheme } from "../common/ramps" const name = "Sandcastle" @@ -32,12 +32,6 @@ export const meta: Meta = { author: "gessig", license: { SPDX: "MIT", - license_text: { - https_url: - "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/LICENSE b/styles/src/themes/solarized/LICENSE new file mode 100644 index 0000000000..221eee6f15 --- /dev/null +++ b/styles/src/themes/solarized/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2011 Ethan Schoonover + +Permission 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: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE 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. \ No newline at end of file diff --git a/styles/src/themes/solarized.ts b/styles/src/themes/solarized/solarized.ts similarity index 72% rename from styles/src/themes/solarized.ts rename to styles/src/themes/solarized/solarized.ts index 1210c43806..6c826fbee7 100644 --- a/styles/src/themes/solarized.ts +++ b/styles/src/themes/solarized/solarized.ts @@ -1,6 +1,6 @@ import chroma from "chroma-js" -import { Meta as Metadata } from "./common/colorScheme" -import { colorRamp, createColorScheme } from "./common/ramps" +import { Meta as Metadata } from "../common/colorScheme" +import { colorRamp, createColorScheme } from "../common/ramps" const name = "Solarized" @@ -35,12 +35,6 @@ export const meta: Metadata = { author: "Ethan Schoonover", license: { SPDX: "MIT", - license_text: { - https_url: - "https://raw.githubusercontent.com/altercation/solarized/master/LICENSE", - license_checksum: - "494aefdabf86acce06bd63001ad8aedad4ee38da23509d3f917d95aa3368b9a6", - }, }, url: "https://github.com/altercation/solarized", } diff --git a/styles/src/themes/summercamp/LICENSE b/styles/src/themes/summercamp/LICENSE new file mode 100644 index 0000000000..d7525414ad --- /dev/null +++ b/styles/src/themes/summercamp/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2019 Zoe FiriH + +Permission 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: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE 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. \ No newline at end of file diff --git a/styles/src/themes/summercamp.ts b/styles/src/themes/summercamp/summercamp.ts similarity index 71% rename from styles/src/themes/summercamp.ts rename to styles/src/themes/summercamp/summercamp.ts index 7df125e866..94c0072e4f 100644 --- a/styles/src/themes/summercamp.ts +++ b/styles/src/themes/summercamp/summercamp.ts @@ -1,6 +1,6 @@ import chroma from "chroma-js" -import { Meta } from "./common/colorScheme" -import { colorRamp, createColorScheme } from "./common/ramps" +import { Meta } from "../common/colorScheme" +import { colorRamp, createColorScheme } from "../common/ramps" const name = "Summercamp" @@ -34,11 +34,5 @@ export const meta: Meta = { url: "https://github.com/zoefiri/base16-sc", license: { SPDX: "MIT", - license_text: { - https_url: - "https://raw.githubusercontent.com/zoefiri/base16-sc/master/LICENSE", - license_checksum: - "fadcc834b7eaf2943800956600e8aeea4b495ecf6490f4c4b6c91556a90accaf", - }, }, }