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