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 // todo! remove
363 pub fn inline_blame_enabled(&self) -> bool {
364 self.inline_blame.enabled
365 // #[allow(unknown_lints, clippy::manual_unwrap_or_default)]
366 // match self.inline_blame {
367 // Some(InlineBlameSettings { enabled, .. }) => enabled,
368 // _ => false,
369 // }
370 }
371
372 // todo! remove
373 pub fn inline_blame_delay(&self) -> Option<Duration> {
374 Some(self.inline_blame.delay_ms)
375 // match self.inline_blame {
376 // Some(InlineBlameSettings { delay_ms, .. }) if delay_ms > 0 => {
377 // Some(Duration::from_millis(delay_ms))
378 // }
379 // _ => None,
380 // }
381 }
382
383 // todo! remove
384 pub fn show_inline_commit_summary(&self) -> bool {
385 self.inline_blame.show_commit_summary
386 // match self.inline_blame {
387 // Some(InlineBlameSettings {
388 // show_commit_summary,
389 // ..
390 // }) => show_commit_summary,
391 // _ => false,
392 // }
393 }
394}
395
396#[derive(Clone, Copy, Debug, Serialize, Deserialize, JsonSchema)]
397#[serde(rename_all = "snake_case")]
398pub struct BranchPickerSettings {
399 /// Whether to show author name as part of the commit information.
400 ///
401 /// Default: false
402 #[serde(default)]
403 pub show_author_name: bool,
404}
405
406impl Default for BranchPickerSettings {
407 fn default() -> Self {
408 Self {
409 show_author_name: true,
410 }
411 }
412}
413
414#[derive(Clone, Debug)]
415pub struct DiagnosticsSettings {
416 /// Whether to show the project diagnostics button in the status bar.
417 pub button: bool,
418
419 /// Whether or not to include warning diagnostics.
420 pub include_warnings: bool,
421
422 /// Settings for using LSP pull diagnostics mechanism in Zed.
423 pub lsp_pull_diagnostics: LspPullDiagnosticsSettings,
424
425 /// Settings for showing inline diagnostics.
426 pub inline: InlineDiagnosticsSettings,
427}
428
429#[derive(Clone, Copy, Debug, PartialEq, Eq)]
430pub struct InlineDiagnosticsSettings {
431 /// Whether or not to show inline diagnostics
432 ///
433 /// Default: false
434 pub enabled: bool,
435 /// Whether to only show the inline diagnostics after a delay after the
436 /// last editor event.
437 ///
438 /// Default: 150
439 pub update_debounce_ms: u64,
440 /// The amount of padding between the end of the source line and the start
441 /// of the inline diagnostic in units of columns.
442 ///
443 /// Default: 4
444 pub padding: u32,
445 /// The minimum column to display inline diagnostics. This setting can be
446 /// used to horizontally align inline diagnostics at some position. Lines
447 /// longer than this value will still push diagnostics further to the right.
448 ///
449 /// Default: 0
450 pub min_column: u32,
451
452 pub max_severity: Option<DiagnosticSeverity>,
453}
454
455#[derive(Clone, Copy, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
456pub struct LspPullDiagnosticsSettings {
457 /// Whether to pull for diagnostics or not.
458 ///
459 /// Default: true
460 pub enabled: bool,
461 /// Minimum time to wait before pulling diagnostics from the language server(s).
462 /// 0 turns the debounce off.
463 ///
464 /// Default: 50
465 pub debounce_ms: u64,
466}
467
468impl Settings for ProjectSettings {
469 fn from_defaults(content: &settings::SettingsContent, _cx: &mut App) -> Self {
470 let project = &content.project.clone();
471 let diagnostics = content.diagnostics.as_ref().unwrap();
472 let lsp_pull_diagnostics = diagnostics.lsp_pull_diagnostics.as_ref().unwrap();
473 let inline_diagnostics = diagnostics.inline.as_ref().unwrap();
474
475 let git = content.git.as_ref().unwrap();
476 let git_settings = GitSettings {
477 git_gutter: git.git_gutter.unwrap(),
478 gutter_debounce: git.gutter_debounce,
479 inline_blame: {
480 let inline = git.inline_blame.unwrap();
481 InlineBlameSettings {
482 enabled: inline.enabled.unwrap(),
483 delay_ms: std::time::Duration::from_millis(inline.delay_ms.unwrap()),
484 padding: inline.padding.unwrap(),
485 min_column: inline.min_column.unwrap(),
486 show_commit_summary: inline.show_commit_summary.unwrap(),
487 }
488 },
489 branch_picker: {
490 let branch_picker = git.branch_picker.unwrap();
491 BranchPickerSettings {
492 show_author_name: branch_picker.show_author_name.unwrap(),
493 }
494 },
495 hunk_style: git.hunk_style.unwrap(),
496 };
497 Self {
498 context_servers: project
499 .context_servers
500 .clone()
501 .into_iter()
502 .map(|(key, value)| (key, value.into()))
503 .collect(),
504 lsp: project
505 .lsp
506 .clone()
507 .into_iter()
508 .map(|(key, value)| (LanguageServerName(key.into()), value.into()))
509 .collect(),
510 global_lsp_settings: GlobalLspSettings {
511 button: content
512 .global_lsp_settings
513 .as_ref()
514 .unwrap()
515 .button
516 .unwrap(),
517 },
518 dap: project
519 .dap
520 .clone()
521 .into_iter()
522 .map(|(key, value)| (DebugAdapterName(key.into()), value))
523 .collect(),
524 diagnostics: DiagnosticsSettings {
525 button: diagnostics.button.unwrap(),
526 include_warnings: diagnostics.include_warnings.unwrap(),
527 lsp_pull_diagnostics: LspPullDiagnosticsSettings {
528 enabled: lsp_pull_diagnostics.enabled.unwrap(),
529 debounce_ms: lsp_pull_diagnostics.debounce_ms.unwrap(),
530 },
531 inline: InlineDiagnosticsSettings {
532 enabled: inline_diagnostics.enabled.unwrap(),
533 update_debounce_ms: inline_diagnostics.update_debounce_ms.unwrap(),
534 padding: inline_diagnostics.padding.unwrap(),
535 min_column: inline_diagnostics.min_column.unwrap(),
536 max_severity: inline_diagnostics.max_severity.map(Into::into),
537 },
538 },
539 git: git_settings,
540 node: content.node.clone().unwrap().into(),
541 load_direnv: project.load_direnv.clone().unwrap(),
542 session: SessionSettings {
543 restore_unsaved_buffers: content.session.unwrap().restore_unsaved_buffers.unwrap(),
544 },
545 }
546 }
547
548 fn refine(&mut self, content: &settings::SettingsContent, _cx: &mut App) {
549 let project = &content.project;
550 self.context_servers.extend(
551 project
552 .context_servers
553 .clone()
554 .into_iter()
555 .map(|(key, value)| (key, value.into())),
556 );
557 self.dap.extend(
558 project
559 .dap
560 .clone()
561 .into_iter()
562 .filter_map(|(key, value)| Some((DebugAdapterName(key.into()), value))),
563 );
564 if let Some(diagnostics) = content.diagnostics.as_ref() {
565 if let Some(inline) = &diagnostics.inline {
566 self.diagnostics.inline.enabled.merge_from(&inline.enabled);
567 self.diagnostics
568 .inline
569 .update_debounce_ms
570 .merge_from(&inline.update_debounce_ms);
571 self.diagnostics.inline.padding.merge_from(&inline.padding);
572 self.diagnostics
573 .inline
574 .min_column
575 .merge_from(&inline.min_column);
576 if let Some(max_severity) = inline.max_severity {
577 self.diagnostics.inline.max_severity = Some(max_severity.into())
578 }
579 }
580
581 self.diagnostics.button.merge_from(&diagnostics.button);
582 self.diagnostics
583 .include_warnings
584 .merge_from(&diagnostics.include_warnings);
585 if let Some(pull_diagnostics) = &diagnostics.lsp_pull_diagnostics {
586 self.diagnostics
587 .lsp_pull_diagnostics
588 .enabled
589 .merge_from(&pull_diagnostics.enabled);
590 self.diagnostics
591 .lsp_pull_diagnostics
592 .debounce_ms
593 .merge_from(&pull_diagnostics.debounce_ms);
594 }
595 }
596 if let Some(git) = content.git.as_ref() {
597 if let Some(branch_picker) = git.branch_picker.as_ref() {
598 self.git
599 .branch_picker
600 .show_author_name
601 .merge_from(&branch_picker.show_author_name);
602 }
603 if let Some(inline_blame) = git.inline_blame.as_ref() {
604 self.git
605 .inline_blame
606 .enabled
607 .merge_from(&inline_blame.enabled);
608 self.git
609 .inline_blame
610 .delay_ms
611 .merge_from(&inline_blame.delay_ms.map(std::time::Duration::from_millis));
612 self.git
613 .inline_blame
614 .padding
615 .merge_from(&inline_blame.padding);
616 self.git
617 .inline_blame
618 .min_column
619 .merge_from(&inline_blame.min_column);
620 self.git
621 .inline_blame
622 .show_commit_summary
623 .merge_from(&inline_blame.show_commit_summary);
624 }
625 self.git.git_gutter.merge_from(&git.git_gutter);
626 self.git.hunk_style.merge_from(&git.hunk_style);
627 if let Some(debounce) = git.gutter_debounce {
628 self.git.gutter_debounce = Some(debounce);
629 }
630 }
631 self.global_lsp_settings.button.merge_from(
632 &content
633 .global_lsp_settings
634 .as_ref()
635 .and_then(|settings| settings.button),
636 );
637 self.load_direnv
638 .merge_from(&content.project.load_direnv.clone());
639
640 for (key, value) in content.project.lsp.clone() {
641 self.lsp.insert(LanguageServerName(key.into()), value);
642 }
643
644 if let Some(node) = content.node.as_ref() {
645 self.node
646 .ignore_system_version
647 .merge_from(&node.ignore_system_version);
648 if let Some(path) = node.path.clone() {
649 self.node.path = Some(path);
650 }
651 if let Some(npm_path) = node.npm_path.clone() {
652 self.node.npm_path = Some(npm_path);
653 }
654 }
655 self.session
656 .restore_unsaved_buffers
657 .merge_from(&content.session.and_then(|s| s.restore_unsaved_buffers));
658 }
659
660 fn import_from_vscode(
661 vscode: &settings::VsCodeSettings,
662 current: &mut settings::SettingsContent,
663 ) {
664 // this just sets the binary name instead of a full path so it relies on path lookup
665 // resolving to the one you want
666 let npm_path = vscode.read_enum("npm.packageManager", |s| match s {
667 v @ ("npm" | "yarn" | "bun" | "pnpm") => Some(v.to_owned()),
668 _ => None,
669 });
670 if npm_path.is_some() {
671 current.node.get_or_insert_default().npm_path = npm_path;
672 }
673
674 if let Some(b) = vscode.read_bool("git.blame.editorDecoration.enabled") {
675 // todo! get_or_insert_default is risky considering defaults probably don't correspond
676 // to default.json,
677 // probably need to pass in "defaults" for this type as additional arg, and
678 // use unwrap on those keys
679 current
680 .git
681 .get_or_insert_default()
682 .inline_blame
683 .get_or_insert_default()
684 .enabled = Some(b);
685 }
686
687 #[derive(Deserialize)]
688 struct VsCodeContextServerCommand {
689 command: PathBuf,
690 args: Option<Vec<String>>,
691 env: Option<HashMap<String, String>>,
692 // note: we don't support envFile and type
693 }
694 if let Some(mcp) = vscode.read_value("mcp").and_then(|v| v.as_object()) {
695 current
696 .project
697 .context_servers
698 .extend(mcp.iter().filter_map(|(k, v)| {
699 Some((
700 k.clone().into(),
701 settings::ContextServerSettingsContent::Custom {
702 enabled: true,
703 command: serde_json::from_value::<VsCodeContextServerCommand>(
704 v.clone(),
705 )
706 .ok()
707 .map(|cmd| {
708 settings::ContextServerCommand {
709 path: cmd.command,
710 args: cmd.args.unwrap_or_default(),
711 env: cmd.env,
712 timeout: None,
713 }
714 })?,
715 },
716 ))
717 }));
718 }
719
720 // TODO: translate lsp settings for rust-analyzer and other popular ones to old.lsp
721 }
722}
723
724pub enum SettingsObserverMode {
725 Local(Arc<dyn Fs>),
726 Remote,
727}
728
729#[derive(Clone, Debug, PartialEq)]
730pub enum SettingsObserverEvent {
731 LocalSettingsUpdated(Result<PathBuf, InvalidSettingsError>),
732 LocalTasksUpdated(Result<PathBuf, InvalidSettingsError>),
733 LocalDebugScenariosUpdated(Result<PathBuf, InvalidSettingsError>),
734}
735
736impl EventEmitter<SettingsObserverEvent> for SettingsObserver {}
737
738pub struct SettingsObserver {
739 mode: SettingsObserverMode,
740 downstream_client: Option<AnyProtoClient>,
741 worktree_store: Entity<WorktreeStore>,
742 project_id: u64,
743 task_store: Entity<TaskStore>,
744 _user_settings_watcher: Option<Subscription>,
745 _global_task_config_watcher: Task<()>,
746 _global_debug_config_watcher: Task<()>,
747}
748
749/// SettingsObserver observers changes to .zed/{settings, task}.json files in local worktrees
750/// (or the equivalent protobuf messages from upstream) and updates local settings
751/// and sends notifications downstream.
752/// In ssh mode it also monitors ~/.config/zed/{settings, task}.json and sends the content
753/// upstream.
754impl SettingsObserver {
755 pub fn init(client: &AnyProtoClient) {
756 client.add_entity_message_handler(Self::handle_update_worktree_settings);
757 client.add_entity_message_handler(Self::handle_update_user_settings);
758 }
759
760 pub fn new_local(
761 fs: Arc<dyn Fs>,
762 worktree_store: Entity<WorktreeStore>,
763 task_store: Entity<TaskStore>,
764 cx: &mut Context<Self>,
765 ) -> Self {
766 cx.subscribe(&worktree_store, Self::on_worktree_store_event)
767 .detach();
768
769 Self {
770 worktree_store,
771 task_store,
772 mode: SettingsObserverMode::Local(fs.clone()),
773 downstream_client: None,
774 _user_settings_watcher: None,
775 project_id: REMOTE_SERVER_PROJECT_ID,
776 _global_task_config_watcher: Self::subscribe_to_global_task_file_changes(
777 fs.clone(),
778 paths::tasks_file().clone(),
779 cx,
780 ),
781 _global_debug_config_watcher: Self::subscribe_to_global_debug_scenarios_changes(
782 fs.clone(),
783 paths::debug_scenarios_file().clone(),
784 cx,
785 ),
786 }
787 }
788
789 pub fn new_remote(
790 fs: Arc<dyn Fs>,
791 worktree_store: Entity<WorktreeStore>,
792 task_store: Entity<TaskStore>,
793 upstream_client: Option<AnyProtoClient>,
794 cx: &mut Context<Self>,
795 ) -> Self {
796 let mut user_settings_watcher = None;
797 if cx.try_global::<SettingsStore>().is_some() {
798 if let Some(upstream_client) = upstream_client {
799 let mut user_settings = None;
800 user_settings_watcher = Some(cx.observe_global::<SettingsStore>(move |_, cx| {
801 if let Some(new_settings) = cx.global::<SettingsStore>().raw_user_settings() {
802 if Some(new_settings) != user_settings.as_ref() {
803 if let Some(new_settings_string) =
804 serde_json::to_string(new_settings).ok()
805 {
806 user_settings = Some(new_settings.clone());
807 upstream_client
808 .send(proto::UpdateUserSettings {
809 project_id: REMOTE_SERVER_PROJECT_ID,
810 contents: new_settings_string,
811 })
812 .log_err();
813 }
814 }
815 }
816 }));
817 }
818 };
819
820 Self {
821 worktree_store,
822 task_store,
823 mode: SettingsObserverMode::Remote,
824 downstream_client: None,
825 project_id: REMOTE_SERVER_PROJECT_ID,
826 _user_settings_watcher: user_settings_watcher,
827 _global_task_config_watcher: Self::subscribe_to_global_task_file_changes(
828 fs.clone(),
829 paths::tasks_file().clone(),
830 cx,
831 ),
832 _global_debug_config_watcher: Self::subscribe_to_global_debug_scenarios_changes(
833 fs.clone(),
834 paths::debug_scenarios_file().clone(),
835 cx,
836 ),
837 }
838 }
839
840 pub fn shared(
841 &mut self,
842 project_id: u64,
843 downstream_client: AnyProtoClient,
844 cx: &mut Context<Self>,
845 ) {
846 self.project_id = project_id;
847 self.downstream_client = Some(downstream_client.clone());
848
849 let store = cx.global::<SettingsStore>();
850 for worktree in self.worktree_store.read(cx).worktrees() {
851 let worktree_id = worktree.read(cx).id().to_proto();
852 for (path, content) in store.local_settings(worktree.read(cx).id()) {
853 downstream_client
854 .send(proto::UpdateWorktreeSettings {
855 project_id,
856 worktree_id,
857 path: path.to_proto(),
858 content: Some(content),
859 kind: Some(
860 local_settings_kind_to_proto(LocalSettingsKind::Settings).into(),
861 ),
862 })
863 .log_err();
864 }
865 for (path, content, _) in store.local_editorconfig_settings(worktree.read(cx).id()) {
866 downstream_client
867 .send(proto::UpdateWorktreeSettings {
868 project_id,
869 worktree_id,
870 path: path.to_proto(),
871 content: Some(content),
872 kind: Some(
873 local_settings_kind_to_proto(LocalSettingsKind::Editorconfig).into(),
874 ),
875 })
876 .log_err();
877 }
878 }
879 }
880
881 pub fn unshared(&mut self, _: &mut Context<Self>) {
882 self.downstream_client = None;
883 }
884
885 async fn handle_update_worktree_settings(
886 this: Entity<Self>,
887 envelope: TypedEnvelope<proto::UpdateWorktreeSettings>,
888 mut cx: AsyncApp,
889 ) -> anyhow::Result<()> {
890 let kind = match envelope.payload.kind {
891 Some(kind) => proto::LocalSettingsKind::from_i32(kind)
892 .with_context(|| format!("unknown kind {kind}"))?,
893 None => proto::LocalSettingsKind::Settings,
894 };
895 this.update(&mut cx, |this, cx| {
896 let worktree_id = WorktreeId::from_proto(envelope.payload.worktree_id);
897 let Some(worktree) = this
898 .worktree_store
899 .read(cx)
900 .worktree_for_id(worktree_id, cx)
901 else {
902 return;
903 };
904
905 this.update_settings(
906 worktree,
907 [(
908 Arc::<Path>::from_proto(envelope.payload.path.clone()),
909 local_settings_kind_from_proto(kind),
910 envelope.payload.content,
911 )],
912 cx,
913 );
914 })?;
915 Ok(())
916 }
917
918 async fn handle_update_user_settings(
919 _: Entity<Self>,
920 envelope: TypedEnvelope<proto::UpdateUserSettings>,
921 cx: AsyncApp,
922 ) -> anyhow::Result<()> {
923 let new_settings = serde_json::from_str(&envelope.payload.contents).with_context(|| {
924 format!("deserializing {} user settings", envelope.payload.contents)
925 })?;
926 cx.update_global(|settings_store: &mut SettingsStore, cx| {
927 settings_store
928 .set_raw_user_settings(new_settings, cx)
929 .context("setting new user settings")?;
930 anyhow::Ok(())
931 })??;
932 Ok(())
933 }
934
935 fn on_worktree_store_event(
936 &mut self,
937 _: Entity<WorktreeStore>,
938 event: &WorktreeStoreEvent,
939 cx: &mut Context<Self>,
940 ) {
941 if let WorktreeStoreEvent::WorktreeAdded(worktree) = event {
942 cx.subscribe(worktree, |this, worktree, event, cx| {
943 if let worktree::Event::UpdatedEntries(changes) = event {
944 this.update_local_worktree_settings(&worktree, changes, cx)
945 }
946 })
947 .detach()
948 }
949 }
950
951 fn update_local_worktree_settings(
952 &mut self,
953 worktree: &Entity<Worktree>,
954 changes: &UpdatedEntriesSet,
955 cx: &mut Context<Self>,
956 ) {
957 let SettingsObserverMode::Local(fs) = &self.mode else {
958 return;
959 };
960
961 let mut settings_contents = Vec::new();
962 for (path, _, change) in changes.iter() {
963 let (settings_dir, kind) = if path.ends_with(local_settings_file_relative_path()) {
964 let settings_dir = Arc::<Path>::from(
965 path.ancestors()
966 .nth(local_settings_file_relative_path().components().count())
967 .unwrap(),
968 );
969 (settings_dir, LocalSettingsKind::Settings)
970 } else if path.ends_with(local_tasks_file_relative_path()) {
971 let settings_dir = Arc::<Path>::from(
972 path.ancestors()
973 .nth(
974 local_tasks_file_relative_path()
975 .components()
976 .count()
977 .saturating_sub(1),
978 )
979 .unwrap(),
980 );
981 (settings_dir, LocalSettingsKind::Tasks)
982 } else if path.ends_with(local_vscode_tasks_file_relative_path()) {
983 let settings_dir = Arc::<Path>::from(
984 path.ancestors()
985 .nth(
986 local_vscode_tasks_file_relative_path()
987 .components()
988 .count()
989 .saturating_sub(1),
990 )
991 .unwrap(),
992 );
993 (settings_dir, LocalSettingsKind::Tasks)
994 } else if path.ends_with(local_debug_file_relative_path()) {
995 let settings_dir = Arc::<Path>::from(
996 path.ancestors()
997 .nth(
998 local_debug_file_relative_path()
999 .components()
1000 .count()
1001 .saturating_sub(1),
1002 )
1003 .unwrap(),
1004 );
1005 (settings_dir, LocalSettingsKind::Debug)
1006 } else if path.ends_with(local_vscode_launch_file_relative_path()) {
1007 let settings_dir = Arc::<Path>::from(
1008 path.ancestors()
1009 .nth(
1010 local_vscode_tasks_file_relative_path()
1011 .components()
1012 .count()
1013 .saturating_sub(1),
1014 )
1015 .unwrap(),
1016 );
1017 (settings_dir, LocalSettingsKind::Debug)
1018 } else if path.ends_with(EDITORCONFIG_NAME) {
1019 let Some(settings_dir) = path.parent().map(Arc::from) else {
1020 continue;
1021 };
1022 (settings_dir, LocalSettingsKind::Editorconfig)
1023 } else {
1024 continue;
1025 };
1026
1027 let removed = change == &PathChange::Removed;
1028 let fs = fs.clone();
1029 let abs_path = match worktree.read(cx).absolutize(path) {
1030 Ok(abs_path) => abs_path,
1031 Err(e) => {
1032 log::warn!("Cannot absolutize {path:?} received as {change:?} FS change: {e}");
1033 continue;
1034 }
1035 };
1036 settings_contents.push(async move {
1037 (
1038 settings_dir,
1039 kind,
1040 if removed {
1041 None
1042 } else {
1043 Some(
1044 async move {
1045 let content = fs.load(&abs_path).await?;
1046 if abs_path.ends_with(local_vscode_tasks_file_relative_path()) {
1047 let vscode_tasks =
1048 parse_json_with_comments::<VsCodeTaskFile>(&content)
1049 .with_context(|| {
1050 format!("parsing VSCode tasks, file {abs_path:?}")
1051 })?;
1052 let zed_tasks = TaskTemplates::try_from(vscode_tasks)
1053 .with_context(|| {
1054 format!(
1055 "converting VSCode tasks into Zed ones, file {abs_path:?}"
1056 )
1057 })?;
1058 serde_json::to_string(&zed_tasks).with_context(|| {
1059 format!(
1060 "serializing Zed tasks into JSON, file {abs_path:?}"
1061 )
1062 })
1063 } else if abs_path.ends_with(local_vscode_launch_file_relative_path()) {
1064 let vscode_tasks =
1065 parse_json_with_comments::<VsCodeDebugTaskFile>(&content)
1066 .with_context(|| {
1067 format!("parsing VSCode debug tasks, file {abs_path:?}")
1068 })?;
1069 let zed_tasks = DebugTaskFile::try_from(vscode_tasks)
1070 .with_context(|| {
1071 format!(
1072 "converting VSCode debug tasks into Zed ones, file {abs_path:?}"
1073 )
1074 })?;
1075 serde_json::to_string(&zed_tasks).with_context(|| {
1076 format!(
1077 "serializing Zed tasks into JSON, file {abs_path:?}"
1078 )
1079 })
1080 } else {
1081 Ok(content)
1082 }
1083 }
1084 .await,
1085 )
1086 },
1087 )
1088 });
1089 }
1090
1091 if settings_contents.is_empty() {
1092 return;
1093 }
1094
1095 let worktree = worktree.clone();
1096 cx.spawn(async move |this, cx| {
1097 let settings_contents: Vec<(Arc<Path>, _, _)> =
1098 futures::future::join_all(settings_contents).await;
1099 cx.update(|cx| {
1100 this.update(cx, |this, cx| {
1101 this.update_settings(
1102 worktree,
1103 settings_contents.into_iter().map(|(path, kind, content)| {
1104 (path, kind, content.and_then(|c| c.log_err()))
1105 }),
1106 cx,
1107 )
1108 })
1109 })
1110 })
1111 .detach();
1112 }
1113
1114 fn update_settings(
1115 &mut self,
1116 worktree: Entity<Worktree>,
1117 settings_contents: impl IntoIterator<Item = (Arc<Path>, LocalSettingsKind, Option<String>)>,
1118 cx: &mut Context<Self>,
1119 ) {
1120 let worktree_id = worktree.read(cx).id();
1121 let remote_worktree_id = worktree.read(cx).id();
1122 let task_store = self.task_store.clone();
1123
1124 for (directory, kind, file_content) in settings_contents {
1125 match kind {
1126 LocalSettingsKind::Settings | LocalSettingsKind::Editorconfig => cx
1127 .update_global::<SettingsStore, _>(|store, cx| {
1128 let result = store.set_local_settings(
1129 worktree_id,
1130 directory.clone(),
1131 kind,
1132 file_content.as_deref(),
1133 cx,
1134 );
1135
1136 match result {
1137 Err(InvalidSettingsError::LocalSettings { path, message }) => {
1138 log::error!("Failed to set local settings in {path:?}: {message}");
1139 cx.emit(SettingsObserverEvent::LocalSettingsUpdated(Err(
1140 InvalidSettingsError::LocalSettings { path, message },
1141 )));
1142 }
1143 Err(e) => {
1144 log::error!("Failed to set local settings: {e}");
1145 }
1146 Ok(()) => {
1147 cx.emit(SettingsObserverEvent::LocalSettingsUpdated(Ok(
1148 directory.join(local_settings_file_relative_path())
1149 )));
1150 }
1151 }
1152 }),
1153 LocalSettingsKind::Tasks => {
1154 let result = task_store.update(cx, |task_store, cx| {
1155 task_store.update_user_tasks(
1156 TaskSettingsLocation::Worktree(SettingsLocation {
1157 worktree_id,
1158 path: directory.as_ref(),
1159 }),
1160 file_content.as_deref(),
1161 cx,
1162 )
1163 });
1164
1165 match result {
1166 Err(InvalidSettingsError::Tasks { path, message }) => {
1167 log::error!("Failed to set local tasks in {path:?}: {message:?}");
1168 cx.emit(SettingsObserverEvent::LocalTasksUpdated(Err(
1169 InvalidSettingsError::Tasks { path, message },
1170 )));
1171 }
1172 Err(e) => {
1173 log::error!("Failed to set local tasks: {e}");
1174 }
1175 Ok(()) => {
1176 cx.emit(SettingsObserverEvent::LocalTasksUpdated(Ok(
1177 directory.join(task_file_name())
1178 )));
1179 }
1180 }
1181 }
1182 LocalSettingsKind::Debug => {
1183 let result = task_store.update(cx, |task_store, cx| {
1184 task_store.update_user_debug_scenarios(
1185 TaskSettingsLocation::Worktree(SettingsLocation {
1186 worktree_id,
1187 path: directory.as_ref(),
1188 }),
1189 file_content.as_deref(),
1190 cx,
1191 )
1192 });
1193
1194 match result {
1195 Err(InvalidSettingsError::Debug { path, message }) => {
1196 log::error!(
1197 "Failed to set local debug scenarios in {path:?}: {message:?}"
1198 );
1199 cx.emit(SettingsObserverEvent::LocalTasksUpdated(Err(
1200 InvalidSettingsError::Debug { path, message },
1201 )));
1202 }
1203 Err(e) => {
1204 log::error!("Failed to set local tasks: {e}");
1205 }
1206 Ok(()) => {
1207 cx.emit(SettingsObserverEvent::LocalTasksUpdated(Ok(
1208 directory.join(task_file_name())
1209 )));
1210 }
1211 }
1212 }
1213 };
1214
1215 if let Some(downstream_client) = &self.downstream_client {
1216 downstream_client
1217 .send(proto::UpdateWorktreeSettings {
1218 project_id: self.project_id,
1219 worktree_id: remote_worktree_id.to_proto(),
1220 path: directory.to_proto(),
1221 content: file_content.clone(),
1222 kind: Some(local_settings_kind_to_proto(kind).into()),
1223 })
1224 .log_err();
1225 }
1226 }
1227 }
1228
1229 fn subscribe_to_global_task_file_changes(
1230 fs: Arc<dyn Fs>,
1231 file_path: PathBuf,
1232 cx: &mut Context<Self>,
1233 ) -> Task<()> {
1234 let mut user_tasks_file_rx =
1235 watch_config_file(cx.background_executor(), fs, file_path.clone());
1236 let user_tasks_content = cx.background_executor().block(user_tasks_file_rx.next());
1237 let weak_entry = cx.weak_entity();
1238 cx.spawn(async move |settings_observer, cx| {
1239 let Ok(task_store) = settings_observer.read_with(cx, |settings_observer, _| {
1240 settings_observer.task_store.clone()
1241 }) else {
1242 return;
1243 };
1244 if let Some(user_tasks_content) = user_tasks_content {
1245 let Ok(()) = task_store.update(cx, |task_store, cx| {
1246 task_store
1247 .update_user_tasks(
1248 TaskSettingsLocation::Global(&file_path),
1249 Some(&user_tasks_content),
1250 cx,
1251 )
1252 .log_err();
1253 }) else {
1254 return;
1255 };
1256 }
1257 while let Some(user_tasks_content) = user_tasks_file_rx.next().await {
1258 let Ok(result) = task_store.update(cx, |task_store, cx| {
1259 task_store.update_user_tasks(
1260 TaskSettingsLocation::Global(&file_path),
1261 Some(&user_tasks_content),
1262 cx,
1263 )
1264 }) else {
1265 break;
1266 };
1267
1268 weak_entry
1269 .update(cx, |_, cx| match result {
1270 Ok(()) => cx.emit(SettingsObserverEvent::LocalTasksUpdated(Ok(
1271 file_path.clone()
1272 ))),
1273 Err(err) => cx.emit(SettingsObserverEvent::LocalTasksUpdated(Err(
1274 InvalidSettingsError::Tasks {
1275 path: file_path.clone(),
1276 message: err.to_string(),
1277 },
1278 ))),
1279 })
1280 .ok();
1281 }
1282 })
1283 }
1284 fn subscribe_to_global_debug_scenarios_changes(
1285 fs: Arc<dyn Fs>,
1286 file_path: PathBuf,
1287 cx: &mut Context<Self>,
1288 ) -> Task<()> {
1289 let mut user_tasks_file_rx =
1290 watch_config_file(cx.background_executor(), fs, file_path.clone());
1291 let user_tasks_content = cx.background_executor().block(user_tasks_file_rx.next());
1292 let weak_entry = cx.weak_entity();
1293 cx.spawn(async move |settings_observer, cx| {
1294 let Ok(task_store) = settings_observer.read_with(cx, |settings_observer, _| {
1295 settings_observer.task_store.clone()
1296 }) else {
1297 return;
1298 };
1299 if let Some(user_tasks_content) = user_tasks_content {
1300 let Ok(()) = task_store.update(cx, |task_store, cx| {
1301 task_store
1302 .update_user_debug_scenarios(
1303 TaskSettingsLocation::Global(&file_path),
1304 Some(&user_tasks_content),
1305 cx,
1306 )
1307 .log_err();
1308 }) else {
1309 return;
1310 };
1311 }
1312 while let Some(user_tasks_content) = user_tasks_file_rx.next().await {
1313 let Ok(result) = task_store.update(cx, |task_store, cx| {
1314 task_store.update_user_debug_scenarios(
1315 TaskSettingsLocation::Global(&file_path),
1316 Some(&user_tasks_content),
1317 cx,
1318 )
1319 }) else {
1320 break;
1321 };
1322
1323 weak_entry
1324 .update(cx, |_, cx| match result {
1325 Ok(()) => cx.emit(SettingsObserverEvent::LocalDebugScenariosUpdated(Ok(
1326 file_path.clone(),
1327 ))),
1328 Err(err) => cx.emit(SettingsObserverEvent::LocalDebugScenariosUpdated(
1329 Err(InvalidSettingsError::Tasks {
1330 path: file_path.clone(),
1331 message: err.to_string(),
1332 }),
1333 )),
1334 })
1335 .ok();
1336 }
1337 })
1338 }
1339}
1340
1341pub fn local_settings_kind_from_proto(kind: proto::LocalSettingsKind) -> LocalSettingsKind {
1342 match kind {
1343 proto::LocalSettingsKind::Settings => LocalSettingsKind::Settings,
1344 proto::LocalSettingsKind::Tasks => LocalSettingsKind::Tasks,
1345 proto::LocalSettingsKind::Editorconfig => LocalSettingsKind::Editorconfig,
1346 proto::LocalSettingsKind::Debug => LocalSettingsKind::Debug,
1347 }
1348}
1349
1350pub fn local_settings_kind_to_proto(kind: LocalSettingsKind) -> proto::LocalSettingsKind {
1351 match kind {
1352 LocalSettingsKind::Settings => proto::LocalSettingsKind::Settings,
1353 LocalSettingsKind::Tasks => proto::LocalSettingsKind::Tasks,
1354 LocalSettingsKind::Editorconfig => proto::LocalSettingsKind::Editorconfig,
1355 LocalSettingsKind::Debug => proto::LocalSettingsKind::Debug,
1356 }
1357}