1use collections::HashMap;
2use fs::Fs;
3use gpui::{AppContext, AsyncAppContext, BorrowAppContext, Model, ModelContext};
4use paths::local_settings_file_relative_path;
5use rpc::{proto, AnyProtoClient, TypedEnvelope};
6use schemars::JsonSchema;
7use serde::{Deserialize, Serialize};
8use settings::{Settings, SettingsSources, SettingsStore};
9use std::{
10 path::{Path, PathBuf},
11 sync::Arc,
12 time::Duration,
13};
14use util::ResultExt;
15use worktree::{PathChange, UpdatedEntriesSet, Worktree, WorktreeId};
16
17use crate::worktree_store::{WorktreeStore, WorktreeStoreEvent};
18
19#[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema)]
20pub struct ProjectSettings {
21 /// Configuration for language servers.
22 ///
23 /// The following settings can be overridden for specific language servers:
24 /// - initialization_options
25 ///
26 /// To override settings for a language, add an entry for that language server's
27 /// name to the lsp value.
28 /// Default: null
29 #[serde(default)]
30 pub lsp: HashMap<Arc<str>, LspSettings>,
31
32 /// Configuration for Git-related features
33 #[serde(default)]
34 pub git: GitSettings,
35
36 /// Configuration for how direnv configuration should be loaded
37 #[serde(default)]
38 pub load_direnv: DirenvSettings,
39
40 /// Configuration for session-related features
41 #[serde(default)]
42 pub session: SessionSettings,
43}
44
45#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema)]
46#[serde(rename_all = "snake_case")]
47pub enum DirenvSettings {
48 /// Load direnv configuration through a shell hook
49 #[default]
50 ShellHook,
51 /// Load direnv configuration directly using `direnv export json`
52 ///
53 /// Warning: This option is experimental and might cause some inconsistent behavior compared to using the shell hook.
54 /// If it does, please report it to GitHub
55 Direct,
56}
57
58#[derive(Copy, Clone, Debug, Default, Serialize, Deserialize, JsonSchema)]
59pub struct GitSettings {
60 /// Whether or not to show the git gutter.
61 ///
62 /// Default: tracked_files
63 pub git_gutter: Option<GitGutterSetting>,
64 pub gutter_debounce: Option<u64>,
65 /// Whether or not to show git blame data inline in
66 /// the currently focused line.
67 ///
68 /// Default: on
69 pub inline_blame: Option<InlineBlameSettings>,
70}
71
72impl GitSettings {
73 pub fn inline_blame_enabled(&self) -> bool {
74 #[allow(unknown_lints, clippy::manual_unwrap_or_default)]
75 match self.inline_blame {
76 Some(InlineBlameSettings { enabled, .. }) => enabled,
77 _ => false,
78 }
79 }
80
81 pub fn inline_blame_delay(&self) -> Option<Duration> {
82 match self.inline_blame {
83 Some(InlineBlameSettings {
84 delay_ms: Some(delay_ms),
85 ..
86 }) if delay_ms > 0 => Some(Duration::from_millis(delay_ms)),
87 _ => None,
88 }
89 }
90}
91
92#[derive(Clone, Copy, Debug, Default, Serialize, Deserialize, JsonSchema)]
93#[serde(rename_all = "snake_case")]
94pub enum GitGutterSetting {
95 /// Show git gutter in tracked files.
96 #[default]
97 TrackedFiles,
98 /// Hide git gutter
99 Hide,
100}
101
102#[derive(Clone, Copy, Debug, Default, Serialize, Deserialize, JsonSchema)]
103#[serde(rename_all = "snake_case")]
104pub struct InlineBlameSettings {
105 /// Whether or not to show git blame data inline in
106 /// the currently focused line.
107 ///
108 /// Default: true
109 #[serde(default = "true_value")]
110 pub enabled: bool,
111 /// Whether to only show the inline blame information
112 /// after a delay once the cursor stops moving.
113 ///
114 /// Default: 0
115 pub delay_ms: Option<u64>,
116 /// The minimum column number to show the inline blame information at
117 ///
118 /// Default: 0
119 pub min_column: Option<u32>,
120}
121
122const fn true_value() -> bool {
123 true
124}
125
126#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
127pub struct BinarySettings {
128 pub path: Option<String>,
129 pub arguments: Option<Vec<String>>,
130 pub path_lookup: Option<bool>,
131}
132
133#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
134#[serde(rename_all = "snake_case")]
135pub struct LspSettings {
136 pub binary: Option<BinarySettings>,
137 pub initialization_options: Option<serde_json::Value>,
138 pub settings: Option<serde_json::Value>,
139}
140
141#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema)]
142pub struct SessionSettings {
143 /// Whether or not to restore unsaved buffers on restart.
144 ///
145 /// If this is true, user won't be prompted whether to save/discard
146 /// dirty files when closing the application.
147 ///
148 /// Default: true
149 pub restore_unsaved_buffers: bool,
150}
151
152impl Default for SessionSettings {
153 fn default() -> Self {
154 Self {
155 restore_unsaved_buffers: true,
156 }
157 }
158}
159
160impl Settings for ProjectSettings {
161 const KEY: Option<&'static str> = None;
162
163 type FileContent = Self;
164
165 fn load(
166 sources: SettingsSources<Self::FileContent>,
167 _: &mut AppContext,
168 ) -> anyhow::Result<Self> {
169 sources.json_merge()
170 }
171}
172
173pub enum SettingsObserverMode {
174 Local(Arc<dyn Fs>),
175 Ssh(AnyProtoClient),
176 Remote,
177}
178
179pub struct SettingsObserver {
180 mode: SettingsObserverMode,
181 downstream_client: Option<AnyProtoClient>,
182 worktree_store: Model<WorktreeStore>,
183 project_id: u64,
184}
185
186/// SettingsObserver observers changes to .zed/settings.json files in local worktrees
187/// (or the equivalent protobuf messages from upstream) and updates local settings
188/// and sends notifications downstream.
189/// In ssh mode it also monitors ~/.config/zed/settings.json and sends the content
190/// upstream.
191impl SettingsObserver {
192 pub fn init(client: &AnyProtoClient) {
193 client.add_model_message_handler(Self::handle_update_worktree_settings);
194 client.add_model_message_handler(Self::handle_update_user_settings)
195 }
196
197 pub fn new_local(
198 fs: Arc<dyn Fs>,
199 worktree_store: Model<WorktreeStore>,
200 cx: &mut ModelContext<Self>,
201 ) -> Self {
202 cx.subscribe(&worktree_store, Self::on_worktree_store_event)
203 .detach();
204
205 Self {
206 worktree_store,
207 mode: SettingsObserverMode::Local(fs),
208 downstream_client: None,
209 project_id: 0,
210 }
211 }
212
213 pub fn new_ssh(
214 client: AnyProtoClient,
215 worktree_store: Model<WorktreeStore>,
216 cx: &mut ModelContext<Self>,
217 ) -> Self {
218 let this = Self {
219 worktree_store,
220 mode: SettingsObserverMode::Ssh(client.clone()),
221 downstream_client: None,
222 project_id: 0,
223 };
224 this.maintain_ssh_settings(client, cx);
225 this
226 }
227
228 pub fn new_remote(worktree_store: Model<WorktreeStore>, _: &mut ModelContext<Self>) -> Self {
229 Self {
230 worktree_store,
231 mode: SettingsObserverMode::Remote,
232 downstream_client: None,
233 project_id: 0,
234 }
235 }
236
237 pub fn shared(
238 &mut self,
239 project_id: u64,
240 downstream_client: AnyProtoClient,
241 cx: &mut ModelContext<Self>,
242 ) {
243 self.project_id = project_id;
244 self.downstream_client = Some(downstream_client.clone());
245
246 let store = cx.global::<SettingsStore>();
247 for worktree in self.worktree_store.read(cx).worktrees() {
248 let worktree_id = worktree.read(cx).id().to_proto();
249 for (path, content) in store.local_settings(worktree.read(cx).id()) {
250 downstream_client
251 .send(proto::UpdateWorktreeSettings {
252 project_id,
253 worktree_id,
254 path: path.to_string_lossy().into(),
255 content: Some(content),
256 })
257 .log_err();
258 }
259 }
260 }
261
262 pub fn unshared(&mut self, _: &mut ModelContext<Self>) {
263 self.downstream_client = None;
264 }
265
266 async fn handle_update_worktree_settings(
267 this: Model<Self>,
268 envelope: TypedEnvelope<proto::UpdateWorktreeSettings>,
269 mut cx: AsyncAppContext,
270 ) -> anyhow::Result<()> {
271 this.update(&mut cx, |this, cx| {
272 let worktree_id = WorktreeId::from_proto(envelope.payload.worktree_id);
273 let Some(worktree) = this
274 .worktree_store
275 .read(cx)
276 .worktree_for_id(worktree_id, cx)
277 else {
278 return;
279 };
280 this.update_settings(
281 worktree,
282 [(
283 PathBuf::from(&envelope.payload.path).into(),
284 envelope.payload.content,
285 )],
286 cx,
287 );
288 })?;
289 Ok(())
290 }
291
292 pub async fn handle_update_user_settings(
293 _: Model<Self>,
294 envelope: TypedEnvelope<proto::UpdateUserSettings>,
295 mut cx: AsyncAppContext,
296 ) -> anyhow::Result<()> {
297 cx.update_global(move |settings_store: &mut SettingsStore, cx| {
298 settings_store.set_user_settings(&envelope.payload.content, cx)
299 })??;
300
301 Ok(())
302 }
303
304 pub fn maintain_ssh_settings(&self, ssh: AnyProtoClient, cx: &mut ModelContext<Self>) {
305 let mut settings = cx.global::<SettingsStore>().raw_user_settings().clone();
306 if let Some(content) = serde_json::to_string(&settings).log_err() {
307 ssh.send(proto::UpdateUserSettings {
308 project_id: 0,
309 content,
310 })
311 .log_err();
312 }
313
314 cx.observe_global::<SettingsStore>(move |_, cx| {
315 let new_settings = cx.global::<SettingsStore>().raw_user_settings();
316 if &settings != new_settings {
317 settings = new_settings.clone()
318 }
319 if let Some(content) = serde_json::to_string(&settings).log_err() {
320 ssh.send(proto::UpdateUserSettings {
321 project_id: 0,
322 content,
323 })
324 .log_err();
325 }
326 })
327 .detach();
328 }
329
330 fn on_worktree_store_event(
331 &mut self,
332 _: Model<WorktreeStore>,
333 event: &WorktreeStoreEvent,
334 cx: &mut ModelContext<Self>,
335 ) {
336 if let WorktreeStoreEvent::WorktreeAdded(worktree) = event {
337 cx.subscribe(worktree, |this, worktree, event, cx| {
338 if let worktree::Event::UpdatedEntries(changes) = event {
339 this.update_local_worktree_settings(&worktree, changes, cx)
340 }
341 })
342 .detach()
343 }
344 }
345
346 fn update_local_worktree_settings(
347 &mut self,
348 worktree: &Model<Worktree>,
349 changes: &UpdatedEntriesSet,
350 cx: &mut ModelContext<Self>,
351 ) {
352 let SettingsObserverMode::Local(fs) = &self.mode else {
353 return;
354 };
355
356 let mut settings_contents = Vec::new();
357 for (path, _, change) in changes.iter() {
358 let removed = change == &PathChange::Removed;
359 let abs_path = match worktree.read(cx).absolutize(path) {
360 Ok(abs_path) => abs_path,
361 Err(e) => {
362 log::warn!("Cannot absolutize {path:?} received as {change:?} FS change: {e}");
363 continue;
364 }
365 };
366
367 if path.ends_with(local_settings_file_relative_path()) {
368 let settings_dir = Arc::from(
369 path.ancestors()
370 .nth(local_settings_file_relative_path().components().count())
371 .unwrap(),
372 );
373 let fs = fs.clone();
374 settings_contents.push(async move {
375 (
376 settings_dir,
377 if removed {
378 None
379 } else {
380 Some(async move { fs.load(&abs_path).await }.await)
381 },
382 )
383 });
384 }
385 }
386
387 if settings_contents.is_empty() {
388 return;
389 }
390
391 let worktree = worktree.clone();
392 cx.spawn(move |this, cx| async move {
393 let settings_contents: Vec<(Arc<Path>, _)> =
394 futures::future::join_all(settings_contents).await;
395 cx.update(|cx| {
396 this.update(cx, |this, cx| {
397 this.update_settings(
398 worktree,
399 settings_contents
400 .into_iter()
401 .map(|(path, content)| (path, content.and_then(|c| c.log_err()))),
402 cx,
403 )
404 })
405 })
406 })
407 .detach();
408 }
409
410 fn update_settings(
411 &mut self,
412 worktree: Model<Worktree>,
413 settings_contents: impl IntoIterator<Item = (Arc<Path>, Option<String>)>,
414 cx: &mut ModelContext<Self>,
415 ) {
416 let worktree_id = worktree.read(cx).id();
417 let remote_worktree_id = worktree.read(cx).id();
418 cx.update_global::<SettingsStore, _>(|store, cx| {
419 for (directory, file_content) in settings_contents {
420 store
421 .set_local_settings(worktree_id, directory.clone(), file_content.as_deref(), cx)
422 .log_err();
423 if let Some(downstream_client) = &self.downstream_client {
424 downstream_client
425 .send(proto::UpdateWorktreeSettings {
426 project_id: self.project_id,
427 worktree_id: remote_worktree_id.to_proto(),
428 path: directory.to_string_lossy().into_owned(),
429 content: file_content,
430 })
431 .log_err();
432 }
433 }
434 })
435 }
436}