Merge remote-tracking branch 'origin/main' into assistant-2

This commit is contained in:
Antonio Scandurra 2023-06-02 09:32:11 +02:00
commit 20e65a533c
106 changed files with 2026 additions and 844 deletions

View file

@ -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.

5
Cargo.lock generated
View file

@ -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",

View file

@ -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",

View file

@ -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]]

View file

@ -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,

View file

@ -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");

View file

@ -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::*;

View file

@ -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 {}

View file

@ -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,

View file

@ -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>,

View file

@ -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 {

View 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);
});

View file

@ -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)]

View file

@ -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)

View file

@ -1231,6 +1231,10 @@ mod tests {
unimplemented!()
}
fn worktree_id(&self) -> usize {
0
}
fn is_deleted(&self) -> bool {
unimplemented!()
}

View file

@ -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> {

View file

@ -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" }

View file

@ -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,

View file

@ -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(),
)

View file

@ -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,

View file

@ -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(),

View file

@ -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,

View file

@ -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,

View file

@ -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)
}

View file

@ -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> {

View file

@ -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
}
}

View file

@ -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 {

View file

@ -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 {

View file

@ -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>,

View file

@ -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,

View file

@ -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;

View file

@ -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
);

View file

@ -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;

View file

@ -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();

View file

@ -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();
user_values_stack = vec![user_settings];
}
if let Some(user_settings) = &self.user_deserialized_settings {
if let Some(user_settings) =
setting_value.deserialize_setting(user_settings).log_err()
{
user_values_stack = vec![user_settings];
}
}
if let Some(setting) = setting_value
.load_setting(&default_settings, &user_values_stack, cx)
.log_err()
{
setting_value.set_global_value(setting);
}
if let Some(setting) = setting_value
.load_setting(&default_settings, &user_values_stack, cx)
.log_err()
{
setting_value.set_global_value(setting);
}
}
}
@ -165,7 +173,7 @@ impl SettingsStore {
///
/// Panics if the given setting type has not been registered, or if there is no
/// value for this setting.
pub fn get<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);
}
}
// Reload the local values for the setting.
for ((root_id, path), local_settings) in &self.local_deserialized_settings {
// Build a stack of all of the local values for that setting.
while let Some(prev_entry) = paths_stack.last() {
if let Some((prev_root_id, prev_path)) = prev_entry {
if root_id != prev_root_id || !path.starts_with(prev_path) {
paths_stack.pop();
user_settings_stack.pop();
continue;
}
}
break;
}
// If the global settings file changed, reload the global value for the field.
if changed_local_path.is_none() {
if let Some(local_settings) =
setting_value.deserialize_setting(&local_settings).log_err()
{
paths_stack.push(Some((*root_id, path.as_ref())));
user_settings_stack.push(local_settings);
// If a local settings file changed, then avoid recomputing local
// settings for any path outside of that directory.
if changed_local_path.map_or(false, |(changed_root_id, changed_local_path)| {
*root_id != changed_root_id || !path.starts_with(changed_local_path)
}) {
continue;
}
if let Some(value) = setting_value
.load_setting(&default_settings, &user_settings_stack, cx)
.log_err()
{
setting_value.set_global_value(value);
}
}
// Reload the local values for the setting.
for (path, local_settings) in &self.local_deserialized_settings {
// Build a stack of all of the local values for that setting.
while let Some(prev_path) = paths_stack.last() {
if let Some(prev_path) = prev_path {
if !path.starts_with(prev_path) {
paths_stack.pop();
user_settings_stack.pop();
continue;
}
}
break;
}
if let Some(local_settings) =
setting_value.deserialize_setting(&local_settings).log_err()
{
paths_stack.push(Some(path.as_ref()));
user_settings_stack.push(local_settings);
// If a local settings file changed, then avoid recomputing local
// settings for any path outside of that directory.
if changed_local_path.map_or(false, |changed_local_path| {
!path.starts_with(changed_local_path)
}) {
continue;
}
if let Some(value) = setting_value
.load_setting(&default_settings, &user_settings_stack, cx)
.log_err()
{
setting_value.set_local_value(path.clone(), value);
}
setting_value.set_local_value(*root_id, path.clone(), value);
}
}
}
@ -510,6 +531,24 @@ impl SettingsStore {
}
}
impl Debug for SettingsStore {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("SettingsStore")
.field(
"types",
&self
.setting_values
.values()
.map(|value| value.setting_type_name())
.collect::<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(),

View file

@ -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)
}
}

View file

@ -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));

View file

@ -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,

View file

@ -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 {

View file

@ -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);

View file

@ -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();
}
}
})

View file

@ -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()
}
}

View file

@ -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) {

View file

@ -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 &timestamp > 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 {
if reveal_dock {
self.dismiss_zoomed_items_to_reveal(Some(dock_side), cx);
}
if focus_center {
cx.focus_self();
}
cx.notify();
self.serialize_workspace(cx);
}
pub fn toggle_panel(
&mut self,
position: DockPosition,
panel_index: usize,
cx: &mut ViewContext<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 {
cx.focus_self();
}
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());
});
}

View file

@ -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]

View file

