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