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