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