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 fn enabled(&self) -> bool {
174 match self {
175 ContextServerSettings::Custom { enabled, .. } => *enabled,
176 ContextServerSettings::Extension { enabled, .. } => *enabled,
177 }
178 }
179
180 pub 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 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 fn min() -> Self {
251 Self::Hint
252 }
253
254 pub 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: settings::DelayMs,
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 fn inline_blame_delay(&self) -> Option<Duration> {
360 if self.inline_blame.delay_ms.0 > 0 {
361 Some(Duration::from_millis(self.inline_blame.delay_ms.0))
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: 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().0,
508 },
509 inline: InlineDiagnosticsSettings {
510 enabled: inline_diagnostics.enabled.unwrap(),
511 update_debounce_ms: inline_diagnostics.update_debounce_ms.unwrap().0,
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 .result()
732 .context("setting new user settings")?;
733 anyhow::Ok(())
734 })??;
735 Ok(())
736 }
737
738 fn on_worktree_store_event(
739 &mut self,
740 _: Entity<WorktreeStore>,
741 event: &WorktreeStoreEvent,
742 cx: &mut Context<Self>,
743 ) {
744 if let WorktreeStoreEvent::WorktreeAdded(worktree) = event {
745 cx.subscribe(worktree, |this, worktree, event, cx| {
746 if let worktree::Event::UpdatedEntries(changes) = event {
747 this.update_local_worktree_settings(&worktree, changes, cx)
748 }
749 })
750 .detach()
751 }
752 }
753
754 fn update_local_worktree_settings(
755 &mut self,
756 worktree: &Entity<Worktree>,
757 changes: &UpdatedEntriesSet,
758 cx: &mut Context<Self>,
759 ) {
760 let SettingsObserverMode::Local(fs) = &self.mode else {
761 return;
762 };
763
764 let mut settings_contents = Vec::new();
765 for (path, _, change) in changes.iter() {
766 let (settings_dir, kind) = if path.ends_with(local_settings_file_relative_path()) {
767 let settings_dir = path
768 .ancestors()
769 .nth(local_settings_file_relative_path().components().count())
770 .unwrap()
771 .into();
772 (settings_dir, LocalSettingsKind::Settings)
773 } else if path.ends_with(local_tasks_file_relative_path()) {
774 let settings_dir = path
775 .ancestors()
776 .nth(
777 local_tasks_file_relative_path()
778 .components()
779 .count()
780 .saturating_sub(1),
781 )
782 .unwrap()
783 .into();
784 (settings_dir, LocalSettingsKind::Tasks)
785 } else if path.ends_with(local_vscode_tasks_file_relative_path()) {
786 let settings_dir = path
787 .ancestors()
788 .nth(
789 local_vscode_tasks_file_relative_path()
790 .components()
791 .count()
792 .saturating_sub(1),
793 )
794 .unwrap()
795 .into();
796 (settings_dir, LocalSettingsKind::Tasks)
797 } else if path.ends_with(local_debug_file_relative_path()) {
798 let settings_dir = path
799 .ancestors()
800 .nth(
801 local_debug_file_relative_path()
802 .components()
803 .count()
804 .saturating_sub(1),
805 )
806 .unwrap()
807 .into();
808 (settings_dir, LocalSettingsKind::Debug)
809 } else if path.ends_with(local_vscode_launch_file_relative_path()) {
810 let settings_dir = path
811 .ancestors()
812 .nth(
813 local_vscode_tasks_file_relative_path()
814 .components()
815 .count()
816 .saturating_sub(1),
817 )
818 .unwrap()
819 .into();
820 (settings_dir, LocalSettingsKind::Debug)
821 } else if path.ends_with(RelPath::unix(EDITORCONFIG_NAME).unwrap()) {
822 let Some(settings_dir) = path.parent().map(Arc::from) else {
823 continue;
824 };
825 (settings_dir, LocalSettingsKind::Editorconfig)
826 } else {
827 continue;
828 };
829
830 let removed = change == &PathChange::Removed;
831 let fs = fs.clone();
832 let abs_path = worktree.read(cx).absolutize(path);
833 settings_contents.push(async move {
834 (
835 settings_dir,
836 kind,
837 if removed {
838 None
839 } else {
840 Some(
841 async move {
842 let content = fs.load(&abs_path).await?;
843 if abs_path.ends_with(local_vscode_tasks_file_relative_path().as_std_path()) {
844 let vscode_tasks =
845 parse_json_with_comments::<VsCodeTaskFile>(&content)
846 .with_context(|| {
847 format!("parsing VSCode tasks, file {abs_path:?}")
848 })?;
849 let zed_tasks = TaskTemplates::try_from(vscode_tasks)
850 .with_context(|| {
851 format!(
852 "converting VSCode tasks into Zed ones, file {abs_path:?}"
853 )
854 })?;
855 serde_json::to_string(&zed_tasks).with_context(|| {
856 format!(
857 "serializing Zed tasks into JSON, file {abs_path:?}"
858 )
859 })
860 } else if abs_path.ends_with(local_vscode_launch_file_relative_path().as_std_path()) {
861 let vscode_tasks =
862 parse_json_with_comments::<VsCodeDebugTaskFile>(&content)
863 .with_context(|| {
864 format!("parsing VSCode debug tasks, file {abs_path:?}")
865 })?;
866 let zed_tasks = DebugTaskFile::try_from(vscode_tasks)
867 .with_context(|| {
868 format!(
869 "converting VSCode debug tasks into Zed ones, file {abs_path:?}"
870 )
871 })?;
872 serde_json::to_string(&zed_tasks).with_context(|| {
873 format!(
874 "serializing Zed tasks into JSON, file {abs_path:?}"
875 )
876 })
877 } else {
878 Ok(content)
879 }
880 }
881 .await,
882 )
883 },
884 )
885 });
886 }
887
888 if settings_contents.is_empty() {
889 return;
890 }
891
892 let worktree = worktree.clone();
893 cx.spawn(async move |this, cx| {
894 let settings_contents: Vec<(Arc<RelPath>, _, _)> =
895 futures::future::join_all(settings_contents).await;
896 cx.update(|cx| {
897 this.update(cx, |this, cx| {
898 this.update_settings(
899 worktree,
900 settings_contents.into_iter().map(|(path, kind, content)| {
901 (path, kind, content.and_then(|c| c.log_err()))
902 }),
903 cx,
904 )
905 })
906 })
907 })
908 .detach();
909 }
910
911 fn update_settings(
912 &mut self,
913 worktree: Entity<Worktree>,
914 settings_contents: impl IntoIterator<Item = (Arc<RelPath>, LocalSettingsKind, Option<String>)>,
915 cx: &mut Context<Self>,
916 ) {
917 let worktree_id = worktree.read(cx).id();
918 let remote_worktree_id = worktree.read(cx).id();
919 let task_store = self.task_store.clone();
920
921 for (directory, kind, file_content) in settings_contents {
922 match kind {
923 LocalSettingsKind::Settings | LocalSettingsKind::Editorconfig => cx
924 .update_global::<SettingsStore, _>(|store, cx| {
925 let result = store.set_local_settings(
926 worktree_id,
927 directory.clone(),
928 kind,
929 file_content.as_deref(),
930 cx,
931 );
932
933 match result {
934 Err(InvalidSettingsError::LocalSettings { path, message }) => {
935 log::error!("Failed to set local settings in {path:?}: {message}");
936 cx.emit(SettingsObserverEvent::LocalSettingsUpdated(Err(
937 InvalidSettingsError::LocalSettings { path, message },
938 )));
939 }
940 Err(e) => {
941 log::error!("Failed to set local settings: {e}");
942 }
943 Ok(()) => {
944 cx.emit(SettingsObserverEvent::LocalSettingsUpdated(Ok(directory
945 .as_std_path()
946 .join(local_settings_file_relative_path().as_std_path()))));
947 }
948 }
949 }),
950 LocalSettingsKind::Tasks => {
951 let result = task_store.update(cx, |task_store, cx| {
952 task_store.update_user_tasks(
953 TaskSettingsLocation::Worktree(SettingsLocation {
954 worktree_id,
955 path: directory.as_ref(),
956 }),
957 file_content.as_deref(),
958 cx,
959 )
960 });
961
962 match result {
963 Err(InvalidSettingsError::Tasks { path, message }) => {
964 log::error!("Failed to set local tasks in {path:?}: {message:?}");
965 cx.emit(SettingsObserverEvent::LocalTasksUpdated(Err(
966 InvalidSettingsError::Tasks { path, message },
967 )));
968 }
969 Err(e) => {
970 log::error!("Failed to set local tasks: {e}");
971 }
972 Ok(()) => {
973 cx.emit(SettingsObserverEvent::LocalTasksUpdated(Ok(directory
974 .as_std_path()
975 .join(task_file_name()))));
976 }
977 }
978 }
979 LocalSettingsKind::Debug => {
980 let result = task_store.update(cx, |task_store, cx| {
981 task_store.update_user_debug_scenarios(
982 TaskSettingsLocation::Worktree(SettingsLocation {
983 worktree_id,
984 path: directory.as_ref(),
985 }),
986 file_content.as_deref(),
987 cx,
988 )
989 });
990
991 match result {
992 Err(InvalidSettingsError::Debug { path, message }) => {
993 log::error!(
994 "Failed to set local debug scenarios in {path:?}: {message:?}"
995 );
996 cx.emit(SettingsObserverEvent::LocalTasksUpdated(Err(
997 InvalidSettingsError::Debug { path, message },
998 )));
999 }
1000 Err(e) => {
1001 log::error!("Failed to set local tasks: {e}");
1002 }
1003 Ok(()) => {
1004 cx.emit(SettingsObserverEvent::LocalTasksUpdated(Ok(directory
1005 .as_std_path()
1006 .join(task_file_name()))));
1007 }
1008 }
1009 }
1010 };
1011
1012 if let Some(downstream_client) = &self.downstream_client {
1013 downstream_client
1014 .send(proto::UpdateWorktreeSettings {
1015 project_id: self.project_id,
1016 worktree_id: remote_worktree_id.to_proto(),
1017 path: directory.to_proto(),
1018 content: file_content.clone(),
1019 kind: Some(local_settings_kind_to_proto(kind).into()),
1020 })
1021 .log_err();
1022 }
1023 }
1024 }
1025
1026 fn subscribe_to_global_task_file_changes(
1027 fs: Arc<dyn Fs>,
1028 file_path: PathBuf,
1029 cx: &mut Context<Self>,
1030 ) -> Task<()> {
1031 let mut user_tasks_file_rx =
1032 watch_config_file(cx.background_executor(), fs, file_path.clone());
1033 let user_tasks_content = cx.background_executor().block(user_tasks_file_rx.next());
1034 let weak_entry = cx.weak_entity();
1035 cx.spawn(async move |settings_observer, cx| {
1036 let Ok(task_store) = settings_observer.read_with(cx, |settings_observer, _| {
1037 settings_observer.task_store.clone()
1038 }) else {
1039 return;
1040 };
1041 if let Some(user_tasks_content) = user_tasks_content {
1042 let Ok(()) = task_store.update(cx, |task_store, cx| {
1043 task_store
1044 .update_user_tasks(
1045 TaskSettingsLocation::Global(&file_path),
1046 Some(&user_tasks_content),
1047 cx,
1048 )
1049 .log_err();
1050 }) else {
1051 return;
1052 };
1053 }
1054 while let Some(user_tasks_content) = user_tasks_file_rx.next().await {
1055 let Ok(result) = task_store.update(cx, |task_store, cx| {
1056 task_store.update_user_tasks(
1057 TaskSettingsLocation::Global(&file_path),
1058 Some(&user_tasks_content),
1059 cx,
1060 )
1061 }) else {
1062 break;
1063 };
1064
1065 weak_entry
1066 .update(cx, |_, cx| match result {
1067 Ok(()) => cx.emit(SettingsObserverEvent::LocalTasksUpdated(Ok(
1068 file_path.clone()
1069 ))),
1070 Err(err) => cx.emit(SettingsObserverEvent::LocalTasksUpdated(Err(
1071 InvalidSettingsError::Tasks {
1072 path: file_path.clone(),
1073 message: err.to_string(),
1074 },
1075 ))),
1076 })
1077 .ok();
1078 }
1079 })
1080 }
1081 fn subscribe_to_global_debug_scenarios_changes(
1082 fs: Arc<dyn Fs>,
1083 file_path: PathBuf,
1084 cx: &mut Context<Self>,
1085 ) -> Task<()> {
1086 let mut user_tasks_file_rx =
1087 watch_config_file(cx.background_executor(), fs, file_path.clone());
1088 let user_tasks_content = cx.background_executor().block(user_tasks_file_rx.next());
1089 let weak_entry = cx.weak_entity();
1090 cx.spawn(async move |settings_observer, cx| {
1091 let Ok(task_store) = settings_observer.read_with(cx, |settings_observer, _| {
1092 settings_observer.task_store.clone()
1093 }) else {
1094 return;
1095 };
1096 if let Some(user_tasks_content) = user_tasks_content {
1097 let Ok(()) = task_store.update(cx, |task_store, cx| {
1098 task_store
1099 .update_user_debug_scenarios(
1100 TaskSettingsLocation::Global(&file_path),
1101 Some(&user_tasks_content),
1102 cx,
1103 )
1104 .log_err();
1105 }) else {
1106 return;
1107 };
1108 }
1109 while let Some(user_tasks_content) = user_tasks_file_rx.next().await {
1110 let Ok(result) = task_store.update(cx, |task_store, cx| {
1111 task_store.update_user_debug_scenarios(
1112 TaskSettingsLocation::Global(&file_path),
1113 Some(&user_tasks_content),
1114 cx,
1115 )
1116 }) else {
1117 break;
1118 };
1119
1120 weak_entry
1121 .update(cx, |_, cx| match result {
1122 Ok(()) => cx.emit(SettingsObserverEvent::LocalDebugScenariosUpdated(Ok(
1123 file_path.clone(),
1124 ))),
1125 Err(err) => cx.emit(SettingsObserverEvent::LocalDebugScenariosUpdated(
1126 Err(InvalidSettingsError::Tasks {
1127 path: file_path.clone(),
1128 message: err.to_string(),
1129 }),
1130 )),
1131 })
1132 .ok();
1133 }
1134 })
1135 }
1136}
1137
1138pub fn local_settings_kind_from_proto(kind: proto::LocalSettingsKind) -> LocalSettingsKind {
1139 match kind {
1140 proto::LocalSettingsKind::Settings => LocalSettingsKind::Settings,
1141 proto::LocalSettingsKind::Tasks => LocalSettingsKind::Tasks,
1142 proto::LocalSettingsKind::Editorconfig => LocalSettingsKind::Editorconfig,
1143 proto::LocalSettingsKind::Debug => LocalSettingsKind::Debug,
1144 }
1145}
1146
1147pub fn local_settings_kind_to_proto(kind: LocalSettingsKind) -> proto::LocalSettingsKind {
1148 match kind {
1149 LocalSettingsKind::Settings => proto::LocalSettingsKind::Settings,
1150 LocalSettingsKind::Tasks => proto::LocalSettingsKind::Tasks,
1151 LocalSettingsKind::Editorconfig => proto::LocalSettingsKind::Editorconfig,
1152 LocalSettingsKind::Debug => proto::LocalSettingsKind::Debug,
1153 }
1154}
1155
1156#[derive(Debug, Clone)]
1157pub struct DapSettings {
1158 pub binary: DapBinary,
1159 pub args: Vec<String>,
1160 pub env: HashMap<String, String>,
1161}
1162
1163impl From<DapSettingsContent> for DapSettings {
1164 fn from(content: DapSettingsContent) -> Self {
1165 DapSettings {
1166 binary: content
1167 .binary
1168 .map_or_else(|| DapBinary::Default, |binary| DapBinary::Custom(binary)),
1169 args: content.args.unwrap_or_default(),
1170 env: content.env.unwrap_or_default(),
1171 }
1172 }
1173}
1174
1175#[derive(Debug, Clone)]
1176pub enum DapBinary {
1177 Default,
1178 Custom(String),
1179}