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