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