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 self.dap.extend(
546 project
547 .dap
548 .clone()
549 .into_iter()
550 .filter_map(|(key, value)| Some((DebugAdapterName(key.into()), value))),
551 );
552 if let Some(diagnostics) = content.diagnostics.as_ref() {
553 if let Some(inline) = &diagnostics.inline {
554 self.diagnostics.inline.enabled.merge_from(&inline.enabled);
555 self.diagnostics
556 .inline
557 .update_debounce_ms
558 .merge_from(&inline.update_debounce_ms);
559 self.diagnostics.inline.padding.merge_from(&inline.padding);
560 self.diagnostics
561 .inline
562 .min_column
563 .merge_from(&inline.min_column);
564 if let Some(max_severity) = inline.max_severity {
565 self.diagnostics.inline.max_severity = Some(max_severity.into())
566 }
567 }
568
569 self.diagnostics.button.merge_from(&diagnostics.button);
570 self.diagnostics
571 .include_warnings
572 .merge_from(&diagnostics.include_warnings);
573 if let Some(pull_diagnostics) = &diagnostics.lsp_pull_diagnostics {
574 self.diagnostics
575 .lsp_pull_diagnostics
576 .enabled
577 .merge_from(&pull_diagnostics.enabled);
578 self.diagnostics
579 .lsp_pull_diagnostics
580 .debounce_ms
581 .merge_from(&pull_diagnostics.debounce_ms);
582 }
583 }
584 if let Some(git) = content.git.as_ref() {
585 if let Some(branch_picker) = git.branch_picker.as_ref() {
586 self.git
587 .branch_picker
588 .show_author_name
589 .merge_from(&branch_picker.show_author_name);
590 }
591 if let Some(inline_blame) = git.inline_blame.as_ref() {
592 self.git
593 .inline_blame
594 .enabled
595 .merge_from(&inline_blame.enabled);
596 self.git
597 .inline_blame
598 .delay_ms
599 .merge_from(&inline_blame.delay_ms.map(std::time::Duration::from_millis));
600 self.git
601 .inline_blame
602 .padding
603 .merge_from(&inline_blame.padding);
604 self.git
605 .inline_blame
606 .min_column
607 .merge_from(&inline_blame.min_column);
608 self.git
609 .inline_blame
610 .show_commit_summary
611 .merge_from(&inline_blame.show_commit_summary);
612 }
613 self.git.git_gutter.merge_from(&git.git_gutter);
614 self.git.hunk_style.merge_from(&git.hunk_style);
615 if let Some(debounce) = git.gutter_debounce {
616 self.git.gutter_debounce = Some(debounce);
617 }
618 }
619 self.global_lsp_settings.button.merge_from(
620 &content
621 .global_lsp_settings
622 .as_ref()
623 .and_then(|settings| settings.button),
624 );
625 self.load_direnv
626 .merge_from(&content.project.load_direnv.clone());
627
628 for (key, value) in content.project.lsp.clone() {
629 self.lsp.insert(LanguageServerName(key.into()), value);
630 }
631
632 if let Some(node) = content.node.as_ref() {
633 self.node
634 .ignore_system_version
635 .merge_from(&node.ignore_system_version);
636 if let Some(path) = node.path.clone() {
637 self.node.path = Some(path);
638 }
639 if let Some(npm_path) = node.npm_path.clone() {
640 self.node.npm_path = Some(npm_path);
641 }
642 }
643 self.session
644 .restore_unsaved_buffers
645 .merge_from(&content.session.and_then(|s| s.restore_unsaved_buffers));
646 }
647
648 fn import_from_vscode(
649 vscode: &settings::VsCodeSettings,
650 current: &mut settings::SettingsContent,
651 ) {
652 // this just sets the binary name instead of a full path so it relies on path lookup
653 // resolving to the one you want
654 let npm_path = vscode.read_enum("npm.packageManager", |s| match s {
655 v @ ("npm" | "yarn" | "bun" | "pnpm") => Some(v.to_owned()),
656 _ => None,
657 });
658 if npm_path.is_some() {
659 current.node.get_or_insert_default().npm_path = npm_path;
660 }
661
662 if let Some(b) = vscode.read_bool("git.blame.editorDecoration.enabled") {
663 // todo! get_or_insert_default is risky considering defaults probably don't correspond
664 // to default.json,
665 // probably need to pass in "defaults" for this type as additional arg, and
666 // use unwrap on those keys
667 current
668 .git
669 .get_or_insert_default()
670 .inline_blame
671 .get_or_insert_default()
672 .enabled = Some(b);
673 }
674
675 #[derive(Deserialize)]
676 struct VsCodeContextServerCommand {
677 command: PathBuf,
678 args: Option<Vec<String>>,
679 env: Option<HashMap<String, String>>,
680 // note: we don't support envFile and type
681 }
682 if let Some(mcp) = vscode.read_value("mcp").and_then(|v| v.as_object()) {
683 current
684 .project
685 .context_servers
686 .extend(mcp.iter().filter_map(|(k, v)| {
687 Some((
688 k.clone().into(),
689 settings::ContextServerSettingsContent::Custom {
690 enabled: true,
691 command: serde_json::from_value::<VsCodeContextServerCommand>(
692 v.clone(),
693 )
694 .ok()
695 .map(|cmd| {
696 settings::ContextServerCommand {
697 path: cmd.command,
698 args: cmd.args.unwrap_or_default(),
699 env: cmd.env,
700 timeout: None,
701 }
702 })?,
703 },
704 ))
705 }));
706 }
707
708 // TODO: translate lsp settings for rust-analyzer and other popular ones to old.lsp
709 }
710}
711
712pub enum SettingsObserverMode {
713 Local(Arc<dyn Fs>),
714 Remote,
715}
716
717#[derive(Clone, Debug, PartialEq)]
718pub enum SettingsObserverEvent {
719 LocalSettingsUpdated(Result<PathBuf, InvalidSettingsError>),
720 LocalTasksUpdated(Result<PathBuf, InvalidSettingsError>),
721 LocalDebugScenariosUpdated(Result<PathBuf, InvalidSettingsError>),
722}
723
724impl EventEmitter<SettingsObserverEvent> for SettingsObserver {}
725
726pub struct SettingsObserver {
727 mode: SettingsObserverMode,
728 downstream_client: Option<AnyProtoClient>,
729 worktree_store: Entity<WorktreeStore>,
730 project_id: u64,
731 task_store: Entity<TaskStore>,
732 _user_settings_watcher: Option<Subscription>,
733 _global_task_config_watcher: Task<()>,
734 _global_debug_config_watcher: Task<()>,
735}
736
737/// SettingsObserver observers changes to .zed/{settings, task}.json files in local worktrees
738/// (or the equivalent protobuf messages from upstream) and updates local settings
739/// and sends notifications downstream.
740/// In ssh mode it also monitors ~/.config/zed/{settings, task}.json and sends the content
741/// upstream.
742impl SettingsObserver {
743 pub fn init(client: &AnyProtoClient) {
744 client.add_entity_message_handler(Self::handle_update_worktree_settings);
745 client.add_entity_message_handler(Self::handle_update_user_settings);
746 }
747
748 pub fn new_local(
749 fs: Arc<dyn Fs>,
750 worktree_store: Entity<WorktreeStore>,
751 task_store: Entity<TaskStore>,
752 cx: &mut Context<Self>,
753 ) -> Self {
754 cx.subscribe(&worktree_store, Self::on_worktree_store_event)
755 .detach();
756
757 Self {
758 worktree_store,
759 task_store,
760 mode: SettingsObserverMode::Local(fs.clone()),
761 downstream_client: None,
762 _user_settings_watcher: None,
763 project_id: REMOTE_SERVER_PROJECT_ID,
764 _global_task_config_watcher: Self::subscribe_to_global_task_file_changes(
765 fs.clone(),
766 paths::tasks_file().clone(),
767 cx,
768 ),
769 _global_debug_config_watcher: Self::subscribe_to_global_debug_scenarios_changes(
770 fs.clone(),
771 paths::debug_scenarios_file().clone(),
772 cx,
773 ),
774 }
775 }
776
777 pub fn new_remote(
778 fs: Arc<dyn Fs>,
779 worktree_store: Entity<WorktreeStore>,
780 task_store: Entity<TaskStore>,
781 upstream_client: Option<AnyProtoClient>,
782 cx: &mut Context<Self>,
783 ) -> Self {
784 let mut user_settings_watcher = None;
785 if cx.try_global::<SettingsStore>().is_some() {
786 if let Some(upstream_client) = upstream_client {
787 let mut user_settings = None;
788 user_settings_watcher = Some(cx.observe_global::<SettingsStore>(move |_, cx| {
789 if let Some(new_settings) = cx.global::<SettingsStore>().raw_user_settings() {
790 if Some(new_settings) != user_settings.as_ref() {
791 if let Some(new_settings_string) =
792 serde_json::to_string(new_settings).ok()
793 {
794 user_settings = Some(new_settings.clone());
795 upstream_client
796 .send(proto::UpdateUserSettings {
797 project_id: REMOTE_SERVER_PROJECT_ID,
798 contents: new_settings_string,
799 })
800 .log_err();
801 }
802 }
803 }
804 }));
805 }
806 };
807
808 Self {
809 worktree_store,
810 task_store,
811 mode: SettingsObserverMode::Remote,
812 downstream_client: None,
813 project_id: REMOTE_SERVER_PROJECT_ID,
814 _user_settings_watcher: user_settings_watcher,
815 _global_task_config_watcher: Self::subscribe_to_global_task_file_changes(
816 fs.clone(),
817 paths::tasks_file().clone(),
818 cx,
819 ),
820 _global_debug_config_watcher: Self::subscribe_to_global_debug_scenarios_changes(
821 fs.clone(),
822 paths::debug_scenarios_file().clone(),
823 cx,
824 ),
825 }
826 }
827
828 pub fn shared(
829 &mut self,
830 project_id: u64,
831 downstream_client: AnyProtoClient,
832 cx: &mut Context<Self>,
833 ) {
834 self.project_id = project_id;
835 self.downstream_client = Some(downstream_client.clone());
836
837 let store = cx.global::<SettingsStore>();
838 for worktree in self.worktree_store.read(cx).worktrees() {
839 let worktree_id = worktree.read(cx).id().to_proto();
840 for (path, content) in store.local_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::Settings).into(),
849 ),
850 })
851 .log_err();
852 }
853 for (path, content, _) in store.local_editorconfig_settings(worktree.read(cx).id()) {
854 downstream_client
855 .send(proto::UpdateWorktreeSettings {
856 project_id,
857 worktree_id,
858 path: path.to_proto(),
859 content: Some(content),
860 kind: Some(
861 local_settings_kind_to_proto(LocalSettingsKind::Editorconfig).into(),
862 ),
863 })
864 .log_err();
865 }
866 }
867 }
868
869 pub fn unshared(&mut self, _: &mut Context<Self>) {
870 self.downstream_client = None;
871 }
872
873 async fn handle_update_worktree_settings(
874 this: Entity<Self>,
875 envelope: TypedEnvelope<proto::UpdateWorktreeSettings>,
876 mut cx: AsyncApp,
877 ) -> anyhow::Result<()> {
878 let kind = match envelope.payload.kind {
879 Some(kind) => proto::LocalSettingsKind::from_i32(kind)
880 .with_context(|| format!("unknown kind {kind}"))?,
881 None => proto::LocalSettingsKind::Settings,
882 };
883 this.update(&mut cx, |this, cx| {
884 let worktree_id = WorktreeId::from_proto(envelope.payload.worktree_id);
885 let Some(worktree) = this
886 .worktree_store
887 .read(cx)
888 .worktree_for_id(worktree_id, cx)
889 else {
890 return;
891 };
892
893 this.update_settings(
894 worktree,
895 [(
896 Arc::<Path>::from_proto(envelope.payload.path.clone()),
897 local_settings_kind_from_proto(kind),
898 envelope.payload.content,
899 )],
900 cx,
901 );
902 })?;
903 Ok(())
904 }
905
906 async fn handle_update_user_settings(
907 _: Entity<Self>,
908 envelope: TypedEnvelope<proto::UpdateUserSettings>,
909 cx: AsyncApp,
910 ) -> anyhow::Result<()> {
911 let new_settings = serde_json::from_str(&envelope.payload.contents).with_context(|| {
912 format!("deserializing {} user settings", envelope.payload.contents)
913 })?;
914 cx.update_global(|settings_store: &mut SettingsStore, cx| {
915 settings_store
916 .set_raw_user_settings(new_settings, cx)
917 .context("setting new user settings")?;
918 anyhow::Ok(())
919 })??;
920 Ok(())
921 }
922
923 fn on_worktree_store_event(
924 &mut self,
925 _: Entity<WorktreeStore>,
926 event: &WorktreeStoreEvent,
927 cx: &mut Context<Self>,
928 ) {
929 if let WorktreeStoreEvent::WorktreeAdded(worktree) = event {
930 cx.subscribe(worktree, |this, worktree, event, cx| {
931 if let worktree::Event::UpdatedEntries(changes) = event {
932 this.update_local_worktree_settings(&worktree, changes, cx)
933 }
934 })
935 .detach()
936 }
937 }
938
939 fn update_local_worktree_settings(
940 &mut self,
941 worktree: &Entity<Worktree>,
942 changes: &UpdatedEntriesSet,
943 cx: &mut Context<Self>,
944 ) {
945 let SettingsObserverMode::Local(fs) = &self.mode else {
946 return;
947 };
948
949 let mut settings_contents = Vec::new();
950 for (path, _, change) in changes.iter() {
951 let (settings_dir, kind) = if path.ends_with(local_settings_file_relative_path()) {
952 let settings_dir = Arc::<Path>::from(
953 path.ancestors()
954 .nth(local_settings_file_relative_path().components().count())
955 .unwrap(),
956 );
957 (settings_dir, LocalSettingsKind::Settings)
958 } else if path.ends_with(local_tasks_file_relative_path()) {
959 let settings_dir = Arc::<Path>::from(
960 path.ancestors()
961 .nth(
962 local_tasks_file_relative_path()
963 .components()
964 .count()
965 .saturating_sub(1),
966 )
967 .unwrap(),
968 );
969 (settings_dir, LocalSettingsKind::Tasks)
970 } else if path.ends_with(local_vscode_tasks_file_relative_path()) {
971 let settings_dir = Arc::<Path>::from(
972 path.ancestors()
973 .nth(
974 local_vscode_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_debug_file_relative_path()) {
983 let settings_dir = Arc::<Path>::from(
984 path.ancestors()
985 .nth(
986 local_debug_file_relative_path()
987 .components()
988 .count()
989 .saturating_sub(1),
990 )
991 .unwrap(),
992 );
993 (settings_dir, LocalSettingsKind::Debug)
994 } else if path.ends_with(local_vscode_launch_file_relative_path()) {
995 let settings_dir = Arc::<Path>::from(
996 path.ancestors()
997 .nth(
998 local_vscode_tasks_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(EDITORCONFIG_NAME) {
1007 let Some(settings_dir) = path.parent().map(Arc::from) else {
1008 continue;
1009 };
1010 (settings_dir, LocalSettingsKind::Editorconfig)
1011 } else {
1012 continue;
1013 };
1014
1015 let removed = change == &PathChange::Removed;
1016 let fs = fs.clone();
1017 let abs_path = match worktree.read(cx).absolutize(path) {
1018 Ok(abs_path) => abs_path,
1019 Err(e) => {
1020 log::warn!("Cannot absolutize {path:?} received as {change:?} FS change: {e}");
1021 continue;
1022 }
1023 };
1024 settings_contents.push(async move {
1025 (
1026 settings_dir,
1027 kind,
1028 if removed {
1029 None
1030 } else {
1031 Some(
1032 async move {
1033 let content = fs.load(&abs_path).await?;
1034 if abs_path.ends_with(local_vscode_tasks_file_relative_path()) {
1035 let vscode_tasks =
1036 parse_json_with_comments::<VsCodeTaskFile>(&content)
1037 .with_context(|| {
1038 format!("parsing VSCode tasks, file {abs_path:?}")
1039 })?;
1040 let zed_tasks = TaskTemplates::try_from(vscode_tasks)
1041 .with_context(|| {
1042 format!(
1043 "converting VSCode tasks into Zed ones, file {abs_path:?}"
1044 )
1045 })?;
1046 serde_json::to_string(&zed_tasks).with_context(|| {
1047 format!(
1048 "serializing Zed tasks into JSON, file {abs_path:?}"
1049 )
1050 })
1051 } else if abs_path.ends_with(local_vscode_launch_file_relative_path()) {
1052 let vscode_tasks =
1053 parse_json_with_comments::<VsCodeDebugTaskFile>(&content)
1054 .with_context(|| {
1055 format!("parsing VSCode debug tasks, file {abs_path:?}")
1056 })?;
1057 let zed_tasks = DebugTaskFile::try_from(vscode_tasks)
1058 .with_context(|| {
1059 format!(
1060 "converting VSCode debug tasks into Zed ones, file {abs_path:?}"
1061 )
1062 })?;
1063 serde_json::to_string(&zed_tasks).with_context(|| {
1064 format!(
1065 "serializing Zed tasks into JSON, file {abs_path:?}"
1066 )
1067 })
1068 } else {
1069 Ok(content)
1070 }
1071 }
1072 .await,
1073 )
1074 },
1075 )
1076 });
1077 }
1078
1079 if settings_contents.is_empty() {
1080 return;
1081 }
1082
1083 let worktree = worktree.clone();
1084 cx.spawn(async move |this, cx| {
1085 let settings_contents: Vec<(Arc<Path>, _, _)> =
1086 futures::future::join_all(settings_contents).await;
1087 cx.update(|cx| {
1088 this.update(cx, |this, cx| {
1089 this.update_settings(
1090 worktree,
1091 settings_contents.into_iter().map(|(path, kind, content)| {
1092 (path, kind, content.and_then(|c| c.log_err()))
1093 }),
1094 cx,
1095 )
1096 })
1097 })
1098 })
1099 .detach();
1100 }
1101
1102 fn update_settings(
1103 &mut self,
1104 worktree: Entity<Worktree>,
1105 settings_contents: impl IntoIterator<Item = (Arc<Path>, LocalSettingsKind, Option<String>)>,
1106 cx: &mut Context<Self>,
1107 ) {
1108 let worktree_id = worktree.read(cx).id();
1109 let remote_worktree_id = worktree.read(cx).id();
1110 let task_store = self.task_store.clone();
1111
1112 for (directory, kind, file_content) in settings_contents {
1113 match kind {
1114 LocalSettingsKind::Settings | LocalSettingsKind::Editorconfig => cx
1115 .update_global::<SettingsStore, _>(|store, cx| {
1116 let result = store.set_local_settings(
1117 worktree_id,
1118 directory.clone(),
1119 kind,
1120 file_content.as_deref(),
1121 cx,
1122 );
1123
1124 match result {
1125 Err(InvalidSettingsError::LocalSettings { path, message }) => {
1126 log::error!("Failed to set local settings in {path:?}: {message}");
1127 cx.emit(SettingsObserverEvent::LocalSettingsUpdated(Err(
1128 InvalidSettingsError::LocalSettings { path, message },
1129 )));
1130 }
1131 Err(e) => {
1132 log::error!("Failed to set local settings: {e}");
1133 }
1134 Ok(()) => {
1135 cx.emit(SettingsObserverEvent::LocalSettingsUpdated(Ok(
1136 directory.join(local_settings_file_relative_path())
1137 )));
1138 }
1139 }
1140 }),
1141 LocalSettingsKind::Tasks => {
1142 let result = task_store.update(cx, |task_store, cx| {
1143 task_store.update_user_tasks(
1144 TaskSettingsLocation::Worktree(SettingsLocation {
1145 worktree_id,
1146 path: directory.as_ref(),
1147 }),
1148 file_content.as_deref(),
1149 cx,
1150 )
1151 });
1152
1153 match result {
1154 Err(InvalidSettingsError::Tasks { path, message }) => {
1155 log::error!("Failed to set local tasks in {path:?}: {message:?}");
1156 cx.emit(SettingsObserverEvent::LocalTasksUpdated(Err(
1157 InvalidSettingsError::Tasks { path, message },
1158 )));
1159 }
1160 Err(e) => {
1161 log::error!("Failed to set local tasks: {e}");
1162 }
1163 Ok(()) => {
1164 cx.emit(SettingsObserverEvent::LocalTasksUpdated(Ok(
1165 directory.join(task_file_name())
1166 )));
1167 }
1168 }
1169 }
1170 LocalSettingsKind::Debug => {
1171 let result = task_store.update(cx, |task_store, cx| {
1172 task_store.update_user_debug_scenarios(
1173 TaskSettingsLocation::Worktree(SettingsLocation {
1174 worktree_id,
1175 path: directory.as_ref(),
1176 }),
1177 file_content.as_deref(),
1178 cx,
1179 )
1180 });
1181
1182 match result {
1183 Err(InvalidSettingsError::Debug { path, message }) => {
1184 log::error!(
1185 "Failed to set local debug scenarios in {path:?}: {message:?}"
1186 );
1187 cx.emit(SettingsObserverEvent::LocalTasksUpdated(Err(
1188 InvalidSettingsError::Debug { path, message },
1189 )));
1190 }
1191 Err(e) => {
1192 log::error!("Failed to set local tasks: {e}");
1193 }
1194 Ok(()) => {
1195 cx.emit(SettingsObserverEvent::LocalTasksUpdated(Ok(
1196 directory.join(task_file_name())
1197 )));
1198 }
1199 }
1200 }
1201 };
1202
1203 if let Some(downstream_client) = &self.downstream_client {
1204 downstream_client
1205 .send(proto::UpdateWorktreeSettings {
1206 project_id: self.project_id,
1207 worktree_id: remote_worktree_id.to_proto(),
1208 path: directory.to_proto(),
1209 content: file_content.clone(),
1210 kind: Some(local_settings_kind_to_proto(kind).into()),
1211 })
1212 .log_err();
1213 }
1214 }
1215 }
1216
1217 fn subscribe_to_global_task_file_changes(
1218 fs: Arc<dyn Fs>,
1219 file_path: PathBuf,
1220 cx: &mut Context<Self>,
1221 ) -> Task<()> {
1222 let mut user_tasks_file_rx =
1223 watch_config_file(cx.background_executor(), fs, file_path.clone());
1224 let user_tasks_content = cx.background_executor().block(user_tasks_file_rx.next());
1225 let weak_entry = cx.weak_entity();
1226 cx.spawn(async move |settings_observer, cx| {
1227 let Ok(task_store) = settings_observer.read_with(cx, |settings_observer, _| {
1228 settings_observer.task_store.clone()
1229 }) else {
1230 return;
1231 };
1232 if let Some(user_tasks_content) = user_tasks_content {
1233 let Ok(()) = task_store.update(cx, |task_store, cx| {
1234 task_store
1235 .update_user_tasks(
1236 TaskSettingsLocation::Global(&file_path),
1237 Some(&user_tasks_content),
1238 cx,
1239 )
1240 .log_err();
1241 }) else {
1242 return;
1243 };
1244 }
1245 while let Some(user_tasks_content) = user_tasks_file_rx.next().await {
1246 let Ok(result) = task_store.update(cx, |task_store, cx| {
1247 task_store.update_user_tasks(
1248 TaskSettingsLocation::Global(&file_path),
1249 Some(&user_tasks_content),
1250 cx,
1251 )
1252 }) else {
1253 break;
1254 };
1255
1256 weak_entry
1257 .update(cx, |_, cx| match result {
1258 Ok(()) => cx.emit(SettingsObserverEvent::LocalTasksUpdated(Ok(
1259 file_path.clone()
1260 ))),
1261 Err(err) => cx.emit(SettingsObserverEvent::LocalTasksUpdated(Err(
1262 InvalidSettingsError::Tasks {
1263 path: file_path.clone(),
1264 message: err.to_string(),
1265 },
1266 ))),
1267 })
1268 .ok();
1269 }
1270 })
1271 }
1272 fn subscribe_to_global_debug_scenarios_changes(
1273 fs: Arc<dyn Fs>,
1274 file_path: PathBuf,
1275 cx: &mut Context<Self>,
1276 ) -> Task<()> {
1277 let mut user_tasks_file_rx =
1278 watch_config_file(cx.background_executor(), fs, file_path.clone());
1279 let user_tasks_content = cx.background_executor().block(user_tasks_file_rx.next());
1280 let weak_entry = cx.weak_entity();
1281 cx.spawn(async move |settings_observer, cx| {
1282 let Ok(task_store) = settings_observer.read_with(cx, |settings_observer, _| {
1283 settings_observer.task_store.clone()
1284 }) else {
1285 return;
1286 };
1287 if let Some(user_tasks_content) = user_tasks_content {
1288 let Ok(()) = task_store.update(cx, |task_store, cx| {
1289 task_store
1290 .update_user_debug_scenarios(
1291 TaskSettingsLocation::Global(&file_path),
1292 Some(&user_tasks_content),
1293 cx,
1294 )
1295 .log_err();
1296 }) else {
1297 return;
1298 };
1299 }
1300 while let Some(user_tasks_content) = user_tasks_file_rx.next().await {
1301 let Ok(result) = task_store.update(cx, |task_store, cx| {
1302 task_store.update_user_debug_scenarios(
1303 TaskSettingsLocation::Global(&file_path),
1304 Some(&user_tasks_content),
1305 cx,
1306 )
1307 }) else {
1308 break;
1309 };
1310
1311 weak_entry
1312 .update(cx, |_, cx| match result {
1313 Ok(()) => cx.emit(SettingsObserverEvent::LocalDebugScenariosUpdated(Ok(
1314 file_path.clone(),
1315 ))),
1316 Err(err) => cx.emit(SettingsObserverEvent::LocalDebugScenariosUpdated(
1317 Err(InvalidSettingsError::Tasks {
1318 path: file_path.clone(),
1319 message: err.to_string(),
1320 }),
1321 )),
1322 })
1323 .ok();
1324 }
1325 })
1326 }
1327}
1328
1329pub fn local_settings_kind_from_proto(kind: proto::LocalSettingsKind) -> LocalSettingsKind {
1330 match kind {
1331 proto::LocalSettingsKind::Settings => LocalSettingsKind::Settings,
1332 proto::LocalSettingsKind::Tasks => LocalSettingsKind::Tasks,
1333 proto::LocalSettingsKind::Editorconfig => LocalSettingsKind::Editorconfig,
1334 proto::LocalSettingsKind::Debug => LocalSettingsKind::Debug,
1335 }
1336}
1337
1338pub fn local_settings_kind_to_proto(kind: LocalSettingsKind) -> proto::LocalSettingsKind {
1339 match kind {
1340 LocalSettingsKind::Settings => proto::LocalSettingsKind::Settings,
1341 LocalSettingsKind::Tasks => proto::LocalSettingsKind::Tasks,
1342 LocalSettingsKind::Editorconfig => proto::LocalSettingsKind::Editorconfig,
1343 LocalSettingsKind::Debug => proto::LocalSettingsKind::Debug,
1344 }
1345}