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