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