Add support for folder-specific settings (#2537)

This PR allows you to customize Zed's settings within a particular
folder by creating a `.zed/settings.json` file within that folder.

Todo

* [x] respect folder-specific settings for local projects
* [x] respect folder-specific settings in remote projects
* [x] pass a path when retrieving editor/language settings
* [x] pass a path when retrieving copilot settings
* [ ] update the `Setting` trait to make it clear which types of
settings are locally overridable

Release Notes:

* Added support for folder-specific settings. You can customize Zed's
settings within a particular folder by creating a `.zed` directory and a
`.zed/settings.json` file within that folder.
This commit is contained in:
Max Brunsfeld 2023-05-31 16:27:08 -07:00 committed by GitHub
commit 788f97ec68
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
27 changed files with 797 additions and 158 deletions

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" 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 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" ( CREATE TABLE "worktree_diagnostic_summaries" (
"project_id" INTEGER NOT NULL, "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_entry;
mod worktree_repository; mod worktree_repository;
mod worktree_repository_statuses; mod worktree_repository_statuses;
mod worktree_settings_file;
use crate::executor::Executor; use crate::executor::Executor;
use crate::{Error, Result}; use crate::{Error, Result};
@ -1494,6 +1495,7 @@ impl Database {
updated_repositories: Default::default(), updated_repositories: Default::default(),
removed_repositories: Default::default(), removed_repositories: Default::default(),
diagnostic_summaries: Default::default(), diagnostic_summaries: Default::default(),
settings_files: Default::default(),
scan_id: db_worktree.scan_id as u64, scan_id: db_worktree.scan_id as u64,
completed_scan_id: db_worktree.completed_scan_id as u64, completed_scan_id: db_worktree.completed_scan_id as u64,
}; };
@ -1638,6 +1640,25 @@ impl Database {
}) })
.collect::<Vec<_>>(); .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 let mut collaborators = project
.find_related(project_collaborator::Entity) .find_related(project_collaborator::Entity)
.all(&*tx) .all(&*tx)
@ -2637,6 +2658,58 @@ impl Database {
.await .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( pub async fn join_project(
&self, &self,
project_id: ProjectId, project_id: ProjectId,
@ -2707,6 +2780,7 @@ impl Database {
entries: Default::default(), entries: Default::default(),
repository_entries: Default::default(), repository_entries: Default::default(),
diagnostic_summaries: Default::default(), diagnostic_summaries: Default::default(),
settings_files: Default::default(),
scan_id: db_worktree.scan_id as u64, scan_id: db_worktree.scan_id as u64,
completed_scan_id: db_worktree.completed_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. // Populate language servers.
let language_servers = project let language_servers = project
.find_related(language_server::Entity) .find_related(language_server::Entity)
@ -3482,6 +3575,7 @@ pub struct RejoinedWorktree {
pub updated_repositories: Vec<proto::RepositoryEntry>, pub updated_repositories: Vec<proto::RepositoryEntry>,
pub removed_repositories: Vec<u64>, pub removed_repositories: Vec<u64>,
pub diagnostic_summaries: Vec<proto::DiagnosticSummary>, pub diagnostic_summaries: Vec<proto::DiagnosticSummary>,
pub settings_files: Vec<WorktreeSettingsFile>,
pub scan_id: u64, pub scan_id: u64,
pub completed_scan_id: u64, pub completed_scan_id: u64,
} }
@ -3537,10 +3631,17 @@ pub struct Worktree {
pub entries: Vec<proto::Entry>, pub entries: Vec<proto::Entry>,
pub repository_entries: BTreeMap<u64, proto::RepositoryEntry>, pub repository_entries: BTreeMap<u64, proto::RepositoryEntry>,
pub diagnostic_summaries: Vec<proto::DiagnosticSummary>, pub diagnostic_summaries: Vec<proto::DiagnosticSummary>,
pub settings_files: Vec<WorktreeSettingsFile>,
pub scan_id: u64, pub scan_id: u64,
pub completed_scan_id: u64, pub completed_scan_id: u64,
} }
#[derive(Debug)]
pub struct WorktreeSettingsFile {
pub path: String,
pub content: String,
}
#[cfg(test)] #[cfg(test)]
pub use 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(start_language_server)
.add_message_handler(update_language_server) .add_message_handler(update_language_server)
.add_message_handler(update_diagnostic_summary) .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::GetHover>)
.add_request_handler(forward_project_request::<proto::GetDefinition>) .add_request_handler(forward_project_request::<proto::GetDefinition>)
.add_request_handler(forward_project_request::<proto::GetTypeDefinition>) .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 { 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 { for language_server in &project.language_servers {
@ -1525,6 +1550,31 @@ async fn update_diagnostic_summary(
Ok(()) 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( async fn start_language_server(
request: proto::StartLanguageServer, request: proto::StartLanguageServer,
session: Session, 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)] #[gpui::test(iterations = 10)]
async fn test_buffer_conflict_after_save( async fn test_buffer_conflict_after_save(
deterministic: Arc<Deterministic>, deterministic: Arc<Deterministic>,

View file

@ -318,7 +318,7 @@ impl Copilot {
fn enable_or_disable_copilot(&mut self, cx: &mut ModelContext<Copilot>) { fn enable_or_disable_copilot(&mut self, cx: &mut ModelContext<Copilot>) {
let http = self.http.clone(); let http = self.http.clone();
let node_runtime = self.node_runtime.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) { if matches!(self.server, CopilotServer::Disabled) {
let start_task = cx let start_task = cx
.spawn({ .spawn({
@ -785,10 +785,7 @@ impl Copilot {
let buffer = buffer.read(cx); let buffer = buffer.read(cx);
let uri = registered_buffer.uri.clone(); let uri = registered_buffer.uri.clone();
let position = position.to_point_utf16(buffer); let position = position.to_point_utf16(buffer);
let settings = language_settings( let settings = language_settings(buffer.language_at(position).as_ref(), buffer.file(), cx);
buffer.language_at(position).map(|l| l.name()).as_deref(),
cx,
);
let tab_size = settings.tab_size; let tab_size = settings.tab_size;
let hard_tabs = settings.hard_tabs; let hard_tabs = settings.hard_tabs;
let relative_path = buffer let relative_path = buffer
@ -1175,6 +1172,10 @@ mod tests {
fn to_proto(&self) -> rpc::proto::File { fn to_proto(&self) -> rpc::proto::File {
unimplemented!() unimplemented!()
} }
fn worktree_id(&self) -> usize {
0
}
} }
impl language::LocalFile for File { impl language::LocalFile for File {

View file

@ -9,7 +9,10 @@ use gpui::{
AnyElement, AppContext, AsyncAppContext, Element, Entity, MouseState, Subscription, View, AnyElement, AppContext, AsyncAppContext, Element, Entity, MouseState, Subscription, View,
ViewContext, ViewHandle, WeakViewHandle, WindowContext, 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 settings::{update_settings_file, SettingsStore};
use std::{path::Path, sync::Arc}; use std::{path::Path, sync::Arc};
use util::{paths, ResultExt}; use util::{paths, ResultExt};
@ -26,8 +29,8 @@ pub struct CopilotButton {
popup_menu: ViewHandle<ContextMenu>, popup_menu: ViewHandle<ContextMenu>,
editor_subscription: Option<(Subscription, usize)>, editor_subscription: Option<(Subscription, usize)>,
editor_enabled: Option<bool>, editor_enabled: Option<bool>,
language: Option<Arc<str>>, language: Option<Arc<Language>>,
path: Option<Arc<Path>>, file: Option<Arc<dyn File>>,
fs: Arc<dyn Fs>, fs: Arc<dyn Fs>,
} }
@ -41,7 +44,7 @@ impl View for CopilotButton {
} }
fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> { 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 { if !all_language_settings.copilot.feature_enabled {
return Empty::new().into_any(); return Empty::new().into_any();
} }
@ -165,7 +168,7 @@ impl CopilotButton {
editor_subscription: None, editor_subscription: None,
editor_enabled: None, editor_enabled: None,
language: None, language: None,
path: None, file: None,
fs, fs,
} }
} }
@ -197,14 +200,13 @@ impl CopilotButton {
if let Some(language) = self.language.clone() { if let Some(language) = self.language.clone() {
let fs = fs.clone(); let fs = fs.clone();
let language_enabled = let language_enabled = language_settings::language_settings(Some(&language), None, cx)
language_settings::language_settings(Some(language.as_ref()), cx)
.show_copilot_suggestions; .show_copilot_suggestions;
menu_options.push(ContextMenuItem::handler( menu_options.push(ContextMenuItem::handler(
format!( format!(
"{} Suggestions for {}", "{} Suggestions for {}",
if language_enabled { "Hide" } else { "Show" }, if language_enabled { "Hide" } else { "Show" },
language language.name()
), ),
move |cx| toggle_copilot_for_language(language.clone(), fs.clone(), cx), move |cx| toggle_copilot_for_language(language.clone(), fs.clone(), cx),
)); ));
@ -212,9 +214,9 @@ impl CopilotButton {
let settings = settings::get::<AllLanguageSettings>(cx); let settings = settings::get::<AllLanguageSettings>(cx);
if let Some(path) = self.path.as_ref() { if let Some(file) = &self.file {
let path_enabled = settings.copilot_enabled_for_path(path); let path = file.path().clone();
let path = path.clone(); let path_enabled = settings.copilot_enabled_for_path(&path);
menu_options.push(ContextMenuItem::handler( menu_options.push(ContextMenuItem::handler(
format!( format!(
"{} Suggestions for This Path", "{} Suggestions for This Path",
@ -276,17 +278,15 @@ impl CopilotButton {
let editor = editor.read(cx); let editor = editor.read(cx);
let snapshot = editor.buffer().read(cx).snapshot(cx); let snapshot = editor.buffer().read(cx).snapshot(cx);
let suggestion_anchor = editor.selections.newest_anchor().start; let suggestion_anchor = editor.selections.newest_anchor().start;
let language_name = snapshot let language = snapshot.language_at(suggestion_anchor);
.language_at(suggestion_anchor) let file = snapshot.file_at(suggestion_anchor).cloned();
.map(|language| language.name());
let path = snapshot.file_at(suggestion_anchor).map(|file| file.path());
self.editor_enabled = Some( self.editor_enabled = Some(
all_language_settings(cx) all_language_settings(self.file.as_ref(), cx)
.copilot_enabled(language_name.as_deref(), path.map(|p| p.as_ref())), .copilot_enabled(language, file.as_ref().map(|file| file.path().as_ref())),
); );
self.language = language_name; self.language = language.cloned();
self.path = path.cloned(); self.file = file;
cx.notify() cx.notify()
} }
@ -363,17 +363,18 @@ async fn configure_disabled_globs(
} }
fn toggle_copilot_globally(fs: Arc<dyn Fs>, cx: &mut AppContext) { 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| { update_settings_file::<AllLanguageSettings>(fs, cx, move |file| {
file.defaults.show_copilot_suggestions = Some((!show_copilot_suggestions).into()) 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) { fn toggle_copilot_for_language(language: Arc<Language>, fs: Arc<dyn Fs>, cx: &mut AppContext) {
let show_copilot_suggestions = all_language_settings(cx).copilot_enabled(Some(&language), None); let show_copilot_suggestions =
all_language_settings(None, cx).copilot_enabled(Some(&language), None);
update_settings_file::<AllLanguageSettings>(fs, cx, move |file| { update_settings_file::<AllLanguageSettings>(fs, cx, move |file| {
file.languages file.languages
.entry(language) .entry(language.name())
.or_default() .or_default()
.show_copilot_suggestions = Some(!show_copilot_suggestions); .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 { fn tab_size(buffer: &ModelHandle<MultiBuffer>, cx: &mut ModelContext<Self>) -> NonZeroU32 {
let language_name = buffer let language = buffer
.read(cx) .read(cx)
.as_singleton() .as_singleton()
.and_then(|buffer| buffer.read(cx).language()) .and_then(|buffer| buffer.read(cx).language());
.map(|language| language.name()); language_settings(language.as_deref(), None, cx).tab_size
language_settings(language_name.as_deref(), cx).tab_size
} }
#[cfg(test)] #[cfg(test)]

View file

@ -3207,12 +3207,10 @@ impl Editor {
snapshot: &MultiBufferSnapshot, snapshot: &MultiBufferSnapshot,
cx: &mut ViewContext<Self>, cx: &mut ViewContext<Self>,
) -> bool { ) -> bool {
let path = snapshot.file_at(location).map(|file| file.path().as_ref()); let file = snapshot.file_at(location);
let language_name = snapshot let language = snapshot.language_at(location);
.language_at(location) let settings = all_language_settings(file, cx);
.map(|language| language.name()); settings.copilot_enabled(language, file.map(|f| f.path().as_ref()))
let settings = all_language_settings(cx);
settings.copilot_enabled(language_name.as_deref(), path)
} }
fn has_active_copilot_suggestion(&self, cx: &AppContext) -> bool { fn has_active_copilot_suggestion(&self, cx: &AppContext) -> bool {
@ -7076,11 +7074,13 @@ impl Editor {
}; };
// If None, we are in a file without an extension // If None, we are in a file without an extension
let file_extension = file_extension.or(self let file = self
.buffer .buffer
.read(cx) .read(cx)
.as_singleton() .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(|file| Path::new(file.file_name(cx)).extension())
.and_then(|e| e.to_str()) .and_then(|e| e.to_str())
.map(|a| a.to_string())); .map(|a| a.to_string()));
@ -7091,7 +7091,7 @@ impl Editor {
.get("vim_mode") .get("vim_mode")
== Some(&serde_json::Value::Bool(true)); == Some(&serde_json::Value::Bool(true));
let telemetry_settings = *settings::get::<TelemetrySettings>(cx); 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 let copilot_enabled_for_language = self
.buffer .buffer
.read(cx) .read(cx)

View file

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

View file

@ -1377,8 +1377,14 @@ impl MultiBuffer {
point: T, point: T,
cx: &'a AppContext, cx: &'a AppContext,
) -> &'a LanguageSettings { ) -> &'a LanguageSettings {
let language = self.language_at(point, cx); let mut language = None;
language_settings(language.map(|l| l.name()).as_deref(), cx) 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>)) { pub fn for_each_buffer(&self, mut f: impl FnMut(&ModelHandle<Buffer>)) {
@ -2785,9 +2791,13 @@ impl MultiBufferSnapshot {
point: T, point: T,
cx: &'a AppContext, cx: &'a AppContext,
) -> &'a LanguageSettings { ) -> &'a LanguageSettings {
self.point_to_buffer_offset(point) let mut language = None;
.map(|(buffer, offset)| buffer.settings_at(offset, cx)) let mut file = None;
.unwrap_or_else(|| language_settings(None, cx)) 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> { pub fn language_scope_at<'a, T: ToOffset>(&'a self, point: T) -> Option<LanguageScope> {

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. /// 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; 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 is_deleted(&self) -> bool;
fn as_any(&self) -> &dyn Any; 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 { 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(self.language_at(position), self.file(), cx);
let settings = language_settings(language_name.as_deref(), cx);
if settings.hard_tabs { if settings.hard_tabs {
IndentSize::tab() IndentSize::tab()
} else { } else {
@ -2127,8 +2131,7 @@ impl BufferSnapshot {
position: D, position: D,
cx: &'a AppContext, cx: &'a AppContext,
) -> &'a LanguageSettings { ) -> &'a LanguageSettings {
let language = self.language_at(position); language_settings(self.language_at(position), self.file.as_ref(), cx)
language_settings(language.map(|l| l.name()).as_deref(), cx)
} }
pub fn language_scope_at<D: ToOffset>(&self, position: D) -> Option<LanguageScope> { 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 anyhow::Result;
use collections::HashMap; use collections::HashMap;
use globset::GlobMatcher; use globset::GlobMatcher;
@ -13,12 +14,21 @@ pub fn init(cx: &mut AppContext) {
settings::register::<AllLanguageSettings>(cx); settings::register::<AllLanguageSettings>(cx);
} }
pub fn language_settings<'a>(language: Option<&str>, cx: &'a AppContext) -> &'a LanguageSettings { pub fn language_settings<'a>(
settings::get::<AllLanguageSettings>(cx).language(language) 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 { pub fn all_language_settings<'a>(
settings::get::<AllLanguageSettings>(cx) 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)] #[derive(Debug, Clone)]
@ -155,7 +165,7 @@ impl AllLanguageSettings {
.any(|glob| glob.is_match(path)) .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 { if !self.copilot.feature_enabled {
return false; 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?; .await?;
let tab_size = buffer.read_with(&cx, |buffer, cx| { let tab_size = buffer.read_with(&cx, |buffer, cx| {
let language_name = buffer.language().map(|language| language.name()); language_settings(buffer.language(), buffer.file(), cx).tab_size
language_settings(language_name.as_deref(), cx).tab_size
}); });
Ok(Self { Ok(Self {

View file

@ -28,7 +28,7 @@ use gpui::{
ModelHandle, Task, WeakModelHandle, ModelHandle, Task, WeakModelHandle,
}; };
use language::{ use language::{
language_settings::{all_language_settings, language_settings, FormatOnSave, Formatter}, language_settings::{language_settings, FormatOnSave, Formatter},
point_to_lsp, point_to_lsp,
proto::{ proto::{
deserialize_anchor, deserialize_fingerprint, deserialize_line_ending, deserialize_version, deserialize_anchor, deserialize_fingerprint, deserialize_line_ending, deserialize_version,
@ -72,7 +72,10 @@ use std::{
time::{Duration, Instant, SystemTime}, time::{Duration, Instant, SystemTime},
}; };
use terminals::Terminals; 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 fs::*;
pub use worktree::*; pub use worktree::*;
@ -460,6 +463,7 @@ impl Project {
client.add_model_request_handler(Self::handle_update_buffer); 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_diagnostic_summary);
client.add_model_message_handler(Self::handle_update_worktree); 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_create_project_entry);
client.add_model_request_handler(Self::handle_rename_project_entry); client.add_model_request_handler(Self::handle_rename_project_entry);
client.add_model_request_handler(Self::handle_copy_project_entry); client.add_model_request_handler(Self::handle_copy_project_entry);
@ -686,45 +690,40 @@ impl Project {
} }
fn on_settings_changed(&mut self, cx: &mut ModelContext<Self>) { fn on_settings_changed(&mut self, cx: &mut ModelContext<Self>) {
let settings = all_language_settings(cx);
let mut language_servers_to_start = Vec::new(); let mut language_servers_to_start = Vec::new();
for buffer in self.opened_buffers.values() { for buffer in self.opened_buffers.values() {
if let Some(buffer) = buffer.upgrade(cx) { if let Some(buffer) = buffer.upgrade(cx) {
let buffer = buffer.read(cx); let buffer = buffer.read(cx);
if let Some((file, language)) = File::from_dyn(buffer.file()).zip(buffer.language()) if let Some((file, language)) = buffer.file().zip(buffer.language()) {
{ let settings = language_settings(Some(language), Some(file), cx);
if settings if settings.enable_language_server {
.language(Some(&language.name())) if let Some(file) = File::from_dyn(Some(file)) {
.enable_language_server language_servers_to_start
{ .push((file.worktree.clone(), language.clone()));
let worktree = file.worktree.read(cx); }
language_servers_to_start.push((
worktree.id(),
worktree.as_local().unwrap().abs_path().clone(),
language.clone(),
));
} }
} }
} }
} }
let mut language_servers_to_stop = Vec::new(); let mut language_servers_to_stop = Vec::new();
for language in self.languages.to_vec() { let languages = 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() { for (worktree_id, started_lsp_name) in self.language_server_ids.keys() {
if lsp_name == started_lsp_name { 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())); language_servers_to_stop.push((*worktree_id, started_lsp_name.clone()));
} }
} }
} }
}
}
// Stop all newly-disabled language servers. // Stop all newly-disabled language servers.
for (worktree_id, adapter_name) in language_servers_to_stop { for (worktree_id, adapter_name) in language_servers_to_stop {
@ -733,8 +732,9 @@ impl Project {
} }
// Start all the newly-enabled language servers. // Start all the newly-enabled language servers.
for (worktree_id, worktree_path, language) in language_servers_to_start { for (worktree, language) in language_servers_to_start {
self.start_language_servers(worktree_id, worktree_path, language, cx); 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() { if !self.copilot_enabled && Copilot::global(cx).is_some() {
@ -1107,6 +1107,21 @@ impl Project {
.log_err(); .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 (updates_tx, mut updates_rx) = mpsc::unbounded();
let client = self.client.clone(); let client = self.client.clone();
self.client_state = Some(ProjectClientState::Local { self.client_state = Some(ProjectClientState::Local {
@ -1219,6 +1234,14 @@ impl Project {
message_id: u32, message_id: u32,
cx: &mut ModelContext<Self>, cx: &mut ModelContext<Self>,
) -> Result<()> { ) -> 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.join_project_response_message_id = message_id;
self.set_worktrees_from_proto(message.worktrees, cx)?; self.set_worktrees_from_proto(message.worktrees, cx)?;
self.set_collaborators_from_proto(message.collaborators, 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(file) = File::from_dyn(buffer.read(cx).file()) {
if let Some(worktree) = file.worktree.read(cx).as_local() { let worktree = file.worktree.clone();
let worktree_id = worktree.id(); if let Some(tree) = worktree.read(cx).as_local() {
let worktree_abs_path = worktree.abs_path().clone(); self.start_language_servers(&worktree, tree.abs_path().clone(), new_language, cx);
self.start_language_servers(worktree_id, worktree_abs_path, new_language, cx);
} }
} }
} }
fn start_language_servers( fn start_language_servers(
&mut self, &mut self,
worktree_id: WorktreeId, worktree: &ModelHandle<Worktree>,
worktree_path: Arc<Path>, worktree_path: Arc<Path>,
language: Arc<Language>, language: Arc<Language>,
cx: &mut ModelContext<Self>, 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; return;
} }
let worktree_id = worktree.read(cx).id();
for adapter in language.lsp_adapters() { for adapter in language.lsp_adapters() {
let key = (worktree_id, adapter.name.clone()); let key = (worktree_id, adapter.name.clone());
if self.language_server_ids.contains_key(&key) { if self.language_server_ids.contains_key(&key) {
@ -2748,23 +2780,22 @@ impl Project {
buffers: impl IntoIterator<Item = ModelHandle<Buffer>>, buffers: impl IntoIterator<Item = ModelHandle<Buffer>>,
cx: &mut ModelContext<Self>, cx: &mut ModelContext<Self>,
) -> Option<()> { ) -> 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() .into_iter()
.filter_map(|buffer| { .filter_map(|buffer| {
let buffer = buffer.read(cx); let buffer = buffer.read(cx);
let file = File::from_dyn(buffer.file())?; let file = File::from_dyn(buffer.file())?;
let worktree = file.worktree.read(cx).as_local()?;
let full_path = file.full_path(cx); let full_path = file.full_path(cx);
let language = self let language = self
.languages .languages
.language_for_file(&full_path, Some(buffer.as_rope())) .language_for_file(&full_path, Some(buffer.as_rope()))
.now_or_never()? .now_or_never()?
.ok()?; .ok()?;
Some((worktree.id(), worktree.abs_path().clone(), language)) Some((file.worktree.clone(), language))
}) })
.collect(); .collect();
for (worktree_id, worktree_abs_path, language) in language_server_lookup_info { for (worktree, language) in language_server_lookup_info {
self.restart_language_servers(worktree_id, worktree_abs_path, language, cx); self.restart_language_servers(worktree, language, cx);
} }
None 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 // TODO This will break in the case where the adapter's root paths and worktrees are not equal
fn restart_language_servers( fn restart_language_servers(
&mut self, &mut self,
worktree_id: WorktreeId, worktree: ModelHandle<Worktree>,
fallback_path: Arc<Path>,
language: Arc<Language>, language: Arc<Language>,
cx: &mut ModelContext<Self>, cx: &mut ModelContext<Self>,
) { ) {
let worktree_id = worktree.read(cx).id();
let fallback_path = worktree.read(cx).abs_path();
let mut stops = Vec::new(); let mut stops = Vec::new();
for adapter in language.lsp_adapters() { for adapter in language.lsp_adapters() {
stops.push(self.stop_language_server(worktree_id, adapter.name.clone(), cx)); 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())) .map(|path_buf| Arc::from(path_buf.as_path()))
.unwrap_or(fallback_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 // Lookup new server ids and set them for each of the orphaned worktrees
for adapter in language.lsp_adapters() { for adapter in language.lsp_adapters() {
@ -3432,8 +3465,7 @@ impl Project {
let mut project_transaction = ProjectTransaction::default(); let mut project_transaction = ProjectTransaction::default();
for (buffer, buffer_abs_path, language_server) in &buffers_with_paths_and_servers { for (buffer, buffer_abs_path, language_server) in &buffers_with_paths_and_servers {
let settings = buffer.read_with(&cx, |buffer, cx| { let settings = buffer.read_with(&cx, |buffer, cx| {
let language_name = buffer.language().map(|language| language.name()); language_settings(buffer.language(), buffer.file(), cx).clone()
language_settings(language_name.as_deref(), cx).clone()
}); });
let remove_trailing_whitespace = settings.remove_trailing_whitespace_on_save; let remove_trailing_whitespace = settings.remove_trailing_whitespace_on_save;
@ -4463,11 +4495,14 @@ impl Project {
push_to_history: bool, push_to_history: bool,
cx: &mut ModelContext<Self>, cx: &mut ModelContext<Self>,
) -> Task<Result<Option<Transaction>>> { ) -> Task<Result<Option<Transaction>>> {
let tab_size = buffer.read_with(cx, |buffer, cx| { let (position, tab_size) = buffer.read_with(cx, |buffer, cx| {
let language_name = buffer.language().map(|language| language.name()); let position = position.to_point_utf16(buffer);
language_settings(language_name.as_deref(), cx).tab_size (
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( self.request_lsp(
buffer.clone(), buffer.clone(),
OnTypeFormatting { OnTypeFormatting {
@ -4873,6 +4908,7 @@ impl Project {
worktree::Event::UpdatedEntries(changes) => { worktree::Event::UpdatedEntries(changes) => {
this.update_local_worktree_buffers(&worktree, changes, cx); this.update_local_worktree_buffers(&worktree, changes, cx);
this.update_local_worktree_language_servers(&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) => { worktree::Event::UpdatedGitRepositories(updated_repos) => {
this.update_local_worktree_buffers_git_repos(worktree, updated_repos, cx) this.update_local_worktree_buffers_git_repos(worktree, updated_repos, cx)
@ -4893,8 +4929,12 @@ impl Project {
.push(WorktreeHandle::Weak(worktree.downgrade())); .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); let _ = this.remove_worktree(worktree.id(), cx);
cx.update_global::<SettingsStore, _, _>(|store, cx| {
store.clear_local_settings(handle_id, cx).log_err()
});
}) })
.detach(); .detach();
@ -5179,6 +5219,71 @@ impl Project {
.detach(); .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>) { pub fn set_active_path(&mut self, entry: Option<ProjectPath>, cx: &mut ModelContext<Self>) {
let new_active_entry = entry.and_then(|project_path| { let new_active_entry = entry.and_then(|project_path| {
let worktree = self.worktree_for_id(project_path.worktree_id, cx)?; 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( async fn handle_create_project_entry(
this: ModelHandle<Self>, this: ModelHandle<Self>,
envelope: TypedEnvelope<proto::CreateProjectEntry>, envelope: TypedEnvelope<proto::CreateProjectEntry>,
@ -6521,8 +6650,8 @@ impl Project {
} }
self.metadata_changed(cx); self.metadata_changed(cx);
for (id, _) in old_worktrees_by_id { for id in old_worktrees_by_id.keys() {
cx.emit(Event::WorktreeRemoved(id)); cx.emit(Event::WorktreeRemoved(*id));
} }
Ok(()) Ok(())
@ -6892,6 +7021,13 @@ impl WorktreeHandle {
WorktreeHandle::Weak(handle) => handle.upgrade(cx), 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 { 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] #[gpui::test]
async fn test_managing_language_servers( async fn test_managing_language_servers(
deterministic: Arc<Deterministic>, deterministic: Arc<Deterministic>,

View file

@ -677,6 +677,11 @@ impl Worktree {
Worktree::Remote(worktree) => worktree.abs_path.clone(), 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 { impl LocalWorktree {
@ -684,14 +689,6 @@ impl LocalWorktree {
path.starts_with(&self.abs_path) 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( pub(crate) fn load_buffer(
&mut self, &mut self,
id: u64, id: u64,
@ -1544,6 +1541,14 @@ impl Snapshot {
&self.abs_path &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 { pub fn contains_entry(&self, entry_id: ProjectEntryId) -> bool {
self.entries_by_id.get(&entry_id, &()).is_some() 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)) .unwrap_or_else(|| OsStr::new(&self.worktree.read(cx).root_name))
} }
fn worktree_id(&self) -> usize {
self.worktree.id()
}
fn is_deleted(&self) -> bool { fn is_deleted(&self) -> bool {
self.is_deleted self.is_deleted
} }
@ -2447,6 +2456,17 @@ impl language::LocalFile for File {
} }
impl 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( pub fn from_proto(
proto: rpc::proto::File, proto: rpc::proto::File,
worktree: ModelHandle<Worktree>, worktree: ModelHandle<Worktree>,
@ -2507,7 +2527,7 @@ pub enum EntryKind {
File(CharBag), File(CharBag),
} }
#[derive(Clone, Copy, Debug)] #[derive(Clone, Copy, Debug, PartialEq)]
pub enum PathChange { pub enum PathChange {
/// A filesystem entry was was created. /// A filesystem entry was was created.
Added, Added,

View file

@ -132,6 +132,8 @@ message Envelope {
OnTypeFormatting on_type_formatting = 111; OnTypeFormatting on_type_formatting = 111;
OnTypeFormattingResponse on_type_formatting_response = 112; OnTypeFormattingResponse on_type_formatting_response = 112;
UpdateWorktreeSettings update_worktree_settings = 113;
} }
} }
@ -339,6 +341,13 @@ message UpdateWorktree {
string abs_path = 10; string abs_path = 10;
} }
message UpdateWorktreeSettings {
uint64 project_id = 1;
uint64 worktree_id = 2;
string path = 3;
optional string content = 4;
}
message CreateProjectEntry { message CreateProjectEntry {
uint64 project_id = 1; uint64 project_id = 1;
uint64 worktree_id = 2; uint64 worktree_id = 2;

View file

@ -236,6 +236,7 @@ messages!(
(UpdateProject, Foreground), (UpdateProject, Foreground),
(UpdateProjectCollaborator, Foreground), (UpdateProjectCollaborator, Foreground),
(UpdateWorktree, Foreground), (UpdateWorktree, Foreground),
(UpdateWorktreeSettings, Foreground),
(UpdateDiffBase, Foreground), (UpdateDiffBase, Foreground),
(GetPrivateUserInfo, Foreground), (GetPrivateUserInfo, Foreground),
(GetPrivateUserInfoResponse, Foreground), (GetPrivateUserInfoResponse, Foreground),
@ -345,6 +346,7 @@ entity_messages!(
UpdateProject, UpdateProject,
UpdateProjectCollaborator, UpdateProjectCollaborator,
UpdateWorktree, UpdateWorktree,
UpdateWorktreeSettings,
UpdateDiffBase UpdateDiffBase
); );

View file

@ -6,4 +6,4 @@ pub use conn::Connection;
pub use peer::*; pub use peer::*;
mod macros; 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 fs::Fs;
use futures::{channel::mpsc, StreamExt}; use futures::{channel::mpsc, StreamExt};
use gpui::{executor::Background, AppContext, AssetSource}; 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}; use util::{paths, ResultExt};
pub fn register<T: Setting>(cx: &mut AppContext) { 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) 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> { pub fn default_settings() -> Cow<'static, str> {
match Assets.load(DEFAULT_SETTINGS_ASSET_PATH).unwrap() { match Assets.load(DEFAULT_SETTINGS_ASSET_PATH).unwrap() {
Cow::Borrowed(s) => Cow::Borrowed(str::from_utf8(s).unwrap()), Cow::Borrowed(s) => Cow::Borrowed(str::from_utf8(s).unwrap()),

View file

@ -89,14 +89,14 @@ pub struct SettingsStore {
setting_values: HashMap<TypeId, Box<dyn AnySettingValue>>, setting_values: HashMap<TypeId, Box<dyn AnySettingValue>>,
default_deserialized_settings: Option<serde_json::Value>, default_deserialized_settings: Option<serde_json::Value>,
user_deserialized_settings: Option<serde_json::Value>, user_deserialized_settings: Option<serde_json::Value>,
local_deserialized_settings: BTreeMap<Arc<Path>, 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>>)>, tab_size_callback: Option<(TypeId, Box<dyn Fn(&dyn Any) -> Option<usize>>)>,
} }
#[derive(Debug)] #[derive(Debug)]
struct SettingValue<T> { struct SettingValue<T> {
global_value: Option<T>, global_value: Option<T>,
local_values: Vec<(Arc<Path>, T)>, local_values: Vec<(usize, Arc<Path>, T)>,
} }
trait AnySettingValue { trait AnySettingValue {
@ -109,9 +109,9 @@ trait AnySettingValue {
custom: &[DeserializedSetting], custom: &[DeserializedSetting],
cx: &AppContext, cx: &AppContext,
) -> Result<Box<dyn Any>>; ) -> 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_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( fn json_schema(
&self, &self,
generator: &mut SchemaGenerator, generator: &mut SchemaGenerator,
@ -165,7 +165,7 @@ impl SettingsStore {
/// ///
/// Panics if the given setting type has not been registered, or if there is no /// Panics if the given setting type has not been registered, or if there is no
/// value for this setting. /// 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 self.setting_values
.get(&TypeId::of::<T>()) .get(&TypeId::of::<T>())
.unwrap_or_else(|| panic!("unregistered setting type {}", type_name::<T>())) .unwrap_or_else(|| panic!("unregistered setting type {}", type_name::<T>()))
@ -343,20 +343,37 @@ impl SettingsStore {
/// Add or remove a set of local settings via a JSON string. /// Add or remove a set of local settings via a JSON string.
pub fn set_local_settings( pub fn set_local_settings(
&mut self, &mut self,
root_id: usize,
path: Arc<Path>, path: Arc<Path>,
settings_content: Option<&str>, settings_content: Option<&str>,
cx: &AppContext, cx: &AppContext,
) -> Result<()> { ) -> Result<()> {
if let Some(content) = settings_content { if let Some(content) = settings_content {
self.local_deserialized_settings self.local_deserialized_settings
.insert(path.clone(), parse_json_with_comments(content)?); .insert((root_id, path.clone()), parse_json_with_comments(content)?);
} else { } 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(()) 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<()> {
eprintln!("clearing local settings {root_id}");
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( pub fn json_schema(
&self, &self,
schema_params: &SettingsJsonSchemaParams, schema_params: &SettingsJsonSchemaParams,
@ -436,12 +453,12 @@ impl SettingsStore {
fn recompute_values( fn recompute_values(
&mut self, &mut self,
changed_local_path: Option<&Path>, changed_local_path: Option<(usize, &Path)>,
cx: &AppContext, cx: &AppContext,
) -> Result<()> { ) -> Result<()> {
// Reload the global and local values for every setting. // Reload the global and local values for every setting.
let mut user_settings_stack = Vec::<DeserializedSetting>::new(); 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() { for setting_value in self.setting_values.values_mut() {
if let Some(default_settings) = &self.default_deserialized_settings { 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(default_settings)?;
@ -469,11 +486,11 @@ impl SettingsStore {
} }
// Reload the local values for the setting. // Reload the local values for the setting.
for (path, local_settings) in &self.local_deserialized_settings { for ((root_id, path), local_settings) in &self.local_deserialized_settings {
// Build a stack of all of the local values for that setting. // Build a stack of all of the local values for that setting.
while let Some(prev_path) = paths_stack.last() { while let Some(prev_entry) = paths_stack.last() {
if let Some(prev_path) = prev_path { if let Some((prev_root_id, prev_path)) = prev_entry {
if !path.starts_with(prev_path) { if root_id != prev_root_id || !path.starts_with(prev_path) {
paths_stack.pop(); paths_stack.pop();
user_settings_stack.pop(); user_settings_stack.pop();
continue; continue;
@ -485,14 +502,17 @@ impl SettingsStore {
if let Some(local_settings) = if let Some(local_settings) =
setting_value.deserialize_setting(&local_settings).log_err() setting_value.deserialize_setting(&local_settings).log_err()
{ {
paths_stack.push(Some(path.as_ref())); paths_stack.push(Some((*root_id, path.as_ref())));
user_settings_stack.push(local_settings); user_settings_stack.push(local_settings);
// If a local settings file changed, then avoid recomputing local // If a local settings file changed, then avoid recomputing local
// settings for any path outside of that directory. // settings for any path outside of that directory.
if changed_local_path.map_or(false, |changed_local_path| { if changed_local_path.map_or(
!path.starts_with(changed_local_path) false,
}) { |(changed_root_id, changed_local_path)| {
*root_id != changed_root_id || !path.starts_with(changed_local_path)
},
) {
continue; continue;
} }
@ -500,7 +520,7 @@ impl SettingsStore {
.load_setting(&default_settings, &user_settings_stack, cx) .load_setting(&default_settings, &user_settings_stack, cx)
.log_err() .log_err()
{ {
setting_value.set_local_value(path.clone(), value); setting_value.set_local_value(*root_id, path.clone(), value);
} }
} }
} }
@ -510,6 +530,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> { impl<T: Setting> AnySettingValue for SettingValue<T> {
fn key(&self) -> Option<&'static str> { fn key(&self) -> Option<&'static str> {
T::KEY T::KEY
@ -546,10 +584,10 @@ impl<T: Setting> AnySettingValue for SettingValue<T> {
Ok(DeserializedSetting(Box::new(value))) Ok(DeserializedSetting(Box::new(value)))
} }
fn value_for_path(&self, path: Option<&Path>) -> &dyn Any { fn value_for_path(&self, path: Option<(usize, &Path)>) -> &dyn Any {
if let Some(path) = path { if let Some((root_id, path)) = path {
for (settings_path, value) in self.local_values.iter().rev() { for (settings_root_id, settings_path, value) in self.local_values.iter().rev() {
if path.starts_with(&settings_path) { if root_id == *settings_root_id && path.starts_with(&settings_path) {
return value; return value;
} }
} }
@ -563,11 +601,14 @@ impl<T: Setting> AnySettingValue for SettingValue<T> {
self.global_value = Some(*value.downcast().unwrap()); 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(); let value = *value.downcast().unwrap();
match self.local_values.binary_search_by_key(&&path, |e| &e.0) { match self
Ok(ix) => self.local_values[ix].1 = value, .local_values
Err(ix) => self.local_values.insert(ix, (path, value)), .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 +925,7 @@ mod tests {
store store
.set_local_settings( .set_local_settings(
1,
Path::new("/root1").into(), Path::new("/root1").into(),
Some(r#"{ "user": { "staff": true } }"#), Some(r#"{ "user": { "staff": true } }"#),
cx, cx,
@ -891,6 +933,7 @@ mod tests {
.unwrap(); .unwrap();
store store
.set_local_settings( .set_local_settings(
1,
Path::new("/root1/subdir").into(), Path::new("/root1/subdir").into(),
Some(r#"{ "user": { "name": "Jane Doe" } }"#), Some(r#"{ "user": { "name": "Jane Doe" } }"#),
cx, cx,
@ -899,6 +942,7 @@ mod tests {
store store
.set_local_settings( .set_local_settings(
1,
Path::new("/root2").into(), Path::new("/root2").into(),
Some(r#"{ "user": { "age": 42 }, "key2": "b" }"#), Some(r#"{ "user": { "age": 42 }, "key2": "b" }"#),
cx, cx,
@ -906,7 +950,7 @@ mod tests {
.unwrap(); .unwrap();
assert_eq!( assert_eq!(
store.get::<UserSettings>(Some(Path::new("/root1/something"))), store.get::<UserSettings>(Some((1, Path::new("/root1/something")))),
&UserSettings { &UserSettings {
name: "John Doe".to_string(), name: "John Doe".to_string(),
age: 31, age: 31,
@ -914,7 +958,7 @@ mod tests {
} }
); );
assert_eq!( assert_eq!(
store.get::<UserSettings>(Some(Path::new("/root1/subdir/something"))), store.get::<UserSettings>(Some((1, Path::new("/root1/subdir/something")))),
&UserSettings { &UserSettings {
name: "Jane Doe".to_string(), name: "Jane Doe".to_string(),
age: 31, age: 31,
@ -922,7 +966,7 @@ mod tests {
} }
); );
assert_eq!( assert_eq!(
store.get::<UserSettings>(Some(Path::new("/root2/something"))), store.get::<UserSettings>(Some((1, Path::new("/root2/something")))),
&UserSettings { &UserSettings {
name: "John Doe".to_string(), name: "John Doe".to_string(),
age: 42, age: 42,
@ -930,7 +974,7 @@ mod tests {
} }
); );
assert_eq!( assert_eq!(
store.get::<MultiKeySettings>(Some(Path::new("/root2/something"))), store.get::<MultiKeySettings>(Some((1, Path::new("/root2/something")))),
&MultiKeySettings { &MultiKeySettings {
key1: "a".to_string(), key1: "a".to_string(),
key2: "b".to_string(), key2: "b".to_string(),

View file

@ -905,7 +905,10 @@ mod tests {
cx: &mut TestAppContext, cx: &mut TestAppContext,
) -> (ModelHandle<Project>, ViewHandle<Workspace>) { ) -> (ModelHandle<Project>, ViewHandle<Workspace>) {
let params = cx.update(AppState::test); 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 project = Project::test(params.fs.clone(), [], cx).await;
let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx)); let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));

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 LAST_USERNAME: PathBuf = CONFIG_DIR.join("last-username.txt");
pub static ref LOG: PathBuf = LOGS_DIR.join("Zed.log"); 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 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 { pub mod legacy {

View file

@ -135,7 +135,10 @@ impl LspAdapter for JsonLspAdapter {
}, },
"schemas": [ "schemas": [
{ {
"fileMatch": [schema_file_match(&paths::SETTINGS)], "fileMatch": [
schema_file_match(&paths::SETTINGS),
&*paths::LOCAL_SETTINGS_RELATIVE_PATH,
],
"schema": settings_schema, "schema": settings_schema,
}, },
{ {

View file

@ -3,7 +3,7 @@ use async_trait::async_trait;
use futures::{future::BoxFuture, FutureExt, StreamExt}; use futures::{future::BoxFuture, FutureExt, StreamExt};
use gpui::AppContext; use gpui::AppContext;
use language::{ use language::{
language_settings::language_settings, LanguageServerBinary, LanguageServerName, LspAdapter, language_settings::all_language_settings, LanguageServerBinary, LanguageServerName, LspAdapter,
}; };
use node_runtime::NodeRuntime; use node_runtime::NodeRuntime;
use serde_json::Value; use serde_json::Value;
@ -101,13 +101,16 @@ impl LspAdapter for YamlLspAdapter {
} }
fn workspace_configuration(&self, cx: &mut AppContext) -> Option<BoxFuture<'static, Value>> { 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( Some(
future::ready(serde_json::json!({ future::ready(serde_json::json!({
"yaml": { "yaml": {
"keyOrdering": false "keyOrdering": false
}, },
"[yaml]": { "[yaml]": {
"editor.tabSize": language_settings(Some("YAML"), cx).tab_size, "editor.tabSize": tab_size,
} }
})) }))
.boxed(), .boxed(),