Detailed changes
@@ -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) ... ([#<public_issue_number_if_exists>](https://github.com/zed-industries/community/issues/<public_issue_number_if_exists>)).
-* ...
+- (Added|Fixed|Improved) ... ([#<public_issue_number_if_exists>](https://github.com/zed-industries/community/issues/<public_issue_number_if_exists>)).
If the release notes are only intended for a specific release channel only, add `(<release_channel>-only)` to the end of the release note line.
These will be removed by the person making the release.
@@ -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",
@@ -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",
@@ -3,7 +3,7 @@ authors = ["Nathan Sobo <nathan@zed.dev>"]
default-run = "collab"
edition = "2021"
name = "collab"
-version = "0.12.4"
+version = "0.12.5"
publish = false
[[bin]]
@@ -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,
@@ -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");
@@ -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::<Vec<_>>();
+ {
+ 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<RoomGuard<Vec<ConnectionId>>> {
+ 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<proto::RepositoryEntry>,
pub removed_repositories: Vec<u64>,
pub diagnostic_summaries: Vec<proto::DiagnosticSummary>,
+ pub settings_files: Vec<WorktreeSettingsFile>,
pub scan_id: u64,
pub completed_scan_id: u64,
}
@@ -3537,10 +3631,17 @@ pub struct Worktree {
pub entries: Vec<proto::Entry>,
pub repository_entries: BTreeMap<u64, proto::RepositoryEntry>,
pub diagnostic_summaries: Vec<proto::DiagnosticSummary>,
+ pub settings_files: Vec<WorktreeSettingsFile>,
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::*;
@@ -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 {}
@@ -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::<proto::GetHover>)
.add_request_handler(forward_project_request::<proto::GetDefinition>)
.add_request_handler(forward_project_request::<proto::GetTypeDefinition>)
@@ -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,
@@ -3114,6 +3114,135 @@ async fn test_fs_operations(
});
}
+#[gpui::test(iterations = 10)]
+async fn test_local_settings(
+ deterministic: Arc<Deterministic>,
+ cx_a: &mut TestAppContext,
+ cx_b: &mut TestAppContext,
+) {
+ deterministic.forbid_parking();
+ let mut server = TestServer::start(&deterministic).await;
+ let client_a = server.create_client(cx_a, "user_a").await;
+ let client_b = server.create_client(cx_b, "user_b").await;
+ server
+ .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
+ .await;
+ let active_call_a = cx_a.read(ActiveCall::global);
+
+ // 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::<SettingsStore>();
+ assert_eq!(
+ store.local_settings(worktree_b.id()).collect::<Vec<_>>(),
+ &[
+ (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::<SettingsStore>();
+ assert_eq!(
+ store.local_settings(worktree_b.id()).collect::<Vec<_>>(),
+ &[
+ (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::<SettingsStore>();
+ assert_eq!(
+ store.local_settings(worktree_b.id()).collect::<Vec<_>>(),
+ &[
+ (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::<SettingsStore>();
+ assert_eq!(
+ store.local_settings(worktree_b.id()).collect::<Vec<_>>(),
+ &[(Path::new("a").into(), r#"{"hard_tabs":true}"#.to_string()),]
+ )
+ });
+}
+
#[gpui::test(iterations = 10)]
async fn test_buffer_conflict_after_save(
deterministic: Arc<Deterministic>,
@@ -318,7 +318,7 @@ impl Copilot {
fn enable_or_disable_copilot(&mut self, cx: &mut ModelContext<Copilot>) {
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 {
@@ -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<ContextMenu>,
editor_subscription: Option<(Subscription, usize)>,
editor_enabled: Option<bool>,
- language: Option<Arc<str>>,
- path: Option<Arc<Path>>,
+ language: Option<Arc<Language>>,
+ file: Option<Arc<dyn File>>,
fs: Arc<dyn Fs>,
}
@@ -41,7 +44,7 @@ impl View for CopilotButton {
}
fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
- 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::<AllLanguageSettings>(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<dyn Fs>, 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::<AllLanguageSettings>(fs, cx, move |file| {
file.defaults.show_copilot_suggestions = Some((!show_copilot_suggestions).into())
});
}
-fn toggle_copilot_for_language(language: Arc<str>, fs: Arc<dyn Fs>, cx: &mut AppContext) {
- let show_copilot_suggestions = all_language_settings(cx).copilot_enabled(Some(&language), None);
+fn toggle_copilot_for_language(language: Arc<Language>, fs: Arc<dyn Fs>, cx: &mut AppContext) {
+ let show_copilot_suggestions =
+ all_language_settings(None, cx).copilot_enabled(Some(&language), None);
update_settings_file::<AllLanguageSettings>(fs, cx, move |file| {
file.languages
- .entry(language)
+ .entry(language.name())
.or_default()
.show_copilot_suggestions = Some(!show_copilot_suggestions);
});
@@ -272,12 +272,11 @@ impl DisplayMap {
}
fn tab_size(buffer: &ModelHandle<MultiBuffer>, cx: &mut ModelContext<Self>) -> 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)]
@@ -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<Self>,
) -> 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::<TelemetrySettings>(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)
@@ -1231,6 +1231,10 @@ mod tests {
unimplemented!()
}
+ fn worktree_id(&self) -> usize {
+ 0
+ }
+
fn is_deleted(&self) -> bool {
unimplemented!()
}
@@ -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<Buffer>)) {
@@ -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<LanguageScope> {
@@ -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" }
@@ -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<String>,
metrics_id: Option<Arc<str>>,
installation_id: Option<Arc<str>>,
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,
@@ -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(),
)
@@ -36,7 +36,7 @@ struct StateInner {
scroll_to: Option<ScrollTarget>,
}
-pub struct LayoutState<V: View> {
+pub struct UniformListLayoutState<V: View> {
scroll_max: f32,
item_height: f32,
items: Vec<AnyElement<V>>,
@@ -152,7 +152,7 @@ impl<V: View> UniformList<V> {
}
impl<V: View> Element<V> for UniformList<V> {
- type LayoutState = LayoutState<V>;
+ type LayoutState = UniformListLayoutState<V>;
type PaintState = ();
fn layout(
@@ -169,7 +169,7 @@ impl<V: View> Element<V> for UniformList<V> {
let no_items = (
constraint.min,
- LayoutState {
+ UniformListLayoutState {
item_height: 0.,
scroll_max: 0.,
items: Default::default(),
@@ -263,7 +263,7 @@ impl<V: View> Element<V> for UniformList<V> {
(
size,
- LayoutState {
+ UniformListLayoutState {
item_height,
scroll_max,
items,
@@ -25,8 +25,9 @@ struct Family {
pub struct FontCache(RwLock<FontCacheState>);
pub struct FontCacheState {
- fonts: Arc<dyn platform::FontSystem>,
+ font_system: Arc<dyn platform::FontSystem>,
families: Vec<Family>,
+ default_family: Option<FamilyId>,
font_selections: HashMap<FamilyId, HashMap<Properties, FontId>>,
metrics: HashMap<FontId, Metrics>,
wrapper_pool: HashMap<(FontId, OrderedFloat<f32>), Vec<LineWrapper>>,
@@ -42,8 +43,9 @@ unsafe impl Send for FontCache {}
impl FontCache {
pub fn new(fonts: Arc<dyn platform::FontSystem>) -> 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(),
@@ -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,
@@ -343,6 +343,7 @@ pub enum RasterizationOptions {
pub trait FontSystem: Send + Sync {
fn add_fonts(&self, fonts: &[Arc<Vec<u8>>]) -> anyhow::Result<()>;
+ fn all_families(&self) -> Vec<String>;
fn load_family(&self, name: &str, features: &FontFeatures) -> anyhow::Result<Vec<FontId>>;
fn select_font(
&self,
@@ -66,6 +66,14 @@ impl platform::FontSystem for FontSystem {
self.0.write().add_fonts(fonts)
}
+ fn all_families(&self) -> Vec<String> {
+ 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<Vec<FontId>> {
self.0.write().load_family(name, features)
}
@@ -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<T: ToOffset>(&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<D: ToOffset>(&self, position: D) -> Option<LanguageScope> {
@@ -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::<AllLanguageSettings>(cx);
}
-pub fn language_settings<'a>(language: Option<&str>, cx: &'a AppContext) -> &'a LanguageSettings {
- settings::get::<AllLanguageSettings>(cx).language(language)
+pub fn language_settings<'a>(
+ language: Option<&Arc<Language>>,
+ file: Option<&Arc<dyn File>>,
+ 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::<AllLanguageSettings>(cx)
+pub fn all_language_settings<'a>(
+ file: Option<&Arc<dyn File>>,
+ 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<Language>>, 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
}
}
@@ -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 {
@@ -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<Self>) {
- 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::<SettingsStore>();
+ 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<Self>,
) -> Result<()> {
+ cx.update_global::<SettingsStore, _, _>(|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>,
worktree_path: Arc<Path>,
language: Arc<Language>,
cx: &mut ModelContext<Self>,
) {
- 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<Item = ModelHandle<Buffer>>,
cx: &mut ModelContext<Self>,
) -> Option<()> {
- let language_server_lookup_info: HashSet<(WorktreeId, Arc<Path>, Arc<Language>)> = buffers
+ let language_server_lookup_info: HashSet<(ModelHandle<Worktree>, Arc<Language>)> = 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<Path>,
+ worktree: ModelHandle<Worktree>,
language: Arc<Language>,
cx: &mut ModelContext<Self>,
) {
+ 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<Self>,
) -> Task<Result<Option<Transaction>>> {
- 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::<SettingsStore, _, _>(|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<Worktree>,
+ changes: &UpdatedEntriesSet,
+ cx: &mut ModelContext<Self>,
+ ) {
+ 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<Path>, _)> =
+ futures::future::join_all(settings_contents).await;
+ cx.update(|cx| {
+ cx.update_global::<SettingsStore, _, _>(|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<ProjectPath>, cx: &mut ModelContext<Self>) {
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<Self>,
+ envelope: TypedEnvelope<proto::UpdateWorktreeSettings>,
+ _: Arc<Client>,
+ 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::<SettingsStore, _, _>(|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<Self>,
envelope: TypedEnvelope<proto::CreateProjectEntry>,
@@ -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 {
@@ -63,6 +63,66 @@ async fn test_symlinks(cx: &mut gpui::TestAppContext) {
});
}
+#[gpui::test]
+async fn test_managing_project_specific_settings(
+ deterministic: Arc<Deterministic>,
+ 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<Deterministic>,
@@ -677,6 +677,11 @@ impl Worktree {
Worktree::Remote(worktree) => worktree.abs_path.clone(),
}
}
+
+ pub fn root_file(&self, cx: &mut ModelContext<Self>) -> Option<Arc<File>> {
+ 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<Worktree>) -> Arc<Self> {
+ 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<Worktree>,
@@ -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,
@@ -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;
@@ -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
);
@@ -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;
@@ -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<T: Setting>(cx: &mut AppContext) {
@@ -17,6 +24,10 @@ pub fn get<'a, T: Setting>(cx: &'a AppContext) -> &'a T {
cx.global::<SettingsStore>().get(None)
}
+pub fn get_local<'a, T: Setting>(location: Option<(usize, &Path)>, cx: &'a AppContext) -> &'a T {
+ cx.global::<SettingsStore>().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();
@@ -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<TypeId, Box<dyn AnySettingValue>>,
- default_deserialized_settings: Option<serde_json::Value>,
- user_deserialized_settings: Option<serde_json::Value>,
- local_deserialized_settings: BTreeMap<Arc<Path>, serde_json::Value>,
+ default_deserialized_settings: serde_json::Value,
+ user_deserialized_settings: serde_json::Value,
+ local_deserialized_settings: BTreeMap<(usize, Arc<Path>), serde_json::Value>,
tab_size_callback: Option<(TypeId, Box<dyn Fn(&dyn Any) -> Option<usize>>)>,
}
+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<T> {
global_value: Option<T>,
- local_values: Vec<(Arc<Path>, T)>,
+ local_values: Vec<(usize, Arc<Path>, T)>,
}
trait AnySettingValue {
@@ -109,9 +120,9 @@ trait AnySettingValue {
custom: &[DeserializedSetting],
cx: &AppContext,
) -> Result<Box<dyn Any>>;
- 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<dyn Any>);
- fn set_local_value(&mut self, path: Arc<Path>, value: Box<dyn Any>);
+ fn set_local_value(&mut self, root_id: usize, path: Arc<Path>, value: Box<dyn Any>);
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();
-
- 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];
- }
- }
+ 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<T: Setting>(&self, path: Option<&Path>) -> &T {
+ pub fn get<T: Setting>(&self, path: Option<(usize, &Path)>) -> &T {
self.setting_values
.get(&TypeId::of::<T>())
.unwrap_or_else(|| panic!("unregistered setting type {}", type_name::<T>()))
@@ -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::<T>(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::<T>()))
- .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<Path>,
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<Item = (Arc<Path>, 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::<DeserializedSetting>::new();
- let mut paths_stack = Vec::<Option<&Path>>::new();
+ let mut paths_stack = Vec::<Option<(usize, &Path)>>::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);
}
+ }
- // 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;
}
- // 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((*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(local_settings) =
- setting_value.deserialize_setting(&local_settings).log_err()
+ if let Some(value) = setting_value
+ .load_setting(&default_settings, &user_settings_stack, cx)
+ .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::<Vec<_>>(),
+ )
+ .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<T: Setting> AnySettingValue for SettingValue<T> {
fn key(&self) -> Option<&'static str> {
T::KEY
@@ -546,10 +585,10 @@ impl<T: Setting> AnySettingValue for SettingValue<T> {
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<T: Setting> AnySettingValue for SettingValue<T> {
self.global_value = Some(*value.downcast().unwrap());
}
- fn set_local_value(&mut self, path: Arc<Path>, value: Box<dyn Any>) {
+ fn set_local_value(&mut self, root_id: usize, path: Arc<Path>, value: Box<dyn Any>) {
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::<UserSettings>(Some(Path::new("/root1/something"))),
+ store.get::<UserSettings>(Some((1, Path::new("/root1/something")))),
&UserSettings {
name: "John Doe".to_string(),
age: 31,
@@ -914,7 +959,7 @@ mod tests {
}
);
assert_eq!(
- store.get::<UserSettings>(Some(Path::new("/root1/subdir/something"))),
+ store.get::<UserSettings>(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::<UserSettings>(Some(Path::new("/root2/something"))),
+ store.get::<UserSettings>(Some((1, Path::new("/root2/something")))),
&UserSettings {
name: "John Doe".to_string(),
age: 42,
@@ -930,7 +975,7 @@ mod tests {
}
);
assert_eq!(
- store.get::<MultiKeySettings>(Some(Path::new("/root2/something"))),
+ store.get::<MultiKeySettings>(Some((1, Path::new("/root2/something")))),
&MultiKeySettings {
key1: "a".to_string(),
key2: "b".to_string(),
@@ -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<Self>) {
+ fn new_terminal(
+ workspace: &mut Workspace,
+ _: &workspace::NewTerminal,
+ cx: &mut ViewContext<Workspace>,
+ ) {
+ let Some(this) = workspace.focus_panel::<Self>(cx) else {
+ return;
+ };
+
+ this.update(cx, |this, cx| this.add_terminal(cx))
+ }
+
+ fn add_terminal(&mut self, cx: &mut ViewContext<Self>) {
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<Self>) {
if active && self.pane.read(cx).items_len() == 0 {
- self.add_terminal(&Default::default(), cx)
+ self.add_terminal(cx)
}
}
@@ -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::<TerminalView>(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<Workspace>,
) {
let strategy = settings::get::<TerminalSettings>(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<Project>, ViewHandle<Workspace>) {
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));
@@ -90,7 +90,8 @@ pub struct Workspace {
pub breadcrumbs: Interactive<ContainedText>,
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,
@@ -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 {
@@ -32,7 +32,7 @@ pub fn init(cx: &mut AppContext) {
pub fn show_welcome_experience(app_state: &Arc<AppState>, 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);
@@ -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<Self>) {
+ pub(crate) fn set_open(&mut self, open: bool, cx: &mut ViewContext<Self>) {
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>) {
- 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<dyn PanelHandle>> {
- let entry = self.active_entry()?;
+ pub fn visible_panel(&self) -> Option<&Rc<dyn PanelHandle>> {
+ let entry = self.visible_entry()?;
Some(&entry.panel)
}
- fn active_entry(&self) -> Option<&PanelEntry> {
+ pub fn active_panel(&self) -> Option<&Rc<dyn PanelHandle>> {
+ 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<Rc<dyn PanelHandle>> {
- 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<Workspace> {
- 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<Self>) -> AnyElement<Self> {
- 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<Self>) {
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::<Self, _>::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();
}
}
})
@@ -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<TypeId>,
+pub(crate) struct NotificationTracker {
+ notifications_sent: HashMap<TypeId, Vec<usize>>,
}
impl std::ops::Deref for NotificationTracker {
- type Target = HashSet<TypeId>;
+ type Target = HashMap<TypeId, Vec<usize>>;
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<V: Notification>(
+ &self,
+ id: usize,
+ cx: &ViewContext<Self>,
+ ) -> bool {
+ cx.global::<NotificationTracker>()
+ .get(&TypeId::of::<V>())
+ .map(|ids| ids.contains(&id))
+ .unwrap_or(false)
+ }
+
pub fn show_notification_once<V: Notification>(
&mut self,
id: usize,
cx: &mut ViewContext<Self>,
build_notification: impl FnOnce(&mut ViewContext<Self>) -> ViewHandle<V>,
) {
- if !cx
- .global::<NotificationTracker>()
- .contains(&TypeId::of::<V>())
- {
+ if !self.has_shown_notification_once::<V>(id, cx) {
cx.update_global::<NotificationTracker, _, _>(|tracker, _| {
- tracker.insert(TypeId::of::<V>())
+ let entry = tracker.entry(TypeId::of::<V>()).or_default();
+ entry.push(id);
});
self.show_notification::<V>(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<MessageNotification>),
+ }
+
pub struct MessageNotification {
- message: Cow<'static, str>,
+ message: NotificationMessage,
on_click: Option<Arc<dyn Fn(&mut ViewContext<Self>)>>,
click_message: Option<Cow<'static, str>>,
}
@@ -204,7 +219,17 @@ pub mod simple_message_notification {
S: Into<Cow<'static, str>>,
{
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>,
+ ) -> 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::<MessageNotificationTag, _>::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::<Cancel, _>::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::<Cancel, _>::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::<MessageNotificationTag, _>::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()
}
}
@@ -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<Box<dyn ItemHandle>>,
activation_history: Vec<usize>,
- is_active: bool,
zoomed: bool,
active_item_index: usize,
last_focused_view_by_item: HashMap<usize, AnyWeakViewHandle>,
@@ -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>) {
- 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<Self>) {
+ // 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::<Tabs>(1, autoscroll, cx);
@@ -1412,6 +1409,7 @@ impl Pane {
pub fn render_tab_bar_button<F: 'static + Fn(&mut Pane, &mut EventContext<Pane>)>(
index: usize,
icon: &'static str,
+ active: bool,
tooltip: Option<(String, Option<Box<dyn Action>>)>,
cx: &mut ViewContext<Pane>,
on_click: F,
@@ -1421,7 +1419,7 @@ impl Pane {
let mut button = MouseEventHandler::<TabBarButton, _>::new(index, cx, |mouse_state, cx| {
let theme = &settings::get::<ThemeSettings>(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) {
@@ -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<PathBuf>,
@@ -268,14 +250,14 @@ pub fn init(app_state: Arc<AppState>, 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<client::Subscription>,
modal: Option<AnyViewHandle>,
zoomed: Option<AnyWeakViewHandle>,
+ zoomed_position: Option<DockPosition>,
center: PaneGroup,
left_dock: ViewHandle<Dock>,
bottom_dock: ViewHandle<Dock>,
@@ -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<Self>,
- ) {
+ pub fn toggle_dock(&mut self, dock_side: DockPosition, cx: &mut ViewContext<Self>) {
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 {
- cx.focus_self();
+ if reveal_dock {
+ self.dismiss_zoomed_items_to_reveal(Some(dock_side), cx);
}
- cx.notify();
- self.serialize_workspace(cx);
- }
-
- pub fn toggle_panel(
- &mut self,
- position: DockPosition,
- panel_index: usize,
- cx: &mut ViewContext<Self>,
- ) {
- 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 {
+ if focus_center {
cx.focus_self();
}
+ cx.notify();
self.serialize_workspace(cx);
+ }
- cx.notify();
+ /// Transfer focus to the panel of the given type.
+ pub fn focus_panel<T: Panel>(&mut self, cx: &mut ViewContext<Self>) -> Option<ViewHandle<T>> {
+ self.focus_or_unfocus_panel::<T>(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<T: Panel>(&mut self, cx: &mut ViewContext<Self>) {
+ self.focus_or_unfocus_panel::<T>(cx, |panel, cx| !panel.has_focus(cx));
+ }
+
+ /// Focus or unfocus the given panel type, depending on the given callback.
+ fn focus_or_unfocus_panel<T: Panel>(
+ &mut self,
+ cx: &mut ViewContext<Self>,
+ should_focus: impl Fn(&dyn PanelHandle, &mut ViewContext<Dock>) -> bool,
+ ) -> Option<Rc<dyn PanelHandle>> {
for dock in [&self.left_dock, &self.bottom_dock, &self.right_dock] {
if let Some(panel_index) = dock.read(cx).panel_index_for_type::<T>() {
- 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<T: Panel>(&self, cx: &WindowContext) -> Option<ViewHandle<T>> {
@@ -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<DockPosition>,
+ cx: &mut ViewContext<Self>,
+ ) {
+ // 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<Pane>, cx: &mut ViewContext<Self>) {
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::<Vec<_>>(),
- 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<Workspace>, 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::<MessageNotification>(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::<MessageNotification>(MESSAGE_ID, cx)
+ })
+ .unwrap_or(false)
+ {
+ cx.update(|cx| {
+ cx.update_global::<NotificationTracker, _, _>(|tracker, _| {
+ let entry = tracker
+ .entry(TypeId::of::<MessageNotification>())
+ .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::<ThemeSettings>(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<Workspace>, 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<Workspace>, 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<Vector2F> {
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::<TestPanel>(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::<TestPanel>(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::<TestPanel>(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::<TestPanel>(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());
});
}
@@ -3,7 +3,7 @@ authors = ["Nathan Sobo <nathansobo@gmail.com>"]
description = "The fast, collaborative code editor."
edition = "2021"
name = "zed"
-version = "0.89.0"
+version = "0.90.0"
publish = false
[lib]
@@ -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,
},
{
@@ -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<BoxFuture<'static, Value>> {
+ 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(),
@@ -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<String>,
// 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() {
@@ -89,18 +89,9 @@ pub fn menus() -> Vec<Menu<'static>> {
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![
@@ -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);
@@ -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)
@@ -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"
@@ -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[] = []
+
+ 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}`
+ )
+ }
-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`)
+ 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)
@@ -94,6 +94,9 @@ export default function tabBar(colorScheme: ColorScheme) {
hover: {
color: foreground(layer, "hovered"),
},
+ active: {
+ color: foreground(layer, "accent"),
+ }
},
paneButtonContainer: {
background: tab.background,
@@ -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: {
@@ -0,0 +1,21 @@
+The MIT License (MIT)
+
+Copyright (c) 2017 <eliverlara@gmail.com>
+
+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.
@@ -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",
}
@@ -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.
@@ -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: {
@@ -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: {
@@ -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: {
@@ -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: {
@@ -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: {
@@ -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: {
@@ -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: {
@@ -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: {
@@ -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: {
@@ -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: {
@@ -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: {
@@ -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: {
@@ -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: {
@@ -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: {
@@ -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: {
@@ -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: {
@@ -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: {
@@ -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: {
@@ -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: {
@@ -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: {
@@ -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",
- },
},
}
@@ -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.
@@ -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,
@@ -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,
@@ -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,
@@ -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",
}
@@ -19,6 +19,11 @@ export interface ColorScheme {
syntax?: Partial<ThemeSyntax>
}
+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
@@ -0,0 +1,21 @@
+The MIT License (MIT)
+
+Copyright (c) <YEAR> <COPYRIGHT HOLDER>
+
+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.
@@ -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:
@@ -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.
@@ -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",
}
@@ -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",
}
@@ -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.
@@ -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",
}
@@ -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",
}
@@ -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",
}
@@ -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.
@@ -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",
}
@@ -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.
@@ -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",
}
@@ -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.
@@ -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",
- },
},
}