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