1use collections::HashMap;
2use fs::Fs;
3use gpui::{AppContext, AsyncAppContext, BorrowAppContext, Model, ModelContext};
4use paths::local_settings_file_relative_path;
5use rpc::{
6 proto::{self, AnyProtoClient},
7 TypedEnvelope,
8};
9use schemars::JsonSchema;
10use serde::{Deserialize, Serialize};
11use settings::{Settings, SettingsSources, SettingsStore};
12use std::{
13 path::{Path, PathBuf},
14 sync::Arc,
15 time::Duration,
16};
17use util::ResultExt;
18use worktree::{PathChange, UpdatedEntriesSet, Worktree, WorktreeId};
19
20use crate::worktree_store::{WorktreeStore, WorktreeStoreEvent};
21
22#[derive(Clone, Default, Serialize, Deserialize, JsonSchema)]
23pub struct ProjectSettings {
24 /// Configuration for language servers.
25 ///
26 /// The following settings can be overridden for specific language servers:
27 /// - initialization_options
28 ///
29 /// To override settings for a language, add an entry for that language server's
30 /// name to the lsp value.
31 /// Default: null
32 #[serde(default)]
33 pub lsp: HashMap<Arc<str>, LspSettings>,
34
35 /// Configuration for Git-related features
36 #[serde(default)]
37 pub git: GitSettings,
38
39 /// Configuration for how direnv configuration should be loaded
40 #[serde(default)]
41 pub load_direnv: DirenvSettings,
42
43 /// Configuration for session-related features
44 #[serde(default)]
45 pub session: SessionSettings,
46}
47
48#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema)]
49#[serde(rename_all = "snake_case")]
50pub enum DirenvSettings {
51 /// Load direnv configuration through a shell hook
52 #[default]
53 ShellHook,
54 /// Load direnv configuration directly using `direnv export json`
55 ///
56 /// Warning: This option is experimental and might cause some inconsistent behavior compared to using the shell hook.
57 /// If it does, please report it to GitHub
58 Direct,
59}
60
61#[derive(Copy, Clone, Debug, Default, Serialize, Deserialize, JsonSchema)]
62pub struct GitSettings {
63 /// Whether or not to show the git gutter.
64 ///
65 /// Default: tracked_files
66 pub git_gutter: Option<GitGutterSetting>,
67 pub gutter_debounce: Option<u64>,
68 /// Whether or not to show git blame data inline in
69 /// the currently focused line.
70 ///
71 /// Default: on
72 pub inline_blame: Option<InlineBlameSettings>,
73}
74
75impl GitSettings {
76 pub fn inline_blame_enabled(&self) -> bool {
77 #[allow(unknown_lints, clippy::manual_unwrap_or_default)]
78 match self.inline_blame {
79 Some(InlineBlameSettings { enabled, .. }) => enabled,
80 _ => false,
81 }
82 }
83
84 pub fn inline_blame_delay(&self) -> Option<Duration> {
85 match self.inline_blame {
86 Some(InlineBlameSettings {
87 delay_ms: Some(delay_ms),
88 ..
89 }) if delay_ms > 0 => Some(Duration::from_millis(delay_ms)),
90 _ => None,
91 }
92 }
93}
94
95#[derive(Clone, Copy, Debug, Default, Serialize, Deserialize, JsonSchema)]
96#[serde(rename_all = "snake_case")]
97pub enum GitGutterSetting {
98 /// Show git gutter in tracked files.
99 #[default]
100 TrackedFiles,
101 /// Hide git gutter
102 Hide,
103}
104
105#[derive(Clone, Copy, Debug, Default, Serialize, Deserialize, JsonSchema)]
106#[serde(rename_all = "snake_case")]
107pub struct InlineBlameSettings {
108 /// Whether or not to show git blame data inline in
109 /// the currently focused line.
110 ///
111 /// Default: true
112 #[serde(default = "true_value")]
113 pub enabled: bool,
114 /// Whether to only show the inline blame information
115 /// after a delay once the cursor stops moving.
116 ///
117 /// Default: 0
118 pub delay_ms: Option<u64>,
119 /// The minimum column number to show the inline blame information at
120 ///
121 /// Default: 0
122 pub min_column: Option<u32>,
123}
124
125const fn true_value() -> bool {
126 true
127}
128
129#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
130pub struct BinarySettings {
131 pub path: Option<String>,
132 pub arguments: Option<Vec<String>>,
133 pub path_lookup: Option<bool>,
134}
135
136#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
137#[serde(rename_all = "snake_case")]
138pub struct LspSettings {
139 pub binary: Option<BinarySettings>,
140 pub initialization_options: Option<serde_json::Value>,
141 pub settings: Option<serde_json::Value>,
142}
143
144#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema)]
145pub struct SessionSettings {
146 /// Whether or not to restore unsaved buffers on restart.
147 ///
148 /// If this is true, user won't be prompted whether to save/discard
149 /// dirty files when closing the application.
150 ///
151 /// Default: true
152 pub restore_unsaved_buffers: bool,
153}
154
155impl Default for SessionSettings {
156 fn default() -> Self {
157 Self {
158 restore_unsaved_buffers: true,
159 }
160 }
161}
162
163impl Settings for ProjectSettings {
164 const KEY: Option<&'static str> = None;
165
166 type FileContent = Self;
167
168 fn load(
169 sources: SettingsSources<Self::FileContent>,
170 _: &mut AppContext,
171 ) -> anyhow::Result<Self> {
172 sources.json_merge()
173 }
174}
175
176pub enum SettingsObserverMode {
177 Local(Arc<dyn Fs>),
178 Ssh(AnyProtoClient),
179 Remote,
180}
181
182pub struct SettingsObserver {
183 mode: SettingsObserverMode,
184 downstream_client: Option<AnyProtoClient>,
185 worktree_store: Model<WorktreeStore>,
186 project_id: u64,
187}
188
189/// SettingsObserver observers changes to .zed/settings.json files in local worktrees
190/// (or the equivalent protobuf messages from upstream) and updates local settings
191/// and sends notifications downstream.
192/// In ssh mode it also monitors ~/.config/zed/settings.json and sends the content
193/// upstream.
194impl SettingsObserver {
195 pub fn init(client: &AnyProtoClient) {
196 client.add_model_message_handler(Self::handle_update_worktree_settings);
197 client.add_model_message_handler(Self::handle_update_user_settings)
198 }
199
200 pub fn new_local(
201 fs: Arc<dyn Fs>,
202 worktree_store: Model<WorktreeStore>,
203 cx: &mut ModelContext<Self>,
204 ) -> Self {
205 cx.subscribe(&worktree_store, Self::on_worktree_store_event)
206 .detach();
207
208 Self {
209 worktree_store,
210 mode: SettingsObserverMode::Local(fs),
211 downstream_client: None,
212 project_id: 0,
213 }
214 }
215
216 pub fn new_ssh(
217 client: AnyProtoClient,
218 worktree_store: Model<WorktreeStore>,
219 cx: &mut ModelContext<Self>,
220 ) -> Self {
221 let this = Self {
222 worktree_store,
223 mode: SettingsObserverMode::Ssh(client.clone()),
224 downstream_client: None,
225 project_id: 0,
226 };
227 this.maintain_ssh_settings(client, cx);
228 this
229 }
230
231 pub fn new_remote(worktree_store: Model<WorktreeStore>, _: &mut ModelContext<Self>) -> Self {
232 Self {
233 worktree_store,
234 mode: SettingsObserverMode::Remote,
235 downstream_client: None,
236 project_id: 0,
237 }
238 }
239
240 pub fn shared(
241 &mut self,
242 project_id: u64,
243 downstream_client: AnyProtoClient,
244 cx: &mut ModelContext<Self>,
245 ) {
246 self.project_id = project_id;
247 self.downstream_client = Some(downstream_client.clone());
248
249 let store = cx.global::<SettingsStore>();
250 for worktree in self.worktree_store.read(cx).worktrees() {
251 let worktree_id = worktree.read(cx).id().to_proto();
252 for (path, content) in store.local_settings(worktree.read(cx).id()) {
253 downstream_client
254 .send(proto::UpdateWorktreeSettings {
255 project_id,
256 worktree_id,
257 path: path.to_string_lossy().into(),
258 content: Some(content),
259 })
260 .log_err();
261 }
262 }
263 }
264
265 pub fn unshared(&mut self, _: &mut ModelContext<Self>) {
266 self.downstream_client = None;
267 }
268
269 async fn handle_update_worktree_settings(
270 this: Model<Self>,
271 envelope: TypedEnvelope<proto::UpdateWorktreeSettings>,
272 mut cx: AsyncAppContext,
273 ) -> anyhow::Result<()> {
274 this.update(&mut cx, |this, cx| {
275 let worktree_id = WorktreeId::from_proto(envelope.payload.worktree_id);
276 let Some(worktree) = this
277 .worktree_store
278 .read(cx)
279 .worktree_for_id(worktree_id, cx)
280 else {
281 return;
282 };
283 this.update_settings(
284 worktree,
285 [(
286 PathBuf::from(&envelope.payload.path).into(),
287 envelope.payload.content,
288 )],
289 cx,
290 );
291 })?;
292 Ok(())
293 }
294
295 pub async fn handle_update_user_settings(
296 _: Model<Self>,
297 envelope: TypedEnvelope<proto::UpdateUserSettings>,
298 mut cx: AsyncAppContext,
299 ) -> anyhow::Result<()> {
300 cx.update_global(move |settings_store: &mut SettingsStore, cx| {
301 settings_store.set_user_settings(&envelope.payload.content, cx)
302 })??;
303
304 Ok(())
305 }
306
307 pub fn maintain_ssh_settings(&self, ssh: AnyProtoClient, cx: &mut ModelContext<Self>) {
308 let mut settings = cx.global::<SettingsStore>().raw_user_settings().clone();
309 if let Some(content) = serde_json::to_string(&settings).log_err() {
310 ssh.send(proto::UpdateUserSettings {
311 project_id: 0,
312 content,
313 })
314 .log_err();
315 }
316
317 cx.observe_global::<SettingsStore>(move |_, cx| {
318 let new_settings = cx.global::<SettingsStore>().raw_user_settings();
319 if &settings != new_settings {
320 settings = new_settings.clone()
321 }
322 if let Some(content) = serde_json::to_string(&settings).log_err() {
323 ssh.send(proto::UpdateUserSettings {
324 project_id: 0,
325 content,
326 })
327 .log_err();
328 }
329 })
330 .detach();
331 }
332
333 fn on_worktree_store_event(
334 &mut self,
335 _: Model<WorktreeStore>,
336 event: &WorktreeStoreEvent,
337 cx: &mut ModelContext<Self>,
338 ) {
339 if let WorktreeStoreEvent::WorktreeAdded(worktree) = event {
340 cx.subscribe(worktree, |this, worktree, event, cx| {
341 if let worktree::Event::UpdatedEntries(changes) = event {
342 this.update_local_worktree_settings(&worktree, changes, cx)
343 }
344 })
345 .detach()
346 }
347 }
348
349 fn update_local_worktree_settings(
350 &mut self,
351 worktree: &Model<Worktree>,
352 changes: &UpdatedEntriesSet,
353 cx: &mut ModelContext<Self>,
354 ) {
355 let SettingsObserverMode::Local(fs) = &self.mode else {
356 return;
357 };
358
359 let mut settings_contents = Vec::new();
360 for (path, _, change) in changes.iter() {
361 let removed = change == &PathChange::Removed;
362 let abs_path = match worktree.read(cx).absolutize(path) {
363 Ok(abs_path) => abs_path,
364 Err(e) => {
365 log::warn!("Cannot absolutize {path:?} received as {change:?} FS change: {e}");
366 continue;
367 }
368 };
369
370 if path.ends_with(local_settings_file_relative_path()) {
371 let settings_dir = Arc::from(
372 path.ancestors()
373 .nth(local_settings_file_relative_path().components().count())
374 .unwrap(),
375 );
376 let fs = fs.clone();
377 settings_contents.push(async move {
378 (
379 settings_dir,
380 if removed {
381 None
382 } else {
383 Some(async move { fs.load(&abs_path).await }.await)
384 },
385 )
386 });
387 }
388 }
389
390 if settings_contents.is_empty() {
391 return;
392 }
393
394 let worktree = worktree.clone();
395 cx.spawn(move |this, cx| async move {
396 let settings_contents: Vec<(Arc<Path>, _)> =
397 futures::future::join_all(settings_contents).await;
398 cx.update(|cx| {
399 this.update(cx, |this, cx| {
400 this.update_settings(
401 worktree,
402 settings_contents
403 .into_iter()
404 .map(|(path, content)| (path, content.and_then(|c| c.log_err()))),
405 cx,
406 )
407 })
408 })
409 })
410 .detach();
411 }
412
413 fn update_settings(
414 &mut self,
415 worktree: Model<Worktree>,
416 settings_contents: impl IntoIterator<Item = (Arc<Path>, Option<String>)>,
417 cx: &mut ModelContext<Self>,
418 ) {
419 let worktree_id = worktree.read(cx).id();
420 let remote_worktree_id = worktree.read(cx).id();
421 cx.update_global::<SettingsStore, _>(|store, cx| {
422 for (directory, file_content) in settings_contents {
423 store
424 .set_local_settings(worktree_id, directory.clone(), file_content.as_deref(), cx)
425 .log_err();
426 if let Some(downstream_client) = &self.downstream_client {
427 downstream_client
428 .send(proto::UpdateWorktreeSettings {
429 project_id: self.project_id,
430 worktree_id: remote_worktree_id.to_proto(),
431 path: directory.to_string_lossy().into_owned(),
432 content: file_content,
433 })
434 .log_err();
435 }
436 }
437 })
438 }
439}