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