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