1use anyhow::Context as _;
2use collections::HashMap;
3use dap::adapters::DebugAdapterName;
4use fs::Fs;
5use gpui::{App, AsyncApp, BorrowAppContext, Context, Entity, EventEmitter};
6use lsp::LanguageServerName;
7use paths::{
8 local_debug_file_relative_path, local_settings_file_relative_path,
9 local_tasks_file_relative_path, local_vscode_tasks_file_relative_path, EDITORCONFIG_NAME,
10};
11use rpc::{
12 proto::{self, FromProto, ToProto},
13 AnyProtoClient, TypedEnvelope,
14};
15use schemars::JsonSchema;
16use serde::{Deserialize, Serialize};
17use settings::{
18 parse_json_with_comments, InvalidSettingsError, LocalSettingsKind, Settings, SettingsLocation,
19 SettingsSources, SettingsStore, TaskKind,
20};
21use std::{path::Path, sync::Arc, time::Duration};
22use task::{TaskTemplates, VsCodeTaskFile};
23use util::ResultExt;
24use worktree::{PathChange, UpdatedEntriesSet, Worktree, WorktreeId};
25
26use crate::{
27 task_store::TaskStore,
28 worktree_store::{WorktreeStore, WorktreeStoreEvent},
29};
30
31#[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema)]
32pub struct ProjectSettings {
33 /// Configuration for language servers.
34 ///
35 /// The following settings can be overridden for specific language servers:
36 /// - initialization_options
37 ///
38 /// To override settings for a language, add an entry for that language server's
39 /// name to the lsp value.
40 /// Default: null
41 #[serde(default)]
42 pub lsp: HashMap<LanguageServerName, LspSettings>,
43
44 /// Configuration for Debugger-related features
45 #[serde(default)]
46 pub dap: HashMap<DebugAdapterName, DapSettings>,
47
48 /// Configuration for Diagnostics-related features.
49 #[serde(default)]
50 pub diagnostics: DiagnosticsSettings,
51
52 /// Configuration for Git-related features
53 #[serde(default)]
54 pub git: GitSettings,
55
56 /// Configuration for Node-related features
57 #[serde(default)]
58 pub node: NodeBinarySettings,
59
60 /// Configuration for how direnv configuration should be loaded
61 #[serde(default)]
62 pub load_direnv: DirenvSettings,
63
64 /// Configuration for session-related features
65 #[serde(default)]
66 pub session: SessionSettings,
67}
68
69#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize, JsonSchema)]
70#[serde(rename_all = "snake_case")]
71pub struct DapSettings {
72 pub binary: Option<String>,
73}
74
75#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize, JsonSchema)]
76pub struct NodeBinarySettings {
77 /// The path to the Node binary.
78 pub path: Option<String>,
79 /// The path to the npm binary Zed should use (defaults to `.path/../npm`).
80 pub npm_path: Option<String>,
81 /// If disabled, Zed will download its own copy of Node.
82 #[serde(default)]
83 pub ignore_system_version: Option<bool>,
84}
85
86#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema)]
87#[serde(rename_all = "snake_case")]
88pub enum DirenvSettings {
89 /// Load direnv configuration through a shell hook
90 ShellHook,
91 /// Load direnv configuration directly using `direnv export json`
92 #[default]
93 Direct,
94}
95
96#[derive(Copy, Clone, Debug, Default, Serialize, Deserialize, JsonSchema)]
97pub struct DiagnosticsSettings {
98 /// Whether or not to include warning diagnostics
99 #[serde(default = "true_value")]
100 pub include_warnings: bool,
101
102 /// Settings for showing inline diagnostics
103 #[serde(default)]
104 pub inline: InlineDiagnosticsSettings,
105}
106
107#[derive(Clone, Copy, Debug, Serialize, Deserialize, JsonSchema)]
108pub struct InlineDiagnosticsSettings {
109 /// Whether or not to show inline diagnostics
110 ///
111 /// Default: false
112 #[serde(default)]
113 pub enabled: bool,
114 /// Whether to only show the inline diaganostics after a delay after the
115 /// last editor event.
116 ///
117 /// Default: 150
118 #[serde(default = "default_inline_diagnostics_debounce_ms")]
119 pub update_debounce_ms: u64,
120 /// The amount of padding between the end of the source line and the start
121 /// of the inline diagnostic in units of columns.
122 ///
123 /// Default: 4
124 #[serde(default = "default_inline_diagnostics_padding")]
125 pub padding: u32,
126 /// The minimum column to display inline diagnostics. This setting can be
127 /// used to horizontally align inline diagnostics at some position. Lines
128 /// longer than this value will still push diagnostics further to the right.
129 ///
130 /// Default: 0
131 #[serde(default)]
132 pub min_column: u32,
133
134 #[serde(default)]
135 pub max_severity: Option<DiagnosticSeverity>,
136}
137
138#[derive(Clone, Copy, Debug, Serialize, Deserialize, JsonSchema)]
139#[serde(rename_all = "snake_case")]
140pub enum DiagnosticSeverity {
141 Error,
142 Warning,
143 Info,
144 Hint,
145}
146
147impl Default for InlineDiagnosticsSettings {
148 fn default() -> Self {
149 Self {
150 enabled: false,
151 update_debounce_ms: default_inline_diagnostics_debounce_ms(),
152 padding: default_inline_diagnostics_padding(),
153 min_column: 0,
154 max_severity: None,
155 }
156 }
157}
158
159fn default_inline_diagnostics_debounce_ms() -> u64 {
160 150
161}
162
163fn default_inline_diagnostics_padding() -> u32 {
164 4
165}
166
167#[derive(Copy, Clone, Debug, Default, Serialize, Deserialize, JsonSchema)]
168pub struct GitSettings {
169 /// Whether or not to show the git gutter.
170 ///
171 /// Default: tracked_files
172 pub git_gutter: Option<GitGutterSetting>,
173 /// Sets the debounce threshold (in milliseconds) after which changes are reflected in the git gutter.
174 ///
175 /// Default: null
176 pub gutter_debounce: Option<u64>,
177 /// Whether or not to show git blame data inline in
178 /// the currently focused line.
179 ///
180 /// Default: on
181 pub inline_blame: Option<InlineBlameSettings>,
182 /// How hunks are displayed visually in the editor.
183 ///
184 /// Default: staged_hollow
185 pub hunk_style: Option<GitHunkStyleSetting>,
186}
187
188impl GitSettings {
189 pub fn inline_blame_enabled(&self) -> bool {
190 #[allow(unknown_lints, clippy::manual_unwrap_or_default)]
191 match self.inline_blame {
192 Some(InlineBlameSettings { enabled, .. }) => enabled,
193 _ => false,
194 }
195 }
196
197 pub fn inline_blame_delay(&self) -> Option<Duration> {
198 match self.inline_blame {
199 Some(InlineBlameSettings {
200 delay_ms: Some(delay_ms),
201 ..
202 }) if delay_ms > 0 => Some(Duration::from_millis(delay_ms)),
203 _ => None,
204 }
205 }
206
207 pub fn show_inline_commit_summary(&self) -> bool {
208 match self.inline_blame {
209 Some(InlineBlameSettings {
210 show_commit_summary,
211 ..
212 }) => show_commit_summary,
213 _ => false,
214 }
215 }
216}
217
218#[derive(Clone, Copy, Debug, Default, Serialize, Deserialize, JsonSchema)]
219#[serde(rename_all = "snake_case")]
220pub enum GitHunkStyleSetting {
221 /// Show unstaged hunks with a filled background and staged hunks hollow.
222 #[default]
223 StagedHollow,
224 /// Show unstaged hunks hollow and staged hunks with a filled background.
225 UnstagedHollow,
226}
227
228#[derive(Clone, Copy, Debug, Default, Serialize, Deserialize, JsonSchema)]
229#[serde(rename_all = "snake_case")]
230pub enum GitGutterSetting {
231 /// Show git gutter in tracked files.
232 #[default]
233 TrackedFiles,
234 /// Hide git gutter
235 Hide,
236}
237
238#[derive(Clone, Copy, Debug, Default, Serialize, Deserialize, JsonSchema)]
239#[serde(rename_all = "snake_case")]
240pub struct InlineBlameSettings {
241 /// Whether or not to show git blame data inline in
242 /// the currently focused line.
243 ///
244 /// Default: true
245 #[serde(default = "true_value")]
246 pub enabled: bool,
247 /// Whether to only show the inline blame information
248 /// after a delay once the cursor stops moving.
249 ///
250 /// Default: 0
251 pub delay_ms: Option<u64>,
252 /// The minimum column number to show the inline blame information at
253 ///
254 /// Default: 0
255 pub min_column: Option<u32>,
256 /// Whether to show commit summary as part of the inline blame.
257 ///
258 /// Default: false
259 #[serde(default)]
260 pub show_commit_summary: bool,
261}
262
263const fn true_value() -> bool {
264 true
265}
266
267#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
268pub struct BinarySettings {
269 pub path: Option<String>,
270 pub arguments: Option<Vec<String>>,
271 pub ignore_system_version: Option<bool>,
272}
273
274#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
275#[serde(rename_all = "snake_case")]
276pub struct LspSettings {
277 pub binary: Option<BinarySettings>,
278 pub initialization_options: Option<serde_json::Value>,
279 pub settings: Option<serde_json::Value>,
280}
281
282#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema)]
283pub struct SessionSettings {
284 /// Whether or not to restore unsaved buffers on restart.
285 ///
286 /// If this is true, user won't be prompted whether to save/discard
287 /// dirty files when closing the application.
288 ///
289 /// Default: true
290 pub restore_unsaved_buffers: bool,
291}
292
293impl Default for SessionSettings {
294 fn default() -> Self {
295 Self {
296 restore_unsaved_buffers: true,
297 }
298 }
299}
300
301impl Settings for ProjectSettings {
302 const KEY: Option<&'static str> = None;
303
304 type FileContent = Self;
305
306 fn load(sources: SettingsSources<Self::FileContent>, _: &mut App) -> anyhow::Result<Self> {
307 sources.json_merge()
308 }
309}
310
311pub enum SettingsObserverMode {
312 Local(Arc<dyn Fs>),
313 Remote,
314}
315
316#[derive(Clone, Debug, PartialEq)]
317pub enum SettingsObserverEvent {
318 LocalSettingsUpdated(Result<(), InvalidSettingsError>),
319}
320
321impl EventEmitter<SettingsObserverEvent> for SettingsObserver {}
322
323pub struct SettingsObserver {
324 mode: SettingsObserverMode,
325 downstream_client: Option<AnyProtoClient>,
326 worktree_store: Entity<WorktreeStore>,
327 project_id: u64,
328 task_store: Entity<TaskStore>,
329}
330
331/// SettingsObserver observers changes to .zed/{settings, task}.json files in local worktrees
332/// (or the equivalent protobuf messages from upstream) and updates local settings
333/// and sends notifications downstream.
334/// In ssh mode it also monitors ~/.config/zed/{settings, task}.json and sends the content
335/// upstream.
336impl SettingsObserver {
337 pub fn init(client: &AnyProtoClient) {
338 client.add_entity_message_handler(Self::handle_update_worktree_settings);
339 }
340
341 pub fn new_local(
342 fs: Arc<dyn Fs>,
343 worktree_store: Entity<WorktreeStore>,
344 task_store: Entity<TaskStore>,
345 cx: &mut Context<Self>,
346 ) -> Self {
347 cx.subscribe(&worktree_store, Self::on_worktree_store_event)
348 .detach();
349
350 Self {
351 worktree_store,
352 task_store,
353 mode: SettingsObserverMode::Local(fs),
354 downstream_client: None,
355 project_id: 0,
356 }
357 }
358
359 pub fn new_remote(
360 worktree_store: Entity<WorktreeStore>,
361 task_store: Entity<TaskStore>,
362 _: &mut Context<Self>,
363 ) -> Self {
364 Self {
365 worktree_store,
366 task_store,
367 mode: SettingsObserverMode::Remote,
368 downstream_client: None,
369 project_id: 0,
370 }
371 }
372
373 pub fn shared(
374 &mut self,
375 project_id: u64,
376 downstream_client: AnyProtoClient,
377 cx: &mut Context<Self>,
378 ) {
379 self.project_id = project_id;
380 self.downstream_client = Some(downstream_client.clone());
381
382 let store = cx.global::<SettingsStore>();
383 for worktree in self.worktree_store.read(cx).worktrees() {
384 let worktree_id = worktree.read(cx).id().to_proto();
385 for (path, content) in store.local_settings(worktree.read(cx).id()) {
386 downstream_client
387 .send(proto::UpdateWorktreeSettings {
388 project_id,
389 worktree_id,
390 path: path.to_proto(),
391 content: Some(content),
392 kind: Some(
393 local_settings_kind_to_proto(LocalSettingsKind::Settings).into(),
394 ),
395 })
396 .log_err();
397 }
398 for (path, content, _) in store.local_editorconfig_settings(worktree.read(cx).id()) {
399 downstream_client
400 .send(proto::UpdateWorktreeSettings {
401 project_id,
402 worktree_id,
403 path: path.to_proto(),
404 content: Some(content),
405 kind: Some(
406 local_settings_kind_to_proto(LocalSettingsKind::Editorconfig).into(),
407 ),
408 })
409 .log_err();
410 }
411 }
412 }
413
414 pub fn unshared(&mut self, _: &mut Context<Self>) {
415 self.downstream_client = None;
416 }
417
418 async fn handle_update_worktree_settings(
419 this: Entity<Self>,
420 envelope: TypedEnvelope<proto::UpdateWorktreeSettings>,
421 mut cx: AsyncApp,
422 ) -> anyhow::Result<()> {
423 let kind = match envelope.payload.kind {
424 Some(kind) => proto::LocalSettingsKind::from_i32(kind)
425 .with_context(|| format!("unknown kind {kind}"))?,
426 None => proto::LocalSettingsKind::Settings,
427 };
428 this.update(&mut cx, |this, cx| {
429 let worktree_id = WorktreeId::from_proto(envelope.payload.worktree_id);
430 let Some(worktree) = this
431 .worktree_store
432 .read(cx)
433 .worktree_for_id(worktree_id, cx)
434 else {
435 return;
436 };
437
438 this.update_settings(
439 worktree,
440 [(
441 Arc::<Path>::from_proto(envelope.payload.path.clone()),
442 local_settings_kind_from_proto(kind),
443 envelope.payload.content,
444 )],
445 cx,
446 );
447 })?;
448 Ok(())
449 }
450
451 fn on_worktree_store_event(
452 &mut self,
453 _: Entity<WorktreeStore>,
454 event: &WorktreeStoreEvent,
455 cx: &mut Context<Self>,
456 ) {
457 if let WorktreeStoreEvent::WorktreeAdded(worktree) = event {
458 cx.subscribe(worktree, |this, worktree, event, cx| {
459 if let worktree::Event::UpdatedEntries(changes) = event {
460 this.update_local_worktree_settings(&worktree, changes, cx)
461 }
462 })
463 .detach()
464 }
465 }
466
467 fn update_local_worktree_settings(
468 &mut self,
469 worktree: &Entity<Worktree>,
470 changes: &UpdatedEntriesSet,
471 cx: &mut Context<Self>,
472 ) {
473 let SettingsObserverMode::Local(fs) = &self.mode else {
474 return;
475 };
476
477 let mut settings_contents = Vec::new();
478 for (path, _, change) in changes.iter() {
479 let (settings_dir, kind) = if path.ends_with(local_settings_file_relative_path()) {
480 let settings_dir = Arc::<Path>::from(
481 path.ancestors()
482 .nth(local_settings_file_relative_path().components().count())
483 .unwrap(),
484 );
485 (settings_dir, LocalSettingsKind::Settings)
486 } else if path.ends_with(local_tasks_file_relative_path()) {
487 let settings_dir = Arc::<Path>::from(
488 path.ancestors()
489 .nth(
490 local_tasks_file_relative_path()
491 .components()
492 .count()
493 .saturating_sub(1),
494 )
495 .unwrap(),
496 );
497 (settings_dir, LocalSettingsKind::Tasks(TaskKind::Script))
498 } else if path.ends_with(local_vscode_tasks_file_relative_path()) {
499 let settings_dir = Arc::<Path>::from(
500 path.ancestors()
501 .nth(
502 local_vscode_tasks_file_relative_path()
503 .components()
504 .count()
505 .saturating_sub(1),
506 )
507 .unwrap(),
508 );
509 (settings_dir, LocalSettingsKind::Tasks(TaskKind::Script))
510 } else if path.ends_with(local_debug_file_relative_path()) {
511 let settings_dir = Arc::<Path>::from(
512 path.ancestors()
513 .nth(
514 local_debug_file_relative_path()
515 .components()
516 .count()
517 .saturating_sub(1),
518 )
519 .unwrap(),
520 );
521 (settings_dir, LocalSettingsKind::Tasks(TaskKind::Debug))
522 } else if path.ends_with(EDITORCONFIG_NAME) {
523 let Some(settings_dir) = path.parent().map(Arc::from) else {
524 continue;
525 };
526 (settings_dir, LocalSettingsKind::Editorconfig)
527 } else {
528 continue;
529 };
530
531 let removed = change == &PathChange::Removed;
532 let fs = fs.clone();
533 let abs_path = match worktree.read(cx).absolutize(path) {
534 Ok(abs_path) => abs_path,
535 Err(e) => {
536 log::warn!("Cannot absolutize {path:?} received as {change:?} FS change: {e}");
537 continue;
538 }
539 };
540 settings_contents.push(async move {
541 (
542 settings_dir,
543 kind,
544 if removed {
545 None
546 } else {
547 Some(
548 async move {
549 let content = fs.load(&abs_path).await?;
550 if abs_path.ends_with(local_vscode_tasks_file_relative_path()) {
551 let vscode_tasks =
552 parse_json_with_comments::<VsCodeTaskFile>(&content)
553 .with_context(|| {
554 format!("parsing VSCode tasks, file {abs_path:?}")
555 })?;
556 let zed_tasks = TaskTemplates::try_from(vscode_tasks)
557 .with_context(|| {
558 format!(
559 "converting VSCode tasks into Zed ones, file {abs_path:?}"
560 )
561 })?;
562 serde_json::to_string(&zed_tasks).with_context(|| {
563 format!(
564 "serializing Zed tasks into JSON, file {abs_path:?}"
565 )
566 })
567 } else {
568 Ok(content)
569 }
570 }
571 .await,
572 )
573 },
574 )
575 });
576 }
577
578 if settings_contents.is_empty() {
579 return;
580 }
581
582 let worktree = worktree.clone();
583 cx.spawn(move |this, cx| async move {
584 let settings_contents: Vec<(Arc<Path>, _, _)> =
585 futures::future::join_all(settings_contents).await;
586 cx.update(|cx| {
587 this.update(cx, |this, cx| {
588 this.update_settings(
589 worktree,
590 settings_contents.into_iter().map(|(path, kind, content)| {
591 (path, kind, content.and_then(|c| c.log_err()))
592 }),
593 cx,
594 )
595 })
596 })
597 })
598 .detach();
599 }
600
601 fn update_settings(
602 &mut self,
603 worktree: Entity<Worktree>,
604 settings_contents: impl IntoIterator<Item = (Arc<Path>, LocalSettingsKind, Option<String>)>,
605 cx: &mut Context<Self>,
606 ) {
607 let worktree_id = worktree.read(cx).id();
608 let remote_worktree_id = worktree.read(cx).id();
609 let task_store = self.task_store.clone();
610
611 for (directory, kind, file_content) in settings_contents {
612 match kind {
613 LocalSettingsKind::Settings | LocalSettingsKind::Editorconfig => cx
614 .update_global::<SettingsStore, _>(|store, cx| {
615 let result = store.set_local_settings(
616 worktree_id,
617 directory.clone(),
618 kind,
619 file_content.as_deref(),
620 cx,
621 );
622
623 match result {
624 Err(InvalidSettingsError::LocalSettings { path, message }) => {
625 log::error!(
626 "Failed to set local settings in {:?}: {:?}",
627 path,
628 message
629 );
630 cx.emit(SettingsObserverEvent::LocalSettingsUpdated(Err(
631 InvalidSettingsError::LocalSettings { path, message },
632 )));
633 }
634 Err(e) => {
635 log::error!("Failed to set local settings: {e}");
636 }
637 Ok(_) => {
638 cx.emit(SettingsObserverEvent::LocalSettingsUpdated(Ok(())));
639 }
640 }
641 }),
642 LocalSettingsKind::Tasks(task_kind) => task_store.update(cx, |task_store, cx| {
643 task_store
644 .update_user_tasks(
645 Some(SettingsLocation {
646 worktree_id,
647 path: directory.as_ref(),
648 }),
649 file_content.as_deref(),
650 task_kind,
651 cx,
652 )
653 .log_err();
654 }),
655 };
656
657 if let Some(downstream_client) = &self.downstream_client {
658 downstream_client
659 .send(proto::UpdateWorktreeSettings {
660 project_id: self.project_id,
661 worktree_id: remote_worktree_id.to_proto(),
662 path: directory.to_proto(),
663 content: file_content,
664 kind: Some(local_settings_kind_to_proto(kind).into()),
665 })
666 .log_err();
667 }
668 }
669 }
670}
671
672pub fn local_settings_kind_from_proto(kind: proto::LocalSettingsKind) -> LocalSettingsKind {
673 match kind {
674 proto::LocalSettingsKind::Settings => LocalSettingsKind::Settings,
675 proto::LocalSettingsKind::Tasks => LocalSettingsKind::Tasks(TaskKind::Script),
676 proto::LocalSettingsKind::Editorconfig => LocalSettingsKind::Editorconfig,
677 }
678}
679
680pub fn local_settings_kind_to_proto(kind: LocalSettingsKind) -> proto::LocalSettingsKind {
681 match kind {
682 LocalSettingsKind::Settings => proto::LocalSettingsKind::Settings,
683 LocalSettingsKind::Tasks(_) => proto::LocalSettingsKind::Tasks,
684 LocalSettingsKind::Editorconfig => proto::LocalSettingsKind::Editorconfig,
685 }
686}