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