1use anyhow::Context;
2use collections::HashMap;
3use fs::Fs;
4use gpui::{AppContext, AsyncAppContext, BorrowAppContext, EventEmitter, Model, ModelContext};
5use language::LanguageServerName;
6use paths::{
7 local_settings_file_relative_path, local_tasks_file_relative_path,
8 local_vscode_tasks_file_relative_path, EDITORCONFIG_NAME,
9};
10use rpc::{proto, AnyProtoClient, TypedEnvelope};
11use schemars::JsonSchema;
12use serde::{Deserialize, Serialize};
13use settings::{
14 parse_json_with_comments, InvalidSettingsError, LocalSettingsKind, Settings, SettingsLocation,
15 SettingsSources, SettingsStore,
16};
17use std::{
18 path::{Path, PathBuf},
19 sync::Arc,
20 time::Duration,
21};
22use task::{TaskTemplates, VsCodeTaskFile};
23use util::ResultExt;
24use worktree::{PathChange, UpdatedEntriesSet, Worktree, WorktreeId};
25
26use crate::{
27 task_store::TaskStore,
28 worktree_store::{WorktreeStore, WorktreeStoreEvent},
29};
30
31#[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema)]
32pub struct ProjectSettings {
33 /// Configuration for language servers.
34 ///
35 /// The following settings can be overridden for specific language servers:
36 /// - initialization_options
37 ///
38 /// To override settings for a language, add an entry for that language server's
39 /// name to the lsp value.
40 /// Default: null
41 #[serde(default)]
42 pub lsp: HashMap<LanguageServerName, LspSettings>,
43
44 /// Configuration for Git-related features
45 #[serde(default)]
46 pub git: GitSettings,
47
48 /// Configuration for Node-related features
49 #[serde(default)]
50 pub node: NodeBinarySettings,
51
52 /// Configuration for how direnv configuration should be loaded
53 #[serde(default)]
54 pub load_direnv: DirenvSettings,
55
56 /// Configuration for session-related features
57 #[serde(default)]
58 pub session: SessionSettings,
59}
60
61#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize, JsonSchema)]
62pub struct NodeBinarySettings {
63 /// The path to the node binary
64 pub path: Option<String>,
65 /// The path to the npm binary Zed should use (defaults to .path/../npm)
66 pub npm_path: Option<String>,
67 /// If disabled, zed will download its own copy of node.
68 #[serde(default)]
69 pub ignore_system_version: Option<bool>,
70}
71
72#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema)]
73#[serde(rename_all = "snake_case")]
74pub enum DirenvSettings {
75 /// Load direnv configuration through a shell hook
76 ShellHook,
77 /// Load direnv configuration directly using `direnv export json`
78 #[default]
79 Direct,
80}
81
82#[derive(Copy, Clone, Debug, Default, Serialize, Deserialize, JsonSchema)]
83pub struct GitSettings {
84 /// Whether or not to show the git gutter.
85 ///
86 /// Default: tracked_files
87 pub git_gutter: Option<GitGutterSetting>,
88 pub gutter_debounce: Option<u64>,
89 /// Whether or not to show git blame data inline in
90 /// the currently focused line.
91 ///
92 /// Default: on
93 pub inline_blame: Option<InlineBlameSettings>,
94}
95
96impl GitSettings {
97 pub fn inline_blame_enabled(&self) -> bool {
98 #[allow(unknown_lints, clippy::manual_unwrap_or_default)]
99 match self.inline_blame {
100 Some(InlineBlameSettings { enabled, .. }) => enabled,
101 _ => false,
102 }
103 }
104
105 pub fn inline_blame_delay(&self) -> Option<Duration> {
106 match self.inline_blame {
107 Some(InlineBlameSettings {
108 delay_ms: Some(delay_ms),
109 ..
110 }) if delay_ms > 0 => Some(Duration::from_millis(delay_ms)),
111 _ => None,
112 }
113 }
114}
115
116#[derive(Clone, Copy, Debug, Default, Serialize, Deserialize, JsonSchema)]
117#[serde(rename_all = "snake_case")]
118pub enum GitGutterSetting {
119 /// Show git gutter in tracked files.
120 #[default]
121 TrackedFiles,
122 /// Hide git gutter
123 Hide,
124}
125
126#[derive(Clone, Copy, Debug, Default, Serialize, Deserialize, JsonSchema)]
127#[serde(rename_all = "snake_case")]
128pub struct InlineBlameSettings {
129 /// Whether or not to show git blame data inline in
130 /// the currently focused line.
131 ///
132 /// Default: true
133 #[serde(default = "true_value")]
134 pub enabled: bool,
135 /// Whether to only show the inline blame information
136 /// after a delay once the cursor stops moving.
137 ///
138 /// Default: 0
139 pub delay_ms: Option<u64>,
140 /// The minimum column number to show the inline blame information at
141 ///
142 /// Default: 0
143 pub min_column: Option<u32>,
144}
145
146const fn true_value() -> bool {
147 true
148}
149
150#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
151pub struct BinarySettings {
152 pub path: Option<String>,
153 pub arguments: Option<Vec<String>>,
154 pub ignore_system_version: Option<bool>,
155}
156
157#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
158#[serde(rename_all = "snake_case")]
159pub struct LspSettings {
160 pub binary: Option<BinarySettings>,
161 pub initialization_options: Option<serde_json::Value>,
162 pub settings: Option<serde_json::Value>,
163}
164
165#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema)]
166pub struct SessionSettings {
167 /// Whether or not to restore unsaved buffers on restart.
168 ///
169 /// If this is true, user won't be prompted whether to save/discard
170 /// dirty files when closing the application.
171 ///
172 /// Default: true
173 pub restore_unsaved_buffers: bool,
174}
175
176impl Default for SessionSettings {
177 fn default() -> Self {
178 Self {
179 restore_unsaved_buffers: true,
180 }
181 }
182}
183
184impl Settings for ProjectSettings {
185 const KEY: Option<&'static str> = None;
186
187 type FileContent = Self;
188
189 fn load(
190 sources: SettingsSources<Self::FileContent>,
191 _: &mut AppContext,
192 ) -> anyhow::Result<Self> {
193 sources.json_merge()
194 }
195}
196
197pub enum SettingsObserverMode {
198 Local(Arc<dyn Fs>),
199 Ssh(AnyProtoClient),
200 Remote,
201}
202
203#[derive(Clone, Debug, PartialEq)]
204pub enum SettingsObserverEvent {
205 LocalSettingsUpdated(Result<(), InvalidSettingsError>),
206}
207
208impl EventEmitter<SettingsObserverEvent> for SettingsObserver {}
209
210pub struct SettingsObserver {
211 mode: SettingsObserverMode,
212 downstream_client: Option<AnyProtoClient>,
213 worktree_store: Model<WorktreeStore>,
214 project_id: u64,
215 task_store: Model<TaskStore>,
216}
217
218/// SettingsObserver observers changes to .zed/{settings, task}.json files in local worktrees
219/// (or the equivalent protobuf messages from upstream) and updates local settings
220/// and sends notifications downstream.
221/// In ssh mode it also monitors ~/.config/zed/{settings, task}.json and sends the content
222/// upstream.
223impl SettingsObserver {
224 pub fn init(client: &AnyProtoClient) {
225 client.add_model_message_handler(Self::handle_update_worktree_settings);
226 client.add_model_message_handler(Self::handle_update_user_settings)
227 }
228
229 pub fn new_local(
230 fs: Arc<dyn Fs>,
231 worktree_store: Model<WorktreeStore>,
232 task_store: Model<TaskStore>,
233 cx: &mut ModelContext<Self>,
234 ) -> Self {
235 cx.subscribe(&worktree_store, Self::on_worktree_store_event)
236 .detach();
237
238 Self {
239 worktree_store,
240 task_store,
241 mode: SettingsObserverMode::Local(fs),
242 downstream_client: None,
243 project_id: 0,
244 }
245 }
246
247 pub fn new_ssh(
248 client: AnyProtoClient,
249 worktree_store: Model<WorktreeStore>,
250 task_store: Model<TaskStore>,
251 cx: &mut ModelContext<Self>,
252 ) -> Self {
253 let this = Self {
254 worktree_store,
255 task_store,
256 mode: SettingsObserverMode::Ssh(client.clone()),
257 downstream_client: None,
258 project_id: 0,
259 };
260 this.maintain_ssh_settings(client, cx);
261 this
262 }
263
264 pub fn new_remote(
265 worktree_store: Model<WorktreeStore>,
266 task_store: Model<TaskStore>,
267 _: &mut ModelContext<Self>,
268 ) -> Self {
269 Self {
270 worktree_store,
271 task_store,
272 mode: SettingsObserverMode::Remote,
273 downstream_client: None,
274 project_id: 0,
275 }
276 }
277
278 pub fn shared(
279 &mut self,
280 project_id: u64,
281 downstream_client: AnyProtoClient,
282 cx: &mut ModelContext<Self>,
283 ) {
284 self.project_id = project_id;
285 self.downstream_client = Some(downstream_client.clone());
286
287 let store = cx.global::<SettingsStore>();
288 for worktree in self.worktree_store.read(cx).worktrees() {
289 let worktree_id = worktree.read(cx).id().to_proto();
290 for (path, content) in store.local_settings(worktree.read(cx).id()) {
291 downstream_client
292 .send(proto::UpdateWorktreeSettings {
293 project_id,
294 worktree_id,
295 path: path.to_string_lossy().into(),
296 content: Some(content),
297 kind: Some(
298 local_settings_kind_to_proto(LocalSettingsKind::Settings).into(),
299 ),
300 })
301 .log_err();
302 }
303 for (path, content, _) in store.local_editorconfig_settings(worktree.read(cx).id()) {
304 downstream_client
305 .send(proto::UpdateWorktreeSettings {
306 project_id,
307 worktree_id,
308 path: path.to_string_lossy().into(),
309 content: Some(content),
310 kind: Some(
311 local_settings_kind_to_proto(LocalSettingsKind::Editorconfig).into(),
312 ),
313 })
314 .log_err();
315 }
316 }
317 }
318
319 pub fn unshared(&mut self, _: &mut ModelContext<Self>) {
320 self.downstream_client = None;
321 }
322
323 async fn handle_update_worktree_settings(
324 this: Model<Self>,
325 envelope: TypedEnvelope<proto::UpdateWorktreeSettings>,
326 mut cx: AsyncAppContext,
327 ) -> anyhow::Result<()> {
328 let kind = match envelope.payload.kind {
329 Some(kind) => proto::LocalSettingsKind::from_i32(kind)
330 .with_context(|| format!("unknown kind {kind}"))?,
331 None => proto::LocalSettingsKind::Settings,
332 };
333 this.update(&mut cx, |this, cx| {
334 let worktree_id = WorktreeId::from_proto(envelope.payload.worktree_id);
335 let Some(worktree) = this
336 .worktree_store
337 .read(cx)
338 .worktree_for_id(worktree_id, cx)
339 else {
340 return;
341 };
342
343 this.update_settings(
344 worktree,
345 [(
346 PathBuf::from(&envelope.payload.path).into(),
347 local_settings_kind_from_proto(kind),
348 envelope.payload.content,
349 )],
350 cx,
351 );
352 })?;
353 Ok(())
354 }
355
356 pub async fn handle_update_user_settings(
357 settings_observer: Model<Self>,
358 envelope: TypedEnvelope<proto::UpdateUserSettings>,
359 mut cx: AsyncAppContext,
360 ) -> anyhow::Result<()> {
361 match envelope.payload.kind() {
362 proto::update_user_settings::Kind::Settings => {
363 cx.update_global(move |settings_store: &mut SettingsStore, cx| {
364 settings_store.set_user_settings(&envelope.payload.content, cx)
365 })
366 }
367 proto::update_user_settings::Kind::Tasks => {
368 settings_observer.update(&mut cx, |settings_observer, cx| {
369 settings_observer.task_store.update(cx, |task_store, cx| {
370 task_store.update_user_tasks(None, Some(&envelope.payload.content), cx)
371 })
372 })
373 }
374 }??;
375
376 Ok(())
377 }
378
379 pub fn maintain_ssh_settings(&self, ssh: AnyProtoClient, cx: &mut ModelContext<Self>) {
380 let settings_store = cx.global::<SettingsStore>();
381
382 let mut settings = settings_store.raw_user_settings().clone();
383 if let Some(content) = serde_json::to_string(&settings).log_err() {
384 ssh.send(proto::UpdateUserSettings {
385 project_id: 0,
386 content,
387 kind: Some(proto::LocalSettingsKind::Settings.into()),
388 })
389 .log_err();
390 }
391
392 let weak_client = ssh.downgrade();
393 cx.observe_global::<SettingsStore>(move |_, cx| {
394 let new_settings = cx.global::<SettingsStore>().raw_user_settings();
395 if &settings != new_settings {
396 settings = new_settings.clone()
397 }
398 if let Some(content) = serde_json::to_string(&settings).log_err() {
399 if let Some(ssh) = weak_client.upgrade() {
400 ssh.send(proto::UpdateUserSettings {
401 project_id: 0,
402 content,
403 kind: Some(proto::LocalSettingsKind::Settings.into()),
404 })
405 .log_err();
406 }
407 }
408 })
409 .detach();
410 }
411
412 fn on_worktree_store_event(
413 &mut self,
414 _: Model<WorktreeStore>,
415 event: &WorktreeStoreEvent,
416 cx: &mut ModelContext<Self>,
417 ) {
418 if let WorktreeStoreEvent::WorktreeAdded(worktree) = event {
419 cx.subscribe(worktree, |this, worktree, event, cx| {
420 if let worktree::Event::UpdatedEntries(changes) = event {
421 this.update_local_worktree_settings(&worktree, changes, cx)
422 }
423 })
424 .detach()
425 }
426 }
427
428 fn update_local_worktree_settings(
429 &mut self,
430 worktree: &Model<Worktree>,
431 changes: &UpdatedEntriesSet,
432 cx: &mut ModelContext<Self>,
433 ) {
434 let SettingsObserverMode::Local(fs) = &self.mode else {
435 return;
436 };
437
438 let mut settings_contents = Vec::new();
439 for (path, _, change) in changes.iter() {
440 let (settings_dir, kind) = if path.ends_with(local_settings_file_relative_path()) {
441 let settings_dir = Arc::<Path>::from(
442 path.ancestors()
443 .nth(local_settings_file_relative_path().components().count())
444 .unwrap(),
445 );
446 (settings_dir, LocalSettingsKind::Settings)
447 } else if path.ends_with(local_tasks_file_relative_path()) {
448 let settings_dir = Arc::<Path>::from(
449 path.ancestors()
450 .nth(
451 local_tasks_file_relative_path()
452 .components()
453 .count()
454 .saturating_sub(1),
455 )
456 .unwrap(),
457 );
458 (settings_dir, LocalSettingsKind::Tasks)
459 } else if path.ends_with(local_vscode_tasks_file_relative_path()) {
460 let settings_dir = Arc::<Path>::from(
461 path.ancestors()
462 .nth(
463 local_vscode_tasks_file_relative_path()
464 .components()
465 .count()
466 .saturating_sub(1),
467 )
468 .unwrap(),
469 );
470 (settings_dir, LocalSettingsKind::Tasks)
471 } else if path.ends_with(EDITORCONFIG_NAME) {
472 let Some(settings_dir) = path.parent().map(Arc::from) else {
473 continue;
474 };
475 (settings_dir, LocalSettingsKind::Editorconfig)
476 } else {
477 continue;
478 };
479
480 let removed = change == &PathChange::Removed;
481 let fs = fs.clone();
482 let abs_path = match worktree.read(cx).absolutize(path) {
483 Ok(abs_path) => abs_path,
484 Err(e) => {
485 log::warn!("Cannot absolutize {path:?} received as {change:?} FS change: {e}");
486 continue;
487 }
488 };
489 settings_contents.push(async move {
490 (
491 settings_dir,
492 kind,
493 if removed {
494 None
495 } else {
496 Some(
497 async move {
498 let content = fs.load(&abs_path).await?;
499 if abs_path.ends_with(local_vscode_tasks_file_relative_path()) {
500 let vscode_tasks =
501 parse_json_with_comments::<VsCodeTaskFile>(&content)
502 .with_context(|| {
503 format!("parsing VSCode tasks, file {abs_path:?}")
504 })?;
505 let zed_tasks = TaskTemplates::try_from(vscode_tasks)
506 .with_context(|| {
507 format!(
508 "converting VSCode tasks into Zed ones, file {abs_path:?}"
509 )
510 })?;
511 serde_json::to_string(&zed_tasks).with_context(|| {
512 format!(
513 "serializing Zed tasks into JSON, file {abs_path:?}"
514 )
515 })
516 } else {
517 Ok(content)
518 }
519 }
520 .await,
521 )
522 },
523 )
524 });
525 }
526
527 if settings_contents.is_empty() {
528 return;
529 }
530
531 let worktree = worktree.clone();
532 cx.spawn(move |this, cx| async move {
533 let settings_contents: Vec<(Arc<Path>, _, _)> =
534 futures::future::join_all(settings_contents).await;
535 cx.update(|cx| {
536 this.update(cx, |this, cx| {
537 this.update_settings(
538 worktree,
539 settings_contents.into_iter().map(|(path, kind, content)| {
540 (path, kind, content.and_then(|c| c.log_err()))
541 }),
542 cx,
543 )
544 })
545 })
546 })
547 .detach();
548 }
549
550 fn update_settings(
551 &mut self,
552 worktree: Model<Worktree>,
553 settings_contents: impl IntoIterator<Item = (Arc<Path>, LocalSettingsKind, Option<String>)>,
554 cx: &mut ModelContext<Self>,
555 ) {
556 let worktree_id = worktree.read(cx).id();
557 let remote_worktree_id = worktree.read(cx).id();
558 let task_store = self.task_store.clone();
559
560 for (directory, kind, file_content) in settings_contents {
561 match kind {
562 LocalSettingsKind::Settings | LocalSettingsKind::Editorconfig => cx
563 .update_global::<SettingsStore, _>(|store, cx| {
564 let result = store.set_local_settings(
565 worktree_id,
566 directory.clone(),
567 kind,
568 file_content.as_deref(),
569 cx,
570 );
571
572 match result {
573 Err(InvalidSettingsError::LocalSettings { path, message }) => {
574 log::error!(
575 "Failed to set local settings in {:?}: {:?}",
576 path,
577 message
578 );
579 cx.emit(SettingsObserverEvent::LocalSettingsUpdated(Err(
580 InvalidSettingsError::LocalSettings { path, message },
581 )));
582 }
583 Err(e) => {
584 log::error!("Failed to set local settings: {e}");
585 }
586 Ok(_) => {
587 cx.emit(SettingsObserverEvent::LocalSettingsUpdated(Ok(())));
588 }
589 }
590 }),
591 LocalSettingsKind::Tasks => task_store.update(cx, |task_store, cx| {
592 task_store
593 .update_user_tasks(
594 Some(SettingsLocation {
595 worktree_id,
596 path: directory.as_ref(),
597 }),
598 file_content.as_deref(),
599 cx,
600 )
601 .log_err();
602 }),
603 };
604
605 if let Some(downstream_client) = &self.downstream_client {
606 downstream_client
607 .send(proto::UpdateWorktreeSettings {
608 project_id: self.project_id,
609 worktree_id: remote_worktree_id.to_proto(),
610 path: directory.to_string_lossy().into_owned(),
611 content: file_content,
612 kind: Some(local_settings_kind_to_proto(kind).into()),
613 })
614 .log_err();
615 }
616 }
617 }
618}
619
620pub fn local_settings_kind_from_proto(kind: proto::LocalSettingsKind) -> LocalSettingsKind {
621 match kind {
622 proto::LocalSettingsKind::Settings => LocalSettingsKind::Settings,
623 proto::LocalSettingsKind::Tasks => LocalSettingsKind::Tasks,
624 proto::LocalSettingsKind::Editorconfig => LocalSettingsKind::Editorconfig,
625 }
626}
627
628pub fn local_settings_kind_to_proto(kind: LocalSettingsKind) -> proto::LocalSettingsKind {
629 match kind {
630 LocalSettingsKind::Settings => proto::LocalSettingsKind::Settings,
631 LocalSettingsKind::Tasks => proto::LocalSettingsKind::Tasks,
632 LocalSettingsKind::Editorconfig => proto::LocalSettingsKind::Editorconfig,
633 }
634}