1use anyhow::Context;
2use collections::HashMap;
3use fs::Fs;
4use gpui::{AppContext, AsyncAppContext, BorrowAppContext, EventEmitter, Model, ModelContext};
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(
212 sources: SettingsSources<Self::FileContent>,
213 _: &mut AppContext,
214 ) -> anyhow::Result<Self> {
215 sources.json_merge()
216 }
217}
218
219pub enum SettingsObserverMode {
220 Local(Arc<dyn Fs>),
221 Remote,
222}
223
224#[derive(Clone, Debug, PartialEq)]
225pub enum SettingsObserverEvent {
226 LocalSettingsUpdated(Result<(), InvalidSettingsError>),
227}
228
229impl EventEmitter<SettingsObserverEvent> for SettingsObserver {}
230
231pub struct SettingsObserver {
232 mode: SettingsObserverMode,
233 downstream_client: Option<AnyProtoClient>,
234 worktree_store: Model<WorktreeStore>,
235 project_id: u64,
236 task_store: Model<TaskStore>,
237}
238
239/// SettingsObserver observers changes to .zed/{settings, task}.json files in local worktrees
240/// (or the equivalent protobuf messages from upstream) and updates local settings
241/// and sends notifications downstream.
242/// In ssh mode it also monitors ~/.config/zed/{settings, task}.json and sends the content
243/// upstream.
244impl SettingsObserver {
245 pub fn init(client: &AnyProtoClient) {
246 client.add_model_message_handler(Self::handle_update_worktree_settings);
247 }
248
249 pub fn new_local(
250 fs: Arc<dyn Fs>,
251 worktree_store: Model<WorktreeStore>,
252 task_store: Model<TaskStore>,
253 cx: &mut ModelContext<Self>,
254 ) -> Self {
255 cx.subscribe(&worktree_store, Self::on_worktree_store_event)
256 .detach();
257
258 Self {
259 worktree_store,
260 task_store,
261 mode: SettingsObserverMode::Local(fs),
262 downstream_client: None,
263 project_id: 0,
264 }
265 }
266
267 pub fn new_remote(
268 worktree_store: Model<WorktreeStore>,
269 task_store: Model<TaskStore>,
270 _: &mut ModelContext<Self>,
271 ) -> Self {
272 Self {
273 worktree_store,
274 task_store,
275 mode: SettingsObserverMode::Remote,
276 downstream_client: None,
277 project_id: 0,
278 }
279 }
280
281 pub fn shared(
282 &mut self,
283 project_id: u64,
284 downstream_client: AnyProtoClient,
285 cx: &mut ModelContext<Self>,
286 ) {
287 self.project_id = project_id;
288 self.downstream_client = Some(downstream_client.clone());
289
290 let store = cx.global::<SettingsStore>();
291 for worktree in self.worktree_store.read(cx).worktrees() {
292 let worktree_id = worktree.read(cx).id().to_proto();
293 for (path, content) in store.local_settings(worktree.read(cx).id()) {
294 downstream_client
295 .send(proto::UpdateWorktreeSettings {
296 project_id,
297 worktree_id,
298 path: path.to_string_lossy().into(),
299 content: Some(content),
300 kind: Some(
301 local_settings_kind_to_proto(LocalSettingsKind::Settings).into(),
302 ),
303 })
304 .log_err();
305 }
306 for (path, content, _) in store.local_editorconfig_settings(worktree.read(cx).id()) {
307 downstream_client
308 .send(proto::UpdateWorktreeSettings {
309 project_id,
310 worktree_id,
311 path: path.to_string_lossy().into(),
312 content: Some(content),
313 kind: Some(
314 local_settings_kind_to_proto(LocalSettingsKind::Editorconfig).into(),
315 ),
316 })
317 .log_err();
318 }
319 }
320 }
321
322 pub fn unshared(&mut self, _: &mut ModelContext<Self>) {
323 self.downstream_client = None;
324 }
325
326 async fn handle_update_worktree_settings(
327 this: Model<Self>,
328 envelope: TypedEnvelope<proto::UpdateWorktreeSettings>,
329 mut cx: AsyncAppContext,
330 ) -> anyhow::Result<()> {
331 let kind = match envelope.payload.kind {
332 Some(kind) => proto::LocalSettingsKind::from_i32(kind)
333 .with_context(|| format!("unknown kind {kind}"))?,
334 None => proto::LocalSettingsKind::Settings,
335 };
336 this.update(&mut cx, |this, cx| {
337 let worktree_id = WorktreeId::from_proto(envelope.payload.worktree_id);
338 let Some(worktree) = this
339 .worktree_store
340 .read(cx)
341 .worktree_for_id(worktree_id, cx)
342 else {
343 return;
344 };
345
346 this.update_settings(
347 worktree,
348 [(
349 PathBuf::from(&envelope.payload.path).into(),
350 local_settings_kind_from_proto(kind),
351 envelope.payload.content,
352 )],
353 cx,
354 );
355 })?;
356 Ok(())
357 }
358
359 fn on_worktree_store_event(
360 &mut self,
361 _: Model<WorktreeStore>,
362 event: &WorktreeStoreEvent,
363 cx: &mut ModelContext<Self>,
364 ) {
365 if let WorktreeStoreEvent::WorktreeAdded(worktree) = event {
366 cx.subscribe(worktree, |this, worktree, event, cx| {
367 if let worktree::Event::UpdatedEntries(changes) = event {
368 this.update_local_worktree_settings(&worktree, changes, cx)
369 }
370 })
371 .detach()
372 }
373 }
374
375 fn update_local_worktree_settings(
376 &mut self,
377 worktree: &Model<Worktree>,
378 changes: &UpdatedEntriesSet,
379 cx: &mut ModelContext<Self>,
380 ) {
381 let SettingsObserverMode::Local(fs) = &self.mode else {
382 return;
383 };
384
385 let mut settings_contents = Vec::new();
386 for (path, _, change) in changes.iter() {
387 let (settings_dir, kind) = if path.ends_with(local_settings_file_relative_path()) {
388 let settings_dir = Arc::<Path>::from(
389 path.ancestors()
390 .nth(local_settings_file_relative_path().components().count())
391 .unwrap(),
392 );
393 (settings_dir, LocalSettingsKind::Settings)
394 } else if path.ends_with(local_tasks_file_relative_path()) {
395 let settings_dir = Arc::<Path>::from(
396 path.ancestors()
397 .nth(
398 local_tasks_file_relative_path()
399 .components()
400 .count()
401 .saturating_sub(1),
402 )
403 .unwrap(),
404 );
405 (settings_dir, LocalSettingsKind::Tasks)
406 } else if path.ends_with(local_vscode_tasks_file_relative_path()) {
407 let settings_dir = Arc::<Path>::from(
408 path.ancestors()
409 .nth(
410 local_vscode_tasks_file_relative_path()
411 .components()
412 .count()
413 .saturating_sub(1),
414 )
415 .unwrap(),
416 );
417 (settings_dir, LocalSettingsKind::Tasks)
418 } else if path.ends_with(EDITORCONFIG_NAME) {
419 let Some(settings_dir) = path.parent().map(Arc::from) else {
420 continue;
421 };
422 (settings_dir, LocalSettingsKind::Editorconfig)
423 } else {
424 continue;
425 };
426
427 let removed = change == &PathChange::Removed;
428 let fs = fs.clone();
429 let abs_path = match worktree.read(cx).absolutize(path) {
430 Ok(abs_path) => abs_path,
431 Err(e) => {
432 log::warn!("Cannot absolutize {path:?} received as {change:?} FS change: {e}");
433 continue;
434 }
435 };
436 settings_contents.push(async move {
437 (
438 settings_dir,
439 kind,
440 if removed {
441 None
442 } else {
443 Some(
444 async move {
445 let content = fs.load(&abs_path).await?;
446 if abs_path.ends_with(local_vscode_tasks_file_relative_path()) {
447 let vscode_tasks =
448 parse_json_with_comments::<VsCodeTaskFile>(&content)
449 .with_context(|| {
450 format!("parsing VSCode tasks, file {abs_path:?}")
451 })?;
452 let zed_tasks = TaskTemplates::try_from(vscode_tasks)
453 .with_context(|| {
454 format!(
455 "converting VSCode tasks into Zed ones, file {abs_path:?}"
456 )
457 })?;
458 serde_json::to_string(&zed_tasks).with_context(|| {
459 format!(
460 "serializing Zed tasks into JSON, file {abs_path:?}"
461 )
462 })
463 } else {
464 Ok(content)
465 }
466 }
467 .await,
468 )
469 },
470 )
471 });
472 }
473
474 if settings_contents.is_empty() {
475 return;
476 }
477
478 let worktree = worktree.clone();
479 cx.spawn(move |this, cx| async move {
480 let settings_contents: Vec<(Arc<Path>, _, _)> =
481 futures::future::join_all(settings_contents).await;
482 cx.update(|cx| {
483 this.update(cx, |this, cx| {
484 this.update_settings(
485 worktree,
486 settings_contents.into_iter().map(|(path, kind, content)| {
487 (path, kind, content.and_then(|c| c.log_err()))
488 }),
489 cx,
490 )
491 })
492 })
493 })
494 .detach();
495 }
496
497 fn update_settings(
498 &mut self,
499 worktree: Model<Worktree>,
500 settings_contents: impl IntoIterator<Item = (Arc<Path>, LocalSettingsKind, Option<String>)>,
501 cx: &mut ModelContext<Self>,
502 ) {
503 let worktree_id = worktree.read(cx).id();
504 let remote_worktree_id = worktree.read(cx).id();
505 let task_store = self.task_store.clone();
506
507 for (directory, kind, file_content) in settings_contents {
508 match kind {
509 LocalSettingsKind::Settings | LocalSettingsKind::Editorconfig => cx
510 .update_global::<SettingsStore, _>(|store, cx| {
511 let result = store.set_local_settings(
512 worktree_id,
513 directory.clone(),
514 kind,
515 file_content.as_deref(),
516 cx,
517 );
518
519 match result {
520 Err(InvalidSettingsError::LocalSettings { path, message }) => {
521 log::error!(
522 "Failed to set local settings in {:?}: {:?}",
523 path,
524 message
525 );
526 cx.emit(SettingsObserverEvent::LocalSettingsUpdated(Err(
527 InvalidSettingsError::LocalSettings { path, message },
528 )));
529 }
530 Err(e) => {
531 log::error!("Failed to set local settings: {e}");
532 }
533 Ok(_) => {
534 cx.emit(SettingsObserverEvent::LocalSettingsUpdated(Ok(())));
535 }
536 }
537 }),
538 LocalSettingsKind::Tasks => task_store.update(cx, |task_store, cx| {
539 task_store
540 .update_user_tasks(
541 Some(SettingsLocation {
542 worktree_id,
543 path: directory.as_ref(),
544 }),
545 file_content.as_deref(),
546 cx,
547 )
548 .log_err();
549 }),
550 };
551
552 if let Some(downstream_client) = &self.downstream_client {
553 downstream_client
554 .send(proto::UpdateWorktreeSettings {
555 project_id: self.project_id,
556 worktree_id: remote_worktree_id.to_proto(),
557 path: directory.to_string_lossy().into_owned(),
558 content: file_content,
559 kind: Some(local_settings_kind_to_proto(kind).into()),
560 })
561 .log_err();
562 }
563 }
564 }
565}
566
567pub fn local_settings_kind_from_proto(kind: proto::LocalSettingsKind) -> LocalSettingsKind {
568 match kind {
569 proto::LocalSettingsKind::Settings => LocalSettingsKind::Settings,
570 proto::LocalSettingsKind::Tasks => LocalSettingsKind::Tasks,
571 proto::LocalSettingsKind::Editorconfig => LocalSettingsKind::Editorconfig,
572 }
573}
574
575pub fn local_settings_kind_to_proto(kind: LocalSettingsKind) -> proto::LocalSettingsKind {
576 match kind {
577 LocalSettingsKind::Settings => proto::LocalSettingsKind::Settings,
578 LocalSettingsKind::Tasks => proto::LocalSettingsKind::Tasks,
579 LocalSettingsKind::Editorconfig => proto::LocalSettingsKind::Editorconfig,
580 }
581}