@ -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,
},
{

View file

@ -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(),

View file

@ -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() {

View file

@ -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![

View file

@ -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);

View file

@ -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)

View file

@ -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"

View file

@ -1,54 +1,79 @@
import fs from "fs"
import path from "path"
import { ColorScheme, Meta } from "./themes/common/colorScheme"
import { ColorScheme, MetaAndLicense } from "./themes/common/colorScheme"
const colorSchemes: ColorScheme[] = []
export default colorSchemes
const THEMES_DIRECTORY = path.resolve(`${__dirname}/themes`)
const STAFF_DIRECTORY = path.resolve(`${__dirname}/themes/staff`)
const IGNORE_ITEMS = ["staff", "common", "common.ts"]
const ACCEPT_EXTENSION = ".ts"
const LICENSE_FILE_NAME = "LICENSE"
const schemeMeta: Meta[] = []
export { schemeMeta }
function getAllTsFiles(directoryPath: string) {
const files = fs.readdirSync(directoryPath)
const fileList: string[] = []
const staffColorSchemes: ColorScheme[] = []
export { staffColorSchemes }
for (const file of files) {
if (!IGNORE_ITEMS.includes(file)) {
const filePath = path.join(directoryPath, file)
const experimentalColorSchemes: ColorScheme[] = []
export { experimentalColorSchemes }
const themes_directory = path.resolve(`${__dirname}/themes`)
function for_all_color_schemes_in(
themesPath: string,
callback: (module: any, path: string) => void
) {
for (const fileName of fs.readdirSync(themesPath)) {
if (fileName == "template.ts") continue
const filePath = path.join(themesPath, fileName)
if (fs.statSync(filePath).isFile()) {
const colorScheme = require(filePath)
callback(colorScheme, path.basename(filePath))
if (fs.statSync(filePath).isDirectory()) {
fileList.push(...getAllTsFiles(filePath))
} else if (path.extname(file) === ACCEPT_EXTENSION) {
fileList.push(filePath)
}
}
}
return fileList
}
function fillColorSchemes(themesPath: string, colorSchemes: ColorScheme[]) {
for_all_color_schemes_in(themesPath, (colorScheme, _path) => {
function getAllColorSchemes(directoryPath: string) {
const files = getAllTsFiles(directoryPath)
return files.map((filePath) => ({
colorScheme: require(filePath),
filePath,
fileName: path.basename(filePath),
licenseFile: `${path.dirname(filePath)}/${LICENSE_FILE_NAME}`,
}))
}
function getColorSchemes(directoryPath: string) {
const colorSchemes: ColorScheme[] = []
for (const { colorScheme } of getAllColorSchemes(directoryPath)) {
if (colorScheme.dark) colorSchemes.push(colorScheme.dark)
if (colorScheme.light) colorSchemes.push(colorScheme.light)
})
else if (colorScheme.light) colorSchemes.push(colorScheme.light)
}
return colorSchemes
}
fillColorSchemes(themes_directory, colorSchemes)
fillColorSchemes(path.resolve(`${themes_directory}/staff`), staffColorSchemes)
function getMetaAndLicense(directoryPath: string) {
const meta: MetaAndLicense[] = []
function fillMeta(themesPath: string, meta: Meta[]) {
for_all_color_schemes_in(themesPath, (colorScheme, path) => {
if (colorScheme.meta) {
meta.push(colorScheme.meta)
} else {
throw Error(`Public theme ${path} must have a meta field`)
for (const { colorScheme, filePath, licenseFile } of getAllColorSchemes(
directoryPath
)) {
const licenseExists = fs.existsSync(licenseFile)
if (!licenseExists) {
throw Error(
`Public theme should have a LICENSE file ${licenseFile}`
)
}
})
if (!colorScheme.meta) {
throw Error(`Public theme ${filePath} must have a meta field`)
}
meta.push({
meta: colorScheme.meta,
licenseFile,
})
}
return meta
}
fillMeta(themes_directory, schemeMeta)
export const colorSchemes = getColorSchemes(THEMES_DIRECTORY)
export const staffColorSchemes = getColorSchemes(STAFF_DIRECTORY)
export const schemeMeta = getMetaAndLicense(THEMES_DIRECTORY)

View file

@ -94,6 +94,9 @@ export default function tabBar(colorScheme: ColorScheme) {
hover: {
color: foreground(layer, "hovered"),
},
active: {
color: foreground(layer, "accent"),
}
},
paneButtonContainer: {
background: tab.background,

View file

@ -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: {

View file

@ -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.

View file

@ -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",
}

View file

@ -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.

View file

@ -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: {

View file

@ -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: {

View file

@ -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: {

View file

@ -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: {

View file

@ -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: {

View file

@ -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: {

View file

@ -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: {

View file

@ -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: {

View file

@ -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: {

View file

@ -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: {

View file

@ -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: {

View file

@ -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: {

View file

@ -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: {

View file

@ -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: {

View file

@ -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: {

View file

@ -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: {

View file

@ -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: {

View file

@ -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: {

View file

@ -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: {

View file

@ -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: {

View file

@ -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",
},
},
}

View file

@ -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.

View file

@ -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,

View file

@ -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,

View file

@ -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,

View file

@ -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",
}

View file

@ -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

View file

@ -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.

View file

@ -1,6 +1,6 @@
import chroma from "chroma-js"
import { Meta, ThemeSyntax } from "./common/colorScheme"
import { colorRamp, createColorScheme } from "./common/ramps"
import { Meta, ThemeSyntax } from "../common/colorScheme"
import { colorRamp, createColorScheme } from "../common/ramps"
const name = "Gruvbox"
@ -248,8 +248,6 @@ export const meta: Meta = {
name,
license: {
SPDX: "MIT", // "MIT/X11"
license_text:
"Copyright <YEAR> <COPYRIGHT HOLDER>\n\nPermission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files(the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/ or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.",
},
author: "morhetz <morhetz@gmail.com>",
url: "https://github.com/morhetz/gruvbox",

View file

@ -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.

View file

@ -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",
}

View file

@ -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",
}

View file

@ -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.

View file

@ -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",
}

View file

@ -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",
}

View file

@ -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",
}

Some files were not shown because too many files have changed in this diff Show more