project_settings.rs

  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}