1use anyhow::Context as _;
2use collections::HashMap;
3use context_server::ContextServerCommand;
4use dap::adapters::DebugAdapterName;
5use fs::Fs;
6use futures::StreamExt as _;
7use gpui::{App, AsyncApp, BorrowAppContext, Context, Entity, EventEmitter, Subscription, Task};
8use lsp::LanguageServerName;
9use paths::{
10 EDITORCONFIG_NAME, local_debug_file_relative_path, local_settings_file_relative_path,
11 local_tasks_file_relative_path, local_vscode_launch_file_relative_path,
12 local_vscode_tasks_file_relative_path, task_file_name,
13};
14use rpc::{
15 AnyProtoClient, TypedEnvelope,
16 proto::{self, FromProto, REMOTE_SERVER_PROJECT_ID, ToProto},
17};
18use schemars::JsonSchema;
19use serde::{Deserialize, Serialize};
20pub use settings::DirenvSettings;
21pub use settings::LspSettings;
22use settings::{
23 InvalidSettingsError, LocalSettingsKind, Settings, SettingsLocation, SettingsStore, SettingsUi,
24 parse_json_with_comments, watch_config_file,
25};
26use std::{
27 path::{Path, PathBuf},
28 sync::Arc,
29 time::Duration,
30};
31use task::{DebugTaskFile, TaskTemplates, VsCodeDebugTaskFile, VsCodeTaskFile};
32use util::{MergeFrom as _, ResultExt, serde::default_true};
33use worktree::{PathChange, UpdatedEntriesSet, Worktree, WorktreeId};
34
35use crate::{
36 task_store::{TaskSettingsLocation, TaskStore},
37 worktree_store::{WorktreeStore, WorktreeStoreEvent},
38};
39
40#[derive(Debug, Clone)]
41pub struct ProjectSettings {
42 /// Configuration for language servers.
43 ///
44 /// The following settings can be overridden for specific language servers:
45 /// - initialization_options
46 ///
47 /// To override settings for a language, add an entry for that language server's
48 /// name to the lsp value.
49 /// Default: null
50 // todo! should these hash map types be Map<key, SettingsContent> or Map<Key, Settings>
51 pub lsp: HashMap<LanguageServerName, settings::LspSettings>,
52
53 /// Common language server settings.
54 pub global_lsp_settings: GlobalLspSettings,
55
56 /// Configuration for Debugger-related features
57 pub dap: HashMap<DebugAdapterName, settings::DapSettings>,
58
59 /// Settings for context servers used for AI-related features.
60 pub context_servers: HashMap<Arc<str>, ContextServerSettings>,
61
62 /// Configuration for Diagnostics-related features.
63 pub diagnostics: DiagnosticsSettings,
64
65 /// Configuration for Git-related features
66 pub git: GitSettings,
67
68 /// Configuration for Node-related features
69 pub node: NodeBinarySettings,
70
71 /// Configuration for how direnv configuration should be loaded
72 pub load_direnv: DirenvSettings,
73
74 /// Configuration for session-related features
75 pub session: SessionSettings,
76}
77
78#[derive(Copy, Clone, Debug)]
79pub struct SessionSettings {
80 /// Whether or not to restore unsaved buffers on restart.
81 ///
82 /// If this is true, user won't be prompted whether to save/discard
83 /// dirty files when closing the application.
84 ///
85 /// Default: true
86 pub restore_unsaved_buffers: bool,
87}
88
89#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize, JsonSchema)]
90pub struct NodeBinarySettings {
91 /// The path to the Node binary.
92 pub path: Option<String>,
93 /// The path to the npm binary Zed should use (defaults to `.path/../npm`).
94 pub npm_path: Option<String>,
95 /// If enabled, Zed will download its own copy of Node.
96 pub ignore_system_version: bool,
97}
98
99impl From<settings::NodeBinarySettings> for NodeBinarySettings {
100 fn from(settings: settings::NodeBinarySettings) -> Self {
101 Self {
102 path: settings.path,
103 npm_path: settings.npm_path,
104 ignore_system_version: settings.ignore_system_version.unwrap_or(false),
105 }
106 }
107}
108
109/// Common language server settings.
110#[derive(Debug, Clone, PartialEq)]
111pub struct GlobalLspSettings {
112 /// Whether to show the LSP servers button in the status bar.
113 ///
114 /// Default: `true`
115 pub button: bool,
116}
117
118#[derive(Deserialize, Serialize, Clone, PartialEq, Eq, JsonSchema, Debug)]
119#[serde(tag = "source", rename_all = "snake_case")]
120pub enum ContextServerSettings {
121 Custom {
122 /// Whether the context server is enabled.
123 #[serde(default = "default_true")]
124 enabled: bool,
125
126 #[serde(flatten)]
127 command: ContextServerCommand,
128 },
129 Extension {
130 /// Whether the context server is enabled.
131 #[serde(default = "default_true")]
132 enabled: bool,
133 /// The settings for this context server specified by the extension.
134 ///
135 /// Consult the documentation for the context server to see what settings
136 /// are supported.
137 settings: serde_json::Value,
138 },
139}
140
141impl From<settings::ContextServerSettingsContent> for ContextServerSettings {
142 fn from(value: settings::ContextServerSettingsContent) -> Self {
143 match value {
144 settings::ContextServerSettingsContent::Custom { enabled, command } => {
145 ContextServerSettings::Custom { enabled, command }
146 }
147 settings::ContextServerSettingsContent::Extension { enabled, settings } => {
148 ContextServerSettings::Extension { enabled, settings }
149 }
150 }
151 }
152}
153impl Into<settings::ContextServerSettingsContent> for ContextServerSettings {
154 fn into(self) -> settings::ContextServerSettingsContent {
155 match self {
156 ContextServerSettings::Custom { enabled, command } => {
157 settings::ContextServerSettingsContent::Custom { enabled, command }
158 }
159 ContextServerSettings::Extension { enabled, settings } => {
160 settings::ContextServerSettingsContent::Extension { enabled, settings }
161 }
162 }
163 }
164}
165
166impl ContextServerSettings {
167 pub fn default_extension() -> Self {
168 Self::Extension {
169 enabled: true,
170 settings: serde_json::json!({}),
171 }
172 }
173
174 pub fn enabled(&self) -> bool {
175 match self {
176 ContextServerSettings::Custom { enabled, .. } => *enabled,
177 ContextServerSettings::Extension { enabled, .. } => *enabled,
178 }
179 }
180
181 pub fn set_enabled(&mut self, enabled: bool) {
182 match self {
183 ContextServerSettings::Custom { enabled: e, .. } => *e = enabled,
184 ContextServerSettings::Extension { enabled: e, .. } => *e = enabled,
185 }
186 }
187}
188
189#[derive(
190 Clone,
191 Copy,
192 Debug,
193 Eq,
194 PartialEq,
195 Ord,
196 PartialOrd,
197 Serialize,
198 Deserialize,
199 JsonSchema,
200 SettingsUi,
201)]
202#[serde(rename_all = "snake_case")]
203pub enum DiagnosticSeverity {
204 // No diagnostics are shown.
205 Off,
206 Error,
207 Warning,
208 Info,
209 Hint,
210}
211
212impl DiagnosticSeverity {
213 pub fn into_lsp(self) -> Option<lsp::DiagnosticSeverity> {
214 match self {
215 DiagnosticSeverity::Off => None,
216 DiagnosticSeverity::Error => Some(lsp::DiagnosticSeverity::ERROR),
217 DiagnosticSeverity::Warning => Some(lsp::DiagnosticSeverity::WARNING),
218 DiagnosticSeverity::Info => Some(lsp::DiagnosticSeverity::INFORMATION),
219 DiagnosticSeverity::Hint => Some(lsp::DiagnosticSeverity::HINT),
220 }
221 }
222}
223
224impl From<settings::DiagnosticSeverityContent> for DiagnosticSeverity {
225 fn from(severity: settings::DiagnosticSeverityContent) -> Self {
226 match severity {
227 settings::DiagnosticSeverityContent::Off => DiagnosticSeverity::Off,
228 settings::DiagnosticSeverityContent::Error => DiagnosticSeverity::Error,
229 settings::DiagnosticSeverityContent::Warning => DiagnosticSeverity::Warning,
230 settings::DiagnosticSeverityContent::Info => DiagnosticSeverity::Info,
231 settings::DiagnosticSeverityContent::Hint => DiagnosticSeverity::Hint,
232 }
233 }
234}
235
236/// Determines the severity of the diagnostic that should be moved to.
237#[derive(PartialEq, PartialOrd, Clone, Copy, Debug, Eq, Deserialize, JsonSchema)]
238#[serde(rename_all = "snake_case")]
239pub enum GoToDiagnosticSeverity {
240 /// Errors
241 Error = 3,
242 /// Warnings
243 Warning = 2,
244 /// Information
245 Information = 1,
246 /// Hints
247 Hint = 0,
248}
249
250impl From<lsp::DiagnosticSeverity> for GoToDiagnosticSeverity {
251 fn from(severity: lsp::DiagnosticSeverity) -> Self {
252 match severity {
253 lsp::DiagnosticSeverity::ERROR => Self::Error,
254 lsp::DiagnosticSeverity::WARNING => Self::Warning,
255 lsp::DiagnosticSeverity::INFORMATION => Self::Information,
256 lsp::DiagnosticSeverity::HINT => Self::Hint,
257 _ => Self::Error,
258 }
259 }
260}
261
262impl GoToDiagnosticSeverity {
263 pub fn min() -> Self {
264 Self::Hint
265 }
266
267 pub fn max() -> Self {
268 Self::Error
269 }
270}
271
272/// Allows filtering diagnostics that should be moved to.
273#[derive(PartialEq, Clone, Copy, Debug, Deserialize, JsonSchema)]
274#[serde(untagged)]
275pub enum GoToDiagnosticSeverityFilter {
276 /// Move to diagnostics of a specific severity.
277 Only(GoToDiagnosticSeverity),
278
279 /// Specify a range of severities to include.
280 Range {
281 /// Minimum severity to move to. Defaults no "error".
282 #[serde(default = "GoToDiagnosticSeverity::min")]
283 min: GoToDiagnosticSeverity,
284 /// Maximum severity to move to. Defaults to "hint".
285 #[serde(default = "GoToDiagnosticSeverity::max")]
286 max: GoToDiagnosticSeverity,
287 },
288}
289
290impl Default for GoToDiagnosticSeverityFilter {
291 fn default() -> Self {
292 Self::Range {
293 min: GoToDiagnosticSeverity::min(),
294 max: GoToDiagnosticSeverity::max(),
295 }
296 }
297}
298
299impl GoToDiagnosticSeverityFilter {
300 pub fn matches(&self, severity: lsp::DiagnosticSeverity) -> bool {
301 let severity: GoToDiagnosticSeverity = severity.into();
302 match self {
303 Self::Only(target) => *target == severity,
304 Self::Range { min, max } => severity >= *min && severity <= *max,
305 }
306 }
307}
308
309#[derive(Copy, Clone, Debug)]
310pub struct GitSettings {
311 /// Whether or not to show the git gutter.
312 ///
313 /// Default: tracked_files
314 pub git_gutter: settings::GitGutterSetting,
315 /// Sets the debounce threshold (in milliseconds) after which changes are reflected in the git gutter.
316 ///
317 /// Default: null
318 pub gutter_debounce: Option<u64>,
319 /// Whether or not to show git blame data inline in
320 /// the currently focused line.
321 ///
322 /// Default: on
323 pub inline_blame: InlineBlameSettings,
324 /// Which information to show in the branch picker.
325 ///
326 /// Default: on
327 pub branch_picker: BranchPickerSettings,
328 /// How hunks are displayed visually in the editor.
329 ///
330 /// Default: staged_hollow
331 pub hunk_style: settings::GitHunkStyleSetting,
332}
333
334#[derive(Clone, Copy, Debug)]
335pub struct InlineBlameSettings {
336 /// Whether or not to show git blame data inline in
337 /// the currently focused line.
338 ///
339 /// Default: true
340 pub enabled: bool,
341 /// Whether to only show the inline blame information
342 /// after a delay once the cursor stops moving.
343 ///
344 /// Default: 0
345 pub delay_ms: std::time::Duration,
346 /// The amount of padding between the end of the source line and the start
347 /// of the inline blame in units of columns.
348 ///
349 /// Default: 7
350 pub padding: u32,
351 /// The minimum column number to show the inline blame information at
352 ///
353 /// Default: 0
354 pub min_column: u32,
355 /// Whether to show commit summary as part of the inline blame.
356 ///
357 /// Default: false
358 pub show_commit_summary: bool,
359}
360
361impl GitSettings {
362 pub fn inline_blame_delay(&self) -> Option<Duration> {
363 if self.inline_blame.delay_ms.as_millis() > 0 {
364 Some(self.inline_blame.delay_ms)
365 } else {
366 None
367 }
368 }
369}
370
371#[derive(Clone, Copy, Debug, Serialize, Deserialize, JsonSchema)]
372#[serde(rename_all = "snake_case")]
373pub struct BranchPickerSettings {
374 /// Whether to show author name as part of the commit information.
375 ///
376 /// Default: false
377 #[serde(default)]
378 pub show_author_name: bool,
379}
380
381impl Default for BranchPickerSettings {
382 fn default() -> Self {
383 Self {
384 show_author_name: true,
385 }
386 }
387}
388
389#[derive(Clone, Debug)]
390pub struct DiagnosticsSettings {
391 /// Whether to show the project diagnostics button in the status bar.
392 pub button: bool,
393
394 /// Whether or not to include warning diagnostics.
395 pub include_warnings: bool,
396
397 /// Settings for using LSP pull diagnostics mechanism in Zed.
398 pub lsp_pull_diagnostics: LspPullDiagnosticsSettings,
399
400 /// Settings for showing inline diagnostics.
401 pub inline: InlineDiagnosticsSettings,
402}
403
404#[derive(Clone, Copy, Debug, PartialEq, Eq)]
405pub struct InlineDiagnosticsSettings {
406 /// Whether or not to show inline diagnostics
407 ///
408 /// Default: false
409 pub enabled: bool,
410 /// Whether to only show the inline diagnostics after a delay after the
411 /// last editor event.
412 ///
413 /// Default: 150
414 pub update_debounce_ms: u64,
415 /// The amount of padding between the end of the source line and the start
416 /// of the inline diagnostic in units of columns.
417 ///
418 /// Default: 4
419 pub padding: u32,
420 /// The minimum column to display inline diagnostics. This setting can be
421 /// used to horizontally align inline diagnostics at some position. Lines
422 /// longer than this value will still push diagnostics further to the right.
423 ///
424 /// Default: 0
425 pub min_column: u32,
426
427 pub max_severity: Option<DiagnosticSeverity>,
428}
429
430#[derive(Clone, Copy, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
431pub struct LspPullDiagnosticsSettings {
432 /// Whether to pull for diagnostics or not.
433 ///
434 /// Default: true
435 pub enabled: bool,
436 /// Minimum time to wait before pulling diagnostics from the language server(s).
437 /// 0 turns the debounce off.
438 ///
439 /// Default: 50
440 pub debounce_ms: u64,
441}
442
443impl Settings for ProjectSettings {
444 fn from_defaults(content: &settings::SettingsContent, _cx: &mut App) -> Self {
445 let project = &content.project.clone();
446 let diagnostics = content.diagnostics.as_ref().unwrap();
447 let lsp_pull_diagnostics = diagnostics.lsp_pull_diagnostics.as_ref().unwrap();
448 let inline_diagnostics = diagnostics.inline.as_ref().unwrap();
449
450 let git = content.git.as_ref().unwrap();
451 let git_settings = GitSettings {
452 git_gutter: git.git_gutter.unwrap(),
453 gutter_debounce: git.gutter_debounce,
454 inline_blame: {
455 let inline = git.inline_blame.unwrap();
456 InlineBlameSettings {
457 enabled: inline.enabled.unwrap(),
458 delay_ms: std::time::Duration::from_millis(inline.delay_ms.unwrap()),
459 padding: inline.padding.unwrap(),
460 min_column: inline.min_column.unwrap(),
461 show_commit_summary: inline.show_commit_summary.unwrap(),
462 }
463 },
464 branch_picker: {
465 let branch_picker = git.branch_picker.unwrap();
466 BranchPickerSettings {
467 show_author_name: branch_picker.show_author_name.unwrap(),
468 }
469 },
470 hunk_style: git.hunk_style.unwrap(),
471 };
472 Self {
473 context_servers: project
474 .context_servers
475 .clone()
476 .into_iter()
477 .map(|(key, value)| (key, value.into()))
478 .collect(),
479 lsp: project
480 .lsp
481 .clone()
482 .into_iter()
483 .map(|(key, value)| (LanguageServerName(key.into()), value))
484 .collect(),
485 global_lsp_settings: GlobalLspSettings {
486 button: content
487 .global_lsp_settings
488 .as_ref()
489 .unwrap()
490 .button
491 .unwrap(),
492 },
493 dap: project
494 .dap
495 .clone()
496 .into_iter()
497 .map(|(key, value)| (DebugAdapterName(key.into()), value))
498 .collect(),
499 diagnostics: DiagnosticsSettings {
500 button: diagnostics.button.unwrap(),
501 include_warnings: diagnostics.include_warnings.unwrap(),
502 lsp_pull_diagnostics: LspPullDiagnosticsSettings {
503 enabled: lsp_pull_diagnostics.enabled.unwrap(),
504 debounce_ms: lsp_pull_diagnostics.debounce_ms.unwrap(),
505 },
506 inline: InlineDiagnosticsSettings {
507 enabled: inline_diagnostics.enabled.unwrap(),
508 update_debounce_ms: inline_diagnostics.update_debounce_ms.unwrap(),
509 padding: inline_diagnostics.padding.unwrap(),
510 min_column: inline_diagnostics.min_column.unwrap(),
511 max_severity: inline_diagnostics.max_severity.map(Into::into),
512 },
513 },
514 git: git_settings,
515 node: content.node.clone().unwrap().into(),
516 load_direnv: project.load_direnv.clone().unwrap(),
517 session: SessionSettings {
518 restore_unsaved_buffers: content.session.unwrap().restore_unsaved_buffers.unwrap(),
519 },
520 }
521 }
522
523 fn refine(&mut self, content: &settings::SettingsContent, _cx: &mut App) {
524 let project = &content.project;
525 self.context_servers.extend(
526 project
527 .context_servers
528 .clone()
529 .into_iter()
530 .map(|(key, value)| (key, value.into())),
531 );
532 self.dap.extend(
533 project
534 .dap
535 .clone()
536 .into_iter()
537 .map(|(key, value)| (DebugAdapterName(key.into()), value)),
538 );
539 if let Some(diagnostics) = content.diagnostics.as_ref() {
540 if let Some(inline) = &diagnostics.inline {
541 self.diagnostics.inline.enabled.merge_from(&inline.enabled);
542 self.diagnostics
543 .inline
544 .update_debounce_ms
545 .merge_from(&inline.update_debounce_ms);
546 self.diagnostics.inline.padding.merge_from(&inline.padding);
547 self.diagnostics
548 .inline
549 .min_column
550 .merge_from(&inline.min_column);
551 if let Some(max_severity) = inline.max_severity {
552 self.diagnostics.inline.max_severity = Some(max_severity.into())
553 }
554 }
555
556 self.diagnostics.button.merge_from(&diagnostics.button);
557 self.diagnostics
558 .include_warnings
559 .merge_from(&diagnostics.include_warnings);
560 if let Some(pull_diagnostics) = &diagnostics.lsp_pull_diagnostics {
561 self.diagnostics
562 .lsp_pull_diagnostics
563 .enabled
564 .merge_from(&pull_diagnostics.enabled);
565 self.diagnostics
566 .lsp_pull_diagnostics
567 .debounce_ms
568 .merge_from(&pull_diagnostics.debounce_ms);
569 }
570 }
571 if let Some(git) = content.git.as_ref() {
572 if let Some(branch_picker) = git.branch_picker.as_ref() {
573 self.git
574 .branch_picker
575 .show_author_name
576 .merge_from(&branch_picker.show_author_name);
577 }
578 if let Some(inline_blame) = git.inline_blame.as_ref() {
579 self.git
580 .inline_blame
581 .enabled
582 .merge_from(&inline_blame.enabled);
583 self.git
584 .inline_blame
585 .delay_ms
586 .merge_from(&inline_blame.delay_ms.map(std::time::Duration::from_millis));
587 self.git
588 .inline_blame
589 .padding
590 .merge_from(&inline_blame.padding);
591 self.git
592 .inline_blame
593 .min_column
594 .merge_from(&inline_blame.min_column);
595 self.git
596 .inline_blame
597 .show_commit_summary
598 .merge_from(&inline_blame.show_commit_summary);
599 }
600 self.git.git_gutter.merge_from(&git.git_gutter);
601 self.git.hunk_style.merge_from(&git.hunk_style);
602 if let Some(debounce) = git.gutter_debounce {
603 self.git.gutter_debounce = Some(debounce);
604 }
605 }
606 self.global_lsp_settings.button.merge_from(
607 &content
608 .global_lsp_settings
609 .as_ref()
610 .and_then(|settings| settings.button),
611 );
612 self.load_direnv
613 .merge_from(&content.project.load_direnv.clone());
614
615 for (key, value) in content.project.lsp.clone() {
616 self.lsp.insert(LanguageServerName(key.into()), value);
617 }
618
619 if let Some(node) = content.node.as_ref() {
620 self.node
621 .ignore_system_version
622 .merge_from(&node.ignore_system_version);
623 if let Some(path) = node.path.clone() {
624 self.node.path = Some(path);
625 }
626 if let Some(npm_path) = node.npm_path.clone() {
627 self.node.npm_path = Some(npm_path);
628 }
629 }
630 self.session
631 .restore_unsaved_buffers
632 .merge_from(&content.session.and_then(|s| s.restore_unsaved_buffers));
633 }
634
635 fn import_from_vscode(
636 vscode: &settings::VsCodeSettings,
637 current: &mut settings::SettingsContent,
638 ) {
639 // this just sets the binary name instead of a full path so it relies on path lookup
640 // resolving to the one you want
641 let npm_path = vscode.read_enum("npm.packageManager", |s| match s {
642 v @ ("npm" | "yarn" | "bun" | "pnpm") => Some(v.to_owned()),
643 _ => None,
644 });
645 if npm_path.is_some() {
646 current.node.get_or_insert_default().npm_path = npm_path;
647 }
648
649 if let Some(b) = vscode.read_bool("git.blame.editorDecoration.enabled") {
650 // todo! get_or_insert_default is risky considering defaults probably don't correspond
651 // to default.json,
652 // probably need to pass in "defaults" for this type as additional arg, and
653 // use unwrap on those keys
654 current
655 .git
656 .get_or_insert_default()
657 .inline_blame
658 .get_or_insert_default()
659 .enabled = Some(b);
660 }
661
662 #[derive(Deserialize)]
663 struct VsCodeContextServerCommand {
664 command: PathBuf,
665 args: Option<Vec<String>>,
666 env: Option<HashMap<String, String>>,
667 // note: we don't support envFile and type
668 }
669 if let Some(mcp) = vscode.read_value("mcp").and_then(|v| v.as_object()) {
670 current
671 .project
672 .context_servers
673 .extend(mcp.iter().filter_map(|(k, v)| {
674 Some((
675 k.clone().into(),
676 settings::ContextServerSettingsContent::Custom {
677 enabled: true,
678 command: serde_json::from_value::<VsCodeContextServerCommand>(
679 v.clone(),
680 )
681 .ok()
682 .map(|cmd| {
683 settings::ContextServerCommand {
684 path: cmd.command,
685 args: cmd.args.unwrap_or_default(),
686 env: cmd.env,
687 timeout: None,
688 }
689 })?,
690 },
691 ))
692 }));
693 }
694
695 // TODO: translate lsp settings for rust-analyzer and other popular ones to old.lsp
696 }
697}
698
699pub enum SettingsObserverMode {
700 Local(Arc<dyn Fs>),
701 Remote,
702}
703
704#[derive(Clone, Debug, PartialEq)]
705pub enum SettingsObserverEvent {
706 LocalSettingsUpdated(Result<PathBuf, InvalidSettingsError>),
707 LocalTasksUpdated(Result<PathBuf, InvalidSettingsError>),
708 LocalDebugScenariosUpdated(Result<PathBuf, InvalidSettingsError>),
709}
710
711impl EventEmitter<SettingsObserverEvent> for SettingsObserver {}
712
713pub struct SettingsObserver {
714 mode: SettingsObserverMode,
715 downstream_client: Option<AnyProtoClient>,
716 worktree_store: Entity<WorktreeStore>,
717 project_id: u64,
718 task_store: Entity<TaskStore>,
719 _user_settings_watcher: Option<Subscription>,
720 _global_task_config_watcher: Task<()>,
721 _global_debug_config_watcher: Task<()>,
722}
723
724/// SettingsObserver observers changes to .zed/{settings, task}.json files in local worktrees
725/// (or the equivalent protobuf messages from upstream) and updates local settings
726/// and sends notifications downstream.
727/// In ssh mode it also monitors ~/.config/zed/{settings, task}.json and sends the content
728/// upstream.
729impl SettingsObserver {
730 pub fn init(client: &AnyProtoClient) {
731 client.add_entity_message_handler(Self::handle_update_worktree_settings);
732 client.add_entity_message_handler(Self::handle_update_user_settings);
733 }
734
735 pub fn new_local(
736 fs: Arc<dyn Fs>,
737 worktree_store: Entity<WorktreeStore>,
738 task_store: Entity<TaskStore>,
739 cx: &mut Context<Self>,
740 ) -> Self {
741 cx.subscribe(&worktree_store, Self::on_worktree_store_event)
742 .detach();
743
744 Self {
745 worktree_store,
746 task_store,
747 mode: SettingsObserverMode::Local(fs.clone()),
748 downstream_client: None,
749 _user_settings_watcher: None,
750 project_id: REMOTE_SERVER_PROJECT_ID,
751 _global_task_config_watcher: Self::subscribe_to_global_task_file_changes(
752 fs.clone(),
753 paths::tasks_file().clone(),
754 cx,
755 ),
756 _global_debug_config_watcher: Self::subscribe_to_global_debug_scenarios_changes(
757 fs.clone(),
758 paths::debug_scenarios_file().clone(),
759 cx,
760 ),
761 }
762 }
763
764 pub fn new_remote(
765 fs: Arc<dyn Fs>,
766 worktree_store: Entity<WorktreeStore>,
767 task_store: Entity<TaskStore>,
768 upstream_client: Option<AnyProtoClient>,
769 cx: &mut Context<Self>,
770 ) -> Self {
771 let mut user_settings_watcher = None;
772 if cx.try_global::<SettingsStore>().is_some() {
773 if let Some(upstream_client) = upstream_client {
774 let mut user_settings = None;
775 user_settings_watcher = Some(cx.observe_global::<SettingsStore>(move |_, cx| {
776 if let Some(new_settings) = cx.global::<SettingsStore>().raw_user_settings() {
777 if Some(new_settings) != user_settings.as_ref() {
778 if let Some(new_settings_string) =
779 serde_json::to_string(new_settings).ok()
780 {
781 user_settings = Some(new_settings.clone());
782 upstream_client
783 .send(proto::UpdateUserSettings {
784 project_id: REMOTE_SERVER_PROJECT_ID,
785 contents: new_settings_string,
786 })
787 .log_err();
788 }
789 }
790 }
791 }));
792 }
793 };
794
795 Self {
796 worktree_store,
797 task_store,
798 mode: SettingsObserverMode::Remote,
799 downstream_client: None,
800 project_id: REMOTE_SERVER_PROJECT_ID,
801 _user_settings_watcher: user_settings_watcher,
802 _global_task_config_watcher: Self::subscribe_to_global_task_file_changes(
803 fs.clone(),
804 paths::tasks_file().clone(),
805 cx,
806 ),
807 _global_debug_config_watcher: Self::subscribe_to_global_debug_scenarios_changes(
808 fs.clone(),
809 paths::debug_scenarios_file().clone(),
810 cx,
811 ),
812 }
813 }
814
815 pub fn shared(
816 &mut self,
817 project_id: u64,
818 downstream_client: AnyProtoClient,
819 cx: &mut Context<Self>,
820 ) {
821 self.project_id = project_id;
822 self.downstream_client = Some(downstream_client.clone());
823
824 let store = cx.global::<SettingsStore>();
825 for worktree in self.worktree_store.read(cx).worktrees() {
826 let worktree_id = worktree.read(cx).id().to_proto();
827 for (path, content) in store.local_settings(worktree.read(cx).id()) {
828 downstream_client
829 .send(proto::UpdateWorktreeSettings {
830 project_id,
831 worktree_id,
832 path: path.to_proto(),
833 content: Some(content),
834 kind: Some(
835 local_settings_kind_to_proto(LocalSettingsKind::Settings).into(),
836 ),
837 })
838 .log_err();
839 }
840 for (path, content, _) in store.local_editorconfig_settings(worktree.read(cx).id()) {
841 downstream_client
842 .send(proto::UpdateWorktreeSettings {
843 project_id,
844 worktree_id,
845 path: path.to_proto(),
846 content: Some(content),
847 kind: Some(
848 local_settings_kind_to_proto(LocalSettingsKind::Editorconfig).into(),
849 ),
850 })
851 .log_err();
852 }
853 }
854 }
855
856 pub fn unshared(&mut self, _: &mut Context<Self>) {
857 self.downstream_client = None;
858 }
859
860 async fn handle_update_worktree_settings(
861 this: Entity<Self>,
862 envelope: TypedEnvelope<proto::UpdateWorktreeSettings>,
863 mut cx: AsyncApp,
864 ) -> anyhow::Result<()> {
865 let kind = match envelope.payload.kind {
866 Some(kind) => proto::LocalSettingsKind::from_i32(kind)
867 .with_context(|| format!("unknown kind {kind}"))?,
868 None => proto::LocalSettingsKind::Settings,
869 };
870 this.update(&mut cx, |this, cx| {
871 let worktree_id = WorktreeId::from_proto(envelope.payload.worktree_id);
872 let Some(worktree) = this
873 .worktree_store
874 .read(cx)
875 .worktree_for_id(worktree_id, cx)
876 else {
877 return;
878 };
879
880 this.update_settings(
881 worktree,
882 [(
883 Arc::<Path>::from_proto(envelope.payload.path.clone()),
884 local_settings_kind_from_proto(kind),
885 envelope.payload.content,
886 )],
887 cx,
888 );
889 })?;
890 Ok(())
891 }
892
893 async fn handle_update_user_settings(
894 _: Entity<Self>,
895 envelope: TypedEnvelope<proto::UpdateUserSettings>,
896 cx: AsyncApp,
897 ) -> anyhow::Result<()> {
898 let new_settings = serde_json::from_str(&envelope.payload.contents).with_context(|| {
899 format!("deserializing {} user settings", envelope.payload.contents)
900 })?;
901 cx.update_global(|settings_store: &mut SettingsStore, cx| {
902 settings_store
903 .set_raw_user_settings(new_settings, cx)
904 .context("setting new user settings")?;
905 anyhow::Ok(())
906 })??;
907 Ok(())
908 }
909
910 fn on_worktree_store_event(
911 &mut self,
912 _: Entity<WorktreeStore>,
913 event: &WorktreeStoreEvent,
914 cx: &mut Context<Self>,
915 ) {
916 if let WorktreeStoreEvent::WorktreeAdded(worktree) = event {
917 cx.subscribe(worktree, |this, worktree, event, cx| {
918 if let worktree::Event::UpdatedEntries(changes) = event {
919 this.update_local_worktree_settings(&worktree, changes, cx)
920 }
921 })
922 .detach()
923 }
924 }
925
926 fn update_local_worktree_settings(
927 &mut self,
928 worktree: &Entity<Worktree>,
929 changes: &UpdatedEntriesSet,
930 cx: &mut Context<Self>,
931 ) {
932 let SettingsObserverMode::Local(fs) = &self.mode else {
933 return;
934 };
935
936 let mut settings_contents = Vec::new();
937 for (path, _, change) in changes.iter() {
938 let (settings_dir, kind) = if path.ends_with(local_settings_file_relative_path()) {
939 let settings_dir = Arc::<Path>::from(
940 path.ancestors()
941 .nth(local_settings_file_relative_path().components().count())
942 .unwrap(),
943 );
944 (settings_dir, LocalSettingsKind::Settings)
945 } else if path.ends_with(local_tasks_file_relative_path()) {
946 let settings_dir = Arc::<Path>::from(
947 path.ancestors()
948 .nth(
949 local_tasks_file_relative_path()
950 .components()
951 .count()
952 .saturating_sub(1),
953 )
954 .unwrap(),
955 );
956 (settings_dir, LocalSettingsKind::Tasks)
957 } else if path.ends_with(local_vscode_tasks_file_relative_path()) {
958 let settings_dir = Arc::<Path>::from(
959 path.ancestors()
960 .nth(
961 local_vscode_tasks_file_relative_path()
962 .components()
963 .count()
964 .saturating_sub(1),
965 )
966 .unwrap(),
967 );
968 (settings_dir, LocalSettingsKind::Tasks)
969 } else if path.ends_with(local_debug_file_relative_path()) {
970 let settings_dir = Arc::<Path>::from(
971 path.ancestors()
972 .nth(
973 local_debug_file_relative_path()
974 .components()
975 .count()
976 .saturating_sub(1),
977 )
978 .unwrap(),
979 );
980 (settings_dir, LocalSettingsKind::Debug)
981 } else if path.ends_with(local_vscode_launch_file_relative_path()) {
982 let settings_dir = Arc::<Path>::from(
983 path.ancestors()
984 .nth(
985 local_vscode_tasks_file_relative_path()
986 .components()
987 .count()
988 .saturating_sub(1),
989 )
990 .unwrap(),
991 );
992 (settings_dir, LocalSettingsKind::Debug)
993 } else if path.ends_with(EDITORCONFIG_NAME) {
994 let Some(settings_dir) = path.parent().map(Arc::from) else {
995 continue;
996 };
997 (settings_dir, LocalSettingsKind::Editorconfig)
998 } else {
999 continue;
1000 };
1001
1002 let removed = change == &PathChange::Removed;
1003 let fs = fs.clone();
1004 let abs_path = match worktree.read(cx).absolutize(path) {
1005 Ok(abs_path) => abs_path,
1006 Err(e) => {
1007 log::warn!("Cannot absolutize {path:?} received as {change:?} FS change: {e}");
1008 continue;
1009 }
1010 };
1011 settings_contents.push(async move {
1012 (
1013 settings_dir,
1014 kind,
1015 if removed {
1016 None
1017 } else {
1018 Some(
1019 async move {
1020 let content = fs.load(&abs_path).await?;
1021 if abs_path.ends_with(local_vscode_tasks_file_relative_path()) {
1022 let vscode_tasks =
1023 parse_json_with_comments::<VsCodeTaskFile>(&content)
1024 .with_context(|| {
1025 format!("parsing VSCode tasks, file {abs_path:?}")
1026 })?;
1027 let zed_tasks = TaskTemplates::try_from(vscode_tasks)
1028 .with_context(|| {
1029 format!(
1030 "converting VSCode tasks into Zed ones, file {abs_path:?}"
1031 )
1032 })?;
1033 serde_json::to_string(&zed_tasks).with_context(|| {
1034 format!(
1035 "serializing Zed tasks into JSON, file {abs_path:?}"
1036 )
1037 })
1038 } else if abs_path.ends_with(local_vscode_launch_file_relative_path()) {
1039 let vscode_tasks =
1040 parse_json_with_comments::<VsCodeDebugTaskFile>(&content)
1041 .with_context(|| {
1042 format!("parsing VSCode debug tasks, file {abs_path:?}")
1043 })?;
1044 let zed_tasks = DebugTaskFile::try_from(vscode_tasks)
1045 .with_context(|| {
1046 format!(
1047 "converting VSCode debug tasks into Zed ones, file {abs_path:?}"
1048 )
1049 })?;
1050 serde_json::to_string(&zed_tasks).with_context(|| {
1051 format!(
1052 "serializing Zed tasks into JSON, file {abs_path:?}"
1053 )
1054 })
1055 } else {
1056 Ok(content)
1057 }
1058 }
1059 .await,
1060 )
1061 },
1062 )
1063 });
1064 }
1065
1066 if settings_contents.is_empty() {
1067 return;
1068 }
1069
1070 let worktree = worktree.clone();
1071 cx.spawn(async move |this, cx| {
1072 let settings_contents: Vec<(Arc<Path>, _, _)> =
1073 futures::future::join_all(settings_contents).await;
1074 cx.update(|cx| {
1075 this.update(cx, |this, cx| {
1076 this.update_settings(
1077 worktree,
1078 settings_contents.into_iter().map(|(path, kind, content)| {
1079 (path, kind, content.and_then(|c| c.log_err()))
1080 }),
1081 cx,
1082 )
1083 })
1084 })
1085 })
1086 .detach();
1087 }
1088
1089 fn update_settings(
1090 &mut self,
1091 worktree: Entity<Worktree>,
1092 settings_contents: impl IntoIterator<Item = (Arc<Path>, LocalSettingsKind, Option<String>)>,
1093 cx: &mut Context<Self>,
1094 ) {
1095 let worktree_id = worktree.read(cx).id();
1096 let remote_worktree_id = worktree.read(cx).id();
1097 let task_store = self.task_store.clone();
1098
1099 for (directory, kind, file_content) in settings_contents {
1100 match kind {
1101 LocalSettingsKind::Settings | LocalSettingsKind::Editorconfig => cx
1102 .update_global::<SettingsStore, _>(|store, cx| {
1103 let result = store.set_local_settings(
1104 worktree_id,
1105 directory.clone(),
1106 kind,
1107 file_content.as_deref(),
1108 cx,
1109 );
1110
1111 match result {
1112 Err(InvalidSettingsError::LocalSettings { path, message }) => {
1113 log::error!("Failed to set local settings in {path:?}: {message}");
1114 cx.emit(SettingsObserverEvent::LocalSettingsUpdated(Err(
1115 InvalidSettingsError::LocalSettings { path, message },
1116 )));
1117 }
1118 Err(e) => {
1119 log::error!("Failed to set local settings: {e}");
1120 }
1121 Ok(()) => {
1122 cx.emit(SettingsObserverEvent::LocalSettingsUpdated(Ok(
1123 directory.join(local_settings_file_relative_path())
1124 )));
1125 }
1126 }
1127 }),
1128 LocalSettingsKind::Tasks => {
1129 let result = task_store.update(cx, |task_store, cx| {
1130 task_store.update_user_tasks(
1131 TaskSettingsLocation::Worktree(SettingsLocation {
1132 worktree_id,
1133 path: directory.as_ref(),
1134 }),
1135 file_content.as_deref(),
1136 cx,
1137 )
1138 });
1139
1140 match result {
1141 Err(InvalidSettingsError::Tasks { path, message }) => {
1142 log::error!("Failed to set local tasks in {path:?}: {message:?}");
1143 cx.emit(SettingsObserverEvent::LocalTasksUpdated(Err(
1144 InvalidSettingsError::Tasks { path, message },
1145 )));
1146 }
1147 Err(e) => {
1148 log::error!("Failed to set local tasks: {e}");
1149 }
1150 Ok(()) => {
1151 cx.emit(SettingsObserverEvent::LocalTasksUpdated(Ok(
1152 directory.join(task_file_name())
1153 )));
1154 }
1155 }
1156 }
1157 LocalSettingsKind::Debug => {
1158 let result = task_store.update(cx, |task_store, cx| {
1159 task_store.update_user_debug_scenarios(
1160 TaskSettingsLocation::Worktree(SettingsLocation {
1161 worktree_id,
1162 path: directory.as_ref(),
1163 }),
1164 file_content.as_deref(),
1165 cx,
1166 )
1167 });
1168
1169 match result {
1170 Err(InvalidSettingsError::Debug { path, message }) => {
1171 log::error!(
1172 "Failed to set local debug scenarios in {path:?}: {message:?}"
1173 );
1174 cx.emit(SettingsObserverEvent::LocalTasksUpdated(Err(
1175 InvalidSettingsError::Debug { path, message },
1176 )));
1177 }
1178 Err(e) => {
1179 log::error!("Failed to set local tasks: {e}");
1180 }
1181 Ok(()) => {
1182 cx.emit(SettingsObserverEvent::LocalTasksUpdated(Ok(
1183 directory.join(task_file_name())
1184 )));
1185 }
1186 }
1187 }
1188 };
1189
1190 if let Some(downstream_client) = &self.downstream_client {
1191 downstream_client
1192 .send(proto::UpdateWorktreeSettings {
1193 project_id: self.project_id,
1194 worktree_id: remote_worktree_id.to_proto(),
1195 path: directory.to_proto(),
1196 content: file_content.clone(),
1197 kind: Some(local_settings_kind_to_proto(kind).into()),
1198 })
1199 .log_err();
1200 }
1201 }
1202 }
1203
1204 fn subscribe_to_global_task_file_changes(
1205 fs: Arc<dyn Fs>,
1206 file_path: PathBuf,
1207 cx: &mut Context<Self>,
1208 ) -> Task<()> {
1209 let mut user_tasks_file_rx =
1210 watch_config_file(cx.background_executor(), fs, file_path.clone());
1211 let user_tasks_content = cx.background_executor().block(user_tasks_file_rx.next());
1212 let weak_entry = cx.weak_entity();
1213 cx.spawn(async move |settings_observer, cx| {
1214 let Ok(task_store) = settings_observer.read_with(cx, |settings_observer, _| {
1215 settings_observer.task_store.clone()
1216 }) else {
1217 return;
1218 };
1219 if let Some(user_tasks_content) = user_tasks_content {
1220 let Ok(()) = task_store.update(cx, |task_store, cx| {
1221 task_store
1222 .update_user_tasks(
1223 TaskSettingsLocation::Global(&file_path),
1224 Some(&user_tasks_content),
1225 cx,
1226 )
1227 .log_err();
1228 }) else {
1229 return;
1230 };
1231 }
1232 while let Some(user_tasks_content) = user_tasks_file_rx.next().await {
1233 let Ok(result) = task_store.update(cx, |task_store, cx| {
1234 task_store.update_user_tasks(
1235 TaskSettingsLocation::Global(&file_path),
1236 Some(&user_tasks_content),
1237 cx,
1238 )
1239 }) else {
1240 break;
1241 };
1242
1243 weak_entry
1244 .update(cx, |_, cx| match result {
1245 Ok(()) => cx.emit(SettingsObserverEvent::LocalTasksUpdated(Ok(
1246 file_path.clone()
1247 ))),
1248 Err(err) => cx.emit(SettingsObserverEvent::LocalTasksUpdated(Err(
1249 InvalidSettingsError::Tasks {
1250 path: file_path.clone(),
1251 message: err.to_string(),
1252 },
1253 ))),
1254 })
1255 .ok();
1256 }
1257 })
1258 }
1259 fn subscribe_to_global_debug_scenarios_changes(
1260 fs: Arc<dyn Fs>,
1261 file_path: PathBuf,
1262 cx: &mut Context<Self>,
1263 ) -> Task<()> {
1264 let mut user_tasks_file_rx =
1265 watch_config_file(cx.background_executor(), fs, file_path.clone());
1266 let user_tasks_content = cx.background_executor().block(user_tasks_file_rx.next());
1267 let weak_entry = cx.weak_entity();
1268 cx.spawn(async move |settings_observer, cx| {
1269 let Ok(task_store) = settings_observer.read_with(cx, |settings_observer, _| {
1270 settings_observer.task_store.clone()
1271 }) else {
1272 return;
1273 };
1274 if let Some(user_tasks_content) = user_tasks_content {
1275 let Ok(()) = task_store.update(cx, |task_store, cx| {
1276 task_store
1277 .update_user_debug_scenarios(
1278 TaskSettingsLocation::Global(&file_path),
1279 Some(&user_tasks_content),
1280 cx,
1281 )
1282 .log_err();
1283 }) else {
1284 return;
1285 };
1286 }
1287 while let Some(user_tasks_content) = user_tasks_file_rx.next().await {
1288 let Ok(result) = task_store.update(cx, |task_store, cx| {
1289 task_store.update_user_debug_scenarios(
1290 TaskSettingsLocation::Global(&file_path),
1291 Some(&user_tasks_content),
1292 cx,
1293 )
1294 }) else {
1295 break;
1296 };
1297
1298 weak_entry
1299 .update(cx, |_, cx| match result {
1300 Ok(()) => cx.emit(SettingsObserverEvent::LocalDebugScenariosUpdated(Ok(
1301 file_path.clone(),
1302 ))),
1303 Err(err) => cx.emit(SettingsObserverEvent::LocalDebugScenariosUpdated(
1304 Err(InvalidSettingsError::Tasks {
1305 path: file_path.clone(),
1306 message: err.to_string(),
1307 }),
1308 )),
1309 })
1310 .ok();
1311 }
1312 })
1313 }
1314}
1315
1316pub fn local_settings_kind_from_proto(kind: proto::LocalSettingsKind) -> LocalSettingsKind {
1317 match kind {
1318 proto::LocalSettingsKind::Settings => LocalSettingsKind::Settings,
1319 proto::LocalSettingsKind::Tasks => LocalSettingsKind::Tasks,
1320 proto::LocalSettingsKind::Editorconfig => LocalSettingsKind::Editorconfig,
1321 proto::LocalSettingsKind::Debug => LocalSettingsKind::Debug,
1322 }
1323}
1324
1325pub fn local_settings_kind_to_proto(kind: LocalSettingsKind) -> proto::LocalSettingsKind {
1326 match kind {
1327 LocalSettingsKind::Settings => proto::LocalSettingsKind::Settings,
1328 LocalSettingsKind::Tasks => proto::LocalSettingsKind::Tasks,
1329 LocalSettingsKind::Editorconfig => proto::LocalSettingsKind::Editorconfig,
1330 LocalSettingsKind::Debug => proto::LocalSettingsKind::Debug,
1331 }
1332}