1use crate::*;
2use anyhow::{Context as _, Result, anyhow};
3use collections::HashMap;
4use fs::Fs;
5use paths::{cursor_settings_file_paths, vscode_settings_file_paths};
6use serde::Deserialize;
7use serde_json::{Map, Value};
8use std::{
9 num::{NonZeroU32, NonZeroUsize},
10 path::{Path, PathBuf},
11 sync::Arc,
12};
13
14#[derive(Clone, Copy, PartialEq, Eq, Debug)]
15pub enum VsCodeSettingsSource {
16 VsCode,
17 Cursor,
18}
19
20impl std::fmt::Display for VsCodeSettingsSource {
21 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
22 match self {
23 VsCodeSettingsSource::VsCode => write!(f, "VS Code"),
24 VsCodeSettingsSource::Cursor => write!(f, "Cursor"),
25 }
26 }
27}
28
29pub struct VsCodeSettings {
30 pub source: VsCodeSettingsSource,
31 pub path: Arc<Path>,
32 content: Map<String, Value>,
33}
34
35impl VsCodeSettings {
36 #[cfg(any(test, feature = "test-support"))]
37 pub fn from_str(content: &str, source: VsCodeSettingsSource) -> Result<Self> {
38 Ok(Self {
39 source,
40 path: Path::new("/example-path/Code/User/settings.json").into(),
41 content: serde_json_lenient::from_str(content)?,
42 })
43 }
44
45 pub async fn load_user_settings(source: VsCodeSettingsSource, fs: Arc<dyn Fs>) -> Result<Self> {
46 let candidate_paths = match source {
47 VsCodeSettingsSource::VsCode => vscode_settings_file_paths(),
48 VsCodeSettingsSource::Cursor => cursor_settings_file_paths(),
49 };
50 let mut path = None;
51 for candidate_path in candidate_paths.iter() {
52 if fs.is_file(candidate_path).await {
53 path = Some(candidate_path.clone());
54 }
55 }
56 let Some(path) = path else {
57 return Err(anyhow!(
58 "No settings file found, expected to find it in one of the following paths:\n{}",
59 candidate_paths
60 .into_iter()
61 .map(|path| path.to_string_lossy().into_owned())
62 .collect::<Vec<_>>()
63 .join("\n")
64 ));
65 };
66 let content = fs.load(&path).await.with_context(|| {
67 format!(
68 "Error loading {} settings file from {}",
69 source,
70 path.display()
71 )
72 })?;
73 let content = serde_json_lenient::from_str(&content).with_context(|| {
74 format!(
75 "Error parsing {} settings file from {}",
76 source,
77 path.display()
78 )
79 })?;
80 Ok(Self {
81 source,
82 path: path.into(),
83 content,
84 })
85 }
86
87 fn read_value(&self, setting: &str) -> Option<&Value> {
88 self.content.get(setting)
89 }
90
91 fn read_str(&self, setting: &str) -> Option<&str> {
92 self.read_value(setting).and_then(|v| v.as_str())
93 }
94
95 fn read_string(&self, setting: &str) -> Option<String> {
96 self.read_value(setting)
97 .and_then(|v| v.as_str())
98 .map(|s| s.to_owned())
99 }
100
101 fn read_bool(&self, setting: &str) -> Option<bool> {
102 self.read_value(setting).and_then(|v| v.as_bool())
103 }
104
105 fn read_f32(&self, setting: &str) -> Option<f32> {
106 self.read_value(setting)
107 .and_then(|v| v.as_f64())
108 .map(|v| v as f32)
109 }
110
111 fn read_u64(&self, setting: &str) -> Option<u64> {
112 self.read_value(setting).and_then(|v| v.as_u64())
113 }
114
115 fn read_usize(&self, setting: &str) -> Option<usize> {
116 self.read_value(setting)
117 .and_then(|v| v.as_u64())
118 .and_then(|v| v.try_into().ok())
119 }
120
121 fn read_u32(&self, setting: &str) -> Option<u32> {
122 self.read_value(setting)
123 .and_then(|v| v.as_u64())
124 .and_then(|v| v.try_into().ok())
125 }
126
127 fn read_enum<T>(&self, key: &str, f: impl FnOnce(&str) -> Option<T>) -> Option<T> {
128 self.content.get(key).and_then(Value::as_str).and_then(f)
129 }
130
131 fn read_fonts(&self, key: &str) -> (Option<FontFamilyName>, Option<Vec<FontFamilyName>>) {
132 let Some(css_name) = self.content.get(key).and_then(Value::as_str) else {
133 return (None, None);
134 };
135
136 let mut name_buffer = String::new();
137 let mut quote_char: Option<char> = None;
138 let mut fonts = Vec::new();
139 let mut add_font = |buffer: &mut String| {
140 let trimmed = buffer.trim();
141 if !trimmed.is_empty() {
142 fonts.push(trimmed.to_string().into());
143 }
144
145 buffer.clear();
146 };
147
148 for ch in css_name.chars() {
149 match (ch, quote_char) {
150 ('"' | '\'', None) => {
151 quote_char = Some(ch);
152 }
153 (_, Some(q)) if ch == q => {
154 quote_char = None;
155 }
156 (',', None) => {
157 add_font(&mut name_buffer);
158 }
159 _ => {
160 name_buffer.push(ch);
161 }
162 }
163 }
164
165 add_font(&mut name_buffer);
166 if fonts.is_empty() {
167 return (None, None);
168 }
169 (Some(fonts.remove(0)), skip_default(fonts))
170 }
171
172 pub fn settings_content(&self) -> SettingsContent {
173 SettingsContent {
174 agent: self.agent_settings_content(),
175 agent_servers: None,
176 audio: None,
177 auto_update: None,
178 base_keymap: Some(BaseKeymapContent::VSCode),
179 calls: None,
180 collaboration_panel: None,
181 debugger: None,
182 diagnostics: None,
183 disable_ai: None,
184 editor: self.editor_settings_content(),
185 extension: ExtensionSettingsContent::default(),
186 file_finder: None,
187 git: self.git_settings_content(),
188 git_panel: self.git_panel_settings_content(),
189 global_lsp_settings: None,
190 helix_mode: None,
191 image_viewer: None,
192 journal: None,
193 language_models: None,
194 line_indicator_format: None,
195 log: None,
196 message_editor: None,
197 node: self.node_binary_settings(),
198 notification_panel: None,
199 outline_panel: self.outline_panel_settings_content(),
200 preview_tabs: self.preview_tabs_settings_content(),
201 project: self.project_settings_content(),
202 project_panel: self.project_panel_settings_content(),
203 proxy: self.read_string("http.proxy"),
204 remote: RemoteSettingsContent::default(),
205 repl: None,
206 server_url: None,
207 session: None,
208 status_bar: self.status_bar_settings_content(),
209 tab_bar: self.tab_bar_settings_content(),
210 tabs: self.item_settings_content(),
211 telemetry: self.telemetry_settings_content(),
212 terminal: self.terminal_settings_content(),
213 theme: Box::new(self.theme_settings_content()),
214 title_bar: None,
215 vim: None,
216 vim_mode: None,
217 workspace: self.workspace_settings_content(),
218 which_key: None,
219 modeline_lines: None,
220 }
221 }
222
223 fn agent_settings_content(&self) -> Option<AgentSettingsContent> {
224 let enabled = self.read_bool("chat.agent.enabled");
225 skip_default(AgentSettingsContent {
226 enabled: enabled,
227 button: enabled,
228 ..Default::default()
229 })
230 }
231
232 fn editor_settings_content(&self) -> EditorSettingsContent {
233 EditorSettingsContent {
234 auto_signature_help: self.read_bool("editor.parameterHints.enabled"),
235 autoscroll_on_clicks: None,
236 cursor_blink: self.read_enum("editor.cursorBlinking", |s| match s {
237 "blink" | "phase" | "expand" | "smooth" => Some(true),
238 "solid" => Some(false),
239 _ => None,
240 }),
241 cursor_shape: self.read_enum("editor.cursorStyle", |s| match s {
242 "block" => Some(CursorShape::Block),
243 "block-outline" => Some(CursorShape::Hollow),
244 "line" | "line-thin" => Some(CursorShape::Bar),
245 "underline" | "underline-thin" => Some(CursorShape::Underline),
246 _ => None,
247 }),
248 current_line_highlight: self.read_enum("editor.renderLineHighlight", |s| match s {
249 "gutter" => Some(CurrentLineHighlight::Gutter),
250 "line" => Some(CurrentLineHighlight::Line),
251 "all" => Some(CurrentLineHighlight::All),
252 _ => None,
253 }),
254 diagnostics_max_severity: None,
255 double_click_in_multibuffer: None,
256 drag_and_drop_selection: None,
257 excerpt_context_lines: None,
258 expand_excerpt_lines: None,
259 fast_scroll_sensitivity: self.read_f32("editor.fastScrollSensitivity"),
260 sticky_scroll: self.sticky_scroll_content(),
261 go_to_definition_fallback: None,
262 gutter: self.gutter_content(),
263 hide_mouse: None,
264 horizontal_scroll_margin: None,
265 hover_popover_delay: self.read_u64("editor.hover.delay").map(Into::into),
266 hover_popover_enabled: self.read_bool("editor.hover.enabled"),
267 inline_code_actions: None,
268 jupyter: None,
269 lsp_document_colors: None,
270 lsp_highlight_debounce: None,
271 middle_click_paste: None,
272 minimap: self.minimap_content(),
273 minimum_contrast_for_highlights: None,
274 multi_cursor_modifier: self.read_enum("editor.multiCursorModifier", |s| match s {
275 "ctrlCmd" => Some(MultiCursorModifier::CmdOrCtrl),
276 "alt" => Some(MultiCursorModifier::Alt),
277 _ => None,
278 }),
279 redact_private_values: None,
280 relative_line_numbers: self.read_enum("editor.lineNumbers", |s| match s {
281 "relative" => Some(RelativeLineNumbers::Enabled),
282 _ => None,
283 }),
284 rounded_selection: self.read_bool("editor.roundedSelection"),
285 scroll_beyond_last_line: None,
286 scroll_sensitivity: self.read_f32("editor.mouseWheelScrollSensitivity"),
287 scrollbar: self.scrollbar_content(),
288 search: self.search_content(),
289 search_wrap: None,
290 seed_search_query_from_cursor: self.read_enum(
291 "editor.find.seedSearchStringFromSelection",
292 |s| match s {
293 "always" => Some(SeedQuerySetting::Always),
294 "selection" => Some(SeedQuerySetting::Selection),
295 "never" => Some(SeedQuerySetting::Never),
296 _ => None,
297 },
298 ),
299 selection_highlight: self.read_bool("editor.selectionHighlight"),
300 show_signature_help_after_edits: self.read_bool("editor.parameterHints.enabled"),
301 snippet_sort_order: None,
302 toolbar: None,
303 use_smartcase_search: self.read_bool("search.smartCase"),
304 vertical_scroll_margin: self.read_f32("editor.cursorSurroundingLines"),
305 completion_menu_scrollbar: None,
306 completion_detail_alignment: None,
307 }
308 }
309
310 fn sticky_scroll_content(&self) -> Option<StickyScrollContent> {
311 skip_default(StickyScrollContent {
312 enabled: self.read_bool("editor.stickyScroll.enabled"),
313 })
314 }
315
316 fn gutter_content(&self) -> Option<GutterContent> {
317 skip_default(GutterContent {
318 line_numbers: self.read_enum("editor.lineNumbers", |s| match s {
319 "on" | "relative" => Some(true),
320 "off" => Some(false),
321 _ => None,
322 }),
323 min_line_number_digits: None,
324 runnables: None,
325 breakpoints: None,
326 folds: self.read_enum("editor.showFoldingControls", |s| match s {
327 "always" | "mouseover" => Some(true),
328 "never" => Some(false),
329 _ => None,
330 }),
331 })
332 }
333
334 fn scrollbar_content(&self) -> Option<ScrollbarContent> {
335 let scrollbar_axes = skip_default(ScrollbarAxesContent {
336 horizontal: self.read_enum("editor.scrollbar.horizontal", |s| match s {
337 "auto" | "visible" => Some(true),
338 "hidden" => Some(false),
339 _ => None,
340 }),
341 vertical: self.read_enum("editor.scrollbar.vertical", |s| match s {
342 "auto" | "visible" => Some(true),
343 "hidden" => Some(false),
344 _ => None,
345 }),
346 })?;
347
348 Some(ScrollbarContent {
349 axes: Some(scrollbar_axes),
350 ..Default::default()
351 })
352 }
353
354 fn search_content(&self) -> Option<SearchSettingsContent> {
355 skip_default(SearchSettingsContent {
356 include_ignored: self.read_bool("search.useIgnoreFiles"),
357 ..Default::default()
358 })
359 }
360
361 fn minimap_content(&self) -> Option<MinimapContent> {
362 let minimap_enabled = self.read_bool("editor.minimap.enabled");
363 let autohide = self.read_bool("editor.minimap.autohide");
364 let show = match (minimap_enabled, autohide) {
365 (Some(true), Some(false)) => Some(ShowMinimap::Always),
366 (Some(true), _) => Some(ShowMinimap::Auto),
367 (Some(false), _) => Some(ShowMinimap::Never),
368 _ => None,
369 };
370
371 skip_default(MinimapContent {
372 show,
373 thumb: self.read_enum("editor.minimap.showSlider", |s| match s {
374 "always" => Some(MinimapThumb::Always),
375 "mouseover" => Some(MinimapThumb::Hover),
376 _ => None,
377 }),
378 max_width_columns: self
379 .read_u32("editor.minimap.maxColumn")
380 .and_then(|v| NonZeroU32::new(v)),
381 ..Default::default()
382 })
383 }
384
385 fn git_panel_settings_content(&self) -> Option<GitPanelSettingsContent> {
386 skip_default(GitPanelSettingsContent {
387 button: self.read_bool("git.enabled"),
388 fallback_branch_name: self.read_string("git.defaultBranchName"),
389 ..Default::default()
390 })
391 }
392
393 fn project_settings_content(&self) -> ProjectSettingsContent {
394 ProjectSettingsContent {
395 all_languages: AllLanguageSettingsContent {
396 features: None,
397 edit_predictions: self.edit_predictions_settings_content(),
398 defaults: self.default_language_settings_content(),
399 languages: Default::default(),
400 file_types: self.file_types(),
401 },
402 worktree: self.worktree_settings_content(),
403 lsp: Default::default(),
404 terminal: None,
405 dap: Default::default(),
406 context_servers: self.context_servers(),
407 context_server_timeout: None,
408 load_direnv: None,
409 slash_commands: None,
410 git_hosting_providers: None,
411 }
412 }
413
414 fn default_language_settings_content(&self) -> LanguageSettingsContent {
415 LanguageSettingsContent {
416 allow_rewrap: None,
417 always_treat_brackets_as_autoclosed: None,
418 auto_indent: None,
419 auto_indent_on_paste: self.read_bool("editor.formatOnPaste"),
420 code_actions_on_format: None,
421 completions: skip_default(CompletionSettingsContent {
422 words: self.read_bool("editor.suggest.showWords").map(|b| {
423 if b {
424 WordsCompletionMode::Enabled
425 } else {
426 WordsCompletionMode::Disabled
427 }
428 }),
429 ..Default::default()
430 }),
431 debuggers: None,
432 edit_predictions_disabled_in: None,
433 enable_language_server: None,
434 ensure_final_newline_on_save: self.read_bool("files.insertFinalNewline"),
435 extend_comment_on_newline: None,
436 extend_list_on_newline: None,
437 indent_list_on_tab: None,
438 format_on_save: self.read_bool("editor.guides.formatOnSave").map(|b| {
439 if b {
440 FormatOnSave::On
441 } else {
442 FormatOnSave::Off
443 }
444 }),
445 formatter: None,
446 hard_tabs: self.read_bool("editor.insertSpaces").map(|v| !v),
447 indent_guides: skip_default(IndentGuideSettingsContent {
448 enabled: self.read_bool("editor.guides.indentation"),
449 ..Default::default()
450 }),
451 inlay_hints: None,
452 jsx_tag_auto_close: None,
453 language_servers: None,
454 linked_edits: self.read_bool("editor.linkedEditing"),
455 preferred_line_length: self.read_u32("editor.wordWrapColumn"),
456 prettier: None,
457 remove_trailing_whitespace_on_save: self.read_bool("editor.trimAutoWhitespace"),
458 show_completion_documentation: None,
459 colorize_brackets: self.read_bool("editor.bracketPairColorization.enabled"),
460 show_completions_on_input: self.read_bool("editor.suggestOnTriggerCharacters"),
461 show_edit_predictions: self.read_bool("editor.inlineSuggest.enabled"),
462 show_whitespaces: self.read_enum("editor.renderWhitespace", |s| {
463 Some(match s {
464 "boundary" => ShowWhitespaceSetting::Boundary,
465 "trailing" => ShowWhitespaceSetting::Trailing,
466 "selection" => ShowWhitespaceSetting::Selection,
467 "all" => ShowWhitespaceSetting::All,
468 _ => ShowWhitespaceSetting::None,
469 })
470 }),
471 show_wrap_guides: None,
472 soft_wrap: self.read_enum("editor.wordWrap", |s| match s {
473 "on" => Some(SoftWrap::EditorWidth),
474 "wordWrapColumn" => Some(SoftWrap::PreferLine),
475 "bounded" => Some(SoftWrap::Bounded),
476 "off" => Some(SoftWrap::None),
477 _ => None,
478 }),
479 tab_size: self
480 .read_u32("editor.tabSize")
481 .and_then(|n| NonZeroU32::new(n)),
482 tasks: None,
483 use_auto_surround: self.read_enum("editor.autoSurround", |s| match s {
484 "languageDefined" | "quotes" | "brackets" => Some(true),
485 "never" => Some(false),
486 _ => None,
487 }),
488 use_autoclose: None,
489 use_on_type_format: self.read_bool("editor.formatOnType"),
490 whitespace_map: None,
491 wrap_guides: self
492 .read_value("editor.rulers")
493 .and_then(|v| v.as_array())
494 .map(|v| {
495 v.iter()
496 .flat_map(|n| n.as_u64().map(|n| n as usize))
497 .collect()
498 }),
499 word_diff_enabled: None,
500 }
501 }
502
503 fn file_types(&self) -> Option<HashMap<Arc<str>, ExtendingVec<String>>> {
504 // vscodes file association map is inverted from ours, so we flip the mapping before merging
505 let mut associations: HashMap<Arc<str>, ExtendingVec<String>> = HashMap::default();
506 let map = self.read_value("files.associations")?.as_object()?;
507 for (k, v) in map {
508 let Some(v) = v.as_str() else { continue };
509 associations.entry(v.into()).or_default().0.push(k.clone());
510 }
511 skip_default(associations)
512 }
513
514 fn edit_predictions_settings_content(&self) -> Option<EditPredictionSettingsContent> {
515 let disabled_globs = self
516 .read_value("cursor.general.globalCursorIgnoreList")?
517 .as_array()?;
518
519 skip_default(EditPredictionSettingsContent {
520 disabled_globs: skip_default(
521 disabled_globs
522 .iter()
523 .filter_map(|glob| glob.as_str())
524 .map(|s| s.to_string())
525 .collect(),
526 ),
527 ..Default::default()
528 })
529 }
530
531 fn outline_panel_settings_content(&self) -> Option<OutlinePanelSettingsContent> {
532 skip_default(OutlinePanelSettingsContent {
533 file_icons: self.read_bool("outline.icons"),
534 folder_icons: self.read_bool("outline.icons"),
535 git_status: self.read_bool("git.decorations.enabled"),
536 ..Default::default()
537 })
538 }
539
540 fn node_binary_settings(&self) -> Option<NodeBinarySettings> {
541 // this just sets the binary name instead of a full path so it relies on path lookup
542 // resolving to the one you want
543 skip_default(NodeBinarySettings {
544 npm_path: self.read_enum("npm.packageManager", |s| match s {
545 v @ ("npm" | "yarn" | "bun" | "pnpm") => Some(v.to_owned()),
546 _ => None,
547 }),
548 ..Default::default()
549 })
550 }
551
552 fn git_settings_content(&self) -> Option<GitSettings> {
553 let inline_blame = self.read_bool("git.blame.editorDecoration.enabled")?;
554 skip_default(GitSettings {
555 inline_blame: Some(InlineBlameSettings {
556 enabled: Some(inline_blame),
557 ..Default::default()
558 }),
559 ..Default::default()
560 })
561 }
562
563 fn context_servers(&self) -> HashMap<Arc<str>, ContextServerSettingsContent> {
564 #[derive(Deserialize)]
565 struct VsCodeContextServerCommand {
566 command: PathBuf,
567 args: Option<Vec<String>>,
568 env: Option<HashMap<String, String>>,
569 // note: we don't support envFile and type
570 }
571 let Some(mcp) = self.read_value("mcp").and_then(|v| v.as_object()) else {
572 return Default::default();
573 };
574 mcp.iter()
575 .filter_map(|(k, v)| {
576 Some((
577 k.clone().into(),
578 ContextServerSettingsContent::Stdio {
579 enabled: true,
580 remote: false,
581 command: serde_json::from_value::<VsCodeContextServerCommand>(v.clone())
582 .ok()
583 .map(|cmd| ContextServerCommand {
584 path: cmd.command,
585 args: cmd.args.unwrap_or_default(),
586 env: cmd.env,
587 timeout: None,
588 })?,
589 },
590 ))
591 })
592 .collect()
593 }
594
595 fn item_settings_content(&self) -> Option<ItemSettingsContent> {
596 skip_default(ItemSettingsContent {
597 git_status: self.read_bool("git.decorations.enabled"),
598 close_position: self.read_enum("workbench.editor.tabActionLocation", |s| match s {
599 "right" => Some(ClosePosition::Right),
600 "left" => Some(ClosePosition::Left),
601 _ => None,
602 }),
603 file_icons: self.read_bool("workbench.editor.showIcons"),
604 activate_on_close: self
605 .read_bool("workbench.editor.focusRecentEditorAfterClose")
606 .map(|b| {
607 if b {
608 ActivateOnClose::History
609 } else {
610 ActivateOnClose::LeftNeighbour
611 }
612 }),
613 show_diagnostics: None,
614 show_close_button: self
615 .read_bool("workbench.editor.tabActionCloseVisibility")
616 .map(|b| {
617 if b {
618 ShowCloseButton::Always
619 } else {
620 ShowCloseButton::Hidden
621 }
622 }),
623 })
624 }
625
626 fn preview_tabs_settings_content(&self) -> Option<PreviewTabsSettingsContent> {
627 skip_default(PreviewTabsSettingsContent {
628 enabled: self.read_bool("workbench.editor.enablePreview"),
629 enable_preview_from_project_panel: None,
630 enable_preview_from_file_finder: self
631 .read_bool("workbench.editor.enablePreviewFromQuickOpen"),
632 enable_preview_from_multibuffer: None,
633 enable_preview_multibuffer_from_code_navigation: None,
634 enable_preview_file_from_code_navigation: None,
635 enable_keep_preview_on_code_navigation: self
636 .read_bool("workbench.editor.enablePreviewFromCodeNavigation"),
637 })
638 }
639
640 fn tab_bar_settings_content(&self) -> Option<TabBarSettingsContent> {
641 skip_default(TabBarSettingsContent {
642 show: self.read_enum("workbench.editor.showTabs", |s| match s {
643 "multiple" => Some(true),
644 "single" | "none" => Some(false),
645 _ => None,
646 }),
647 show_nav_history_buttons: None,
648 show_tab_bar_buttons: self
649 .read_str("workbench.editor.editorActionsLocation")
650 .and_then(|str| if str == "hidden" { Some(false) } else { None }),
651 show_pinned_tabs_in_separate_row: None,
652 })
653 }
654
655 fn status_bar_settings_content(&self) -> Option<StatusBarSettingsContent> {
656 skip_default(StatusBarSettingsContent {
657 show: self.read_bool("workbench.statusBar.visible"),
658 active_language_button: None,
659 cursor_position_button: None,
660 line_endings_button: None,
661 active_encoding_button: None,
662 })
663 }
664
665 fn project_panel_settings_content(&self) -> Option<ProjectPanelSettingsContent> {
666 let mut project_panel_settings = ProjectPanelSettingsContent {
667 auto_fold_dirs: self.read_bool("explorer.compactFolders"),
668 auto_reveal_entries: self.read_bool("explorer.autoReveal"),
669 button: None,
670 default_width: None,
671 dock: None,
672 drag_and_drop: None,
673 entry_spacing: None,
674 file_icons: None,
675 folder_icons: None,
676 git_status: self.read_bool("git.decorations.enabled"),
677 hide_gitignore: self.read_bool("explorer.excludeGitIgnore"),
678 hide_hidden: None,
679 hide_root: None,
680 indent_guides: None,
681 indent_size: None,
682 scrollbar: None,
683 show_diagnostics: self
684 .read_bool("problems.decorations.enabled")
685 .and_then(|b| if b { Some(ShowDiagnostics::Off) } else { None }),
686 sort_mode: None,
687 starts_open: None,
688 sticky_scroll: None,
689 auto_open: None,
690 };
691
692 if let (Some(false), Some(false)) = (
693 self.read_bool("explorer.decorations.badges"),
694 self.read_bool("explorer.decorations.colors"),
695 ) {
696 project_panel_settings.git_status = Some(false);
697 project_panel_settings.show_diagnostics = Some(ShowDiagnostics::Off);
698 }
699
700 skip_default(project_panel_settings)
701 }
702
703 fn telemetry_settings_content(&self) -> Option<TelemetrySettingsContent> {
704 self.read_enum("telemetry.telemetryLevel", |level| {
705 let (metrics, diagnostics) = match level {
706 "all" => (true, true),
707 "error" | "crash" => (false, true),
708 "off" => (false, false),
709 _ => return None,
710 };
711 Some(TelemetrySettingsContent {
712 metrics: Some(metrics),
713 diagnostics: Some(diagnostics),
714 })
715 })
716 }
717
718 fn terminal_settings_content(&self) -> Option<TerminalSettingsContent> {
719 let (font_family, font_fallbacks) = self.read_fonts("terminal.integrated.fontFamily");
720 skip_default(TerminalSettingsContent {
721 alternate_scroll: None,
722 blinking: self
723 .read_bool("terminal.integrated.cursorBlinking")
724 .map(|b| {
725 if b {
726 TerminalBlink::On
727 } else {
728 TerminalBlink::Off
729 }
730 }),
731 button: None,
732 copy_on_select: self.read_bool("terminal.integrated.copyOnSelection"),
733 cursor_shape: self.read_enum("terminal.integrated.cursorStyle", |s| match s {
734 "block" => Some(CursorShapeContent::Block),
735 "line" => Some(CursorShapeContent::Bar),
736 "underline" => Some(CursorShapeContent::Underline),
737 _ => None,
738 }),
739 default_height: None,
740 default_width: None,
741 dock: None,
742 font_fallbacks,
743 font_family,
744 font_features: None,
745 font_size: self
746 .read_f32("terminal.integrated.fontSize")
747 .map(FontSize::from),
748 font_weight: None,
749 keep_selection_on_copy: None,
750 line_height: self
751 .read_f32("terminal.integrated.lineHeight")
752 .map(|lh| TerminalLineHeight::Custom(lh)),
753 max_scroll_history_lines: self.read_usize("terminal.integrated.scrollback"),
754 minimum_contrast: None,
755 option_as_meta: self.read_bool("terminal.integrated.macOptionIsMeta"),
756 project: self.project_terminal_settings_content(),
757 scrollbar: None,
758 scroll_multiplier: None,
759 toolbar: None,
760 })
761 }
762
763 fn project_terminal_settings_content(&self) -> ProjectTerminalSettingsContent {
764 #[cfg(target_os = "windows")]
765 let platform = "windows";
766 #[cfg(target_os = "linux")]
767 let platform = "linux";
768 #[cfg(target_os = "macos")]
769 let platform = "osx";
770 #[cfg(target_os = "freebsd")]
771 let platform = "freebsd";
772 let env = self
773 .read_value(&format!("terminal.integrated.env.{platform}"))
774 .and_then(|v| v.as_object())
775 .map(|v| {
776 v.iter()
777 .map(|(k, v)| (k.clone(), v.to_string()))
778 // zed does not support substitutions, so this can break env vars
779 .filter(|(_, v)| !v.contains('$'))
780 .collect()
781 });
782
783 ProjectTerminalSettingsContent {
784 // TODO: handle arguments
785 shell: self
786 .read_string(&format!("terminal.integrated.{platform}Exec"))
787 .map(|s| Shell::Program(s)),
788 working_directory: None,
789 env,
790 detect_venv: None,
791 path_hyperlink_regexes: None,
792 path_hyperlink_timeout_ms: None,
793 }
794 }
795
796 fn theme_settings_content(&self) -> ThemeSettingsContent {
797 let (buffer_font_family, buffer_font_fallbacks) = self.read_fonts("editor.fontFamily");
798 ThemeSettingsContent {
799 ui_font_size: None,
800 ui_font_family: None,
801 ui_font_fallbacks: None,
802 ui_font_features: None,
803 ui_font_weight: None,
804 buffer_font_family,
805 buffer_font_fallbacks,
806 buffer_font_size: self.read_f32("editor.fontSize").map(FontSize::from),
807 buffer_font_weight: self.read_f32("editor.fontWeight").map(FontWeightContent),
808 buffer_line_height: None,
809 buffer_font_features: None,
810 agent_ui_font_size: None,
811 agent_buffer_font_size: None,
812 theme: None,
813 icon_theme: None,
814 ui_density: None,
815 unnecessary_code_fade: None,
816 experimental_theme_overrides: None,
817 theme_overrides: Default::default(),
818 }
819 }
820
821 fn workspace_settings_content(&self) -> WorkspaceSettingsContent {
822 WorkspaceSettingsContent {
823 active_pane_modifiers: self.active_pane_modifiers(),
824 text_rendering_mode: None,
825 autosave: self.read_enum("files.autoSave", |s| match s {
826 "off" => Some(AutosaveSetting::Off),
827 "afterDelay" => Some(AutosaveSetting::AfterDelay {
828 milliseconds: self
829 .read_value("files.autoSaveDelay")
830 .and_then(|v| v.as_u64())
831 .unwrap_or(1000)
832 .into(),
833 }),
834 "onFocusChange" => Some(AutosaveSetting::OnFocusChange),
835 "onWindowChange" => Some(AutosaveSetting::OnWindowChange),
836 _ => None,
837 }),
838 bottom_dock_layout: None,
839 centered_layout: None,
840 close_on_file_delete: None,
841 command_aliases: Default::default(),
842 confirm_quit: self.read_enum("window.confirmBeforeClose", |s| match s {
843 "always" | "keyboardOnly" => Some(true),
844 "never" => Some(false),
845 _ => None,
846 }),
847 drop_target_size: None,
848 // workbench.editor.limit contains "enabled", "value", and "perEditorGroup"
849 // our semantics match if those are set to true, some N, and true respectively.
850 // we'll ignore "perEditorGroup" for now since we only support a global max
851 max_tabs: if self.read_bool("workbench.editor.limit.enabled") == Some(true) {
852 self.read_usize("workbench.editor.limit.value")
853 .and_then(|n| NonZeroUsize::new(n))
854 } else {
855 None
856 },
857 on_last_window_closed: None,
858 pane_split_direction_horizontal: None,
859 pane_split_direction_vertical: None,
860 resize_all_panels_in_dock: None,
861 restore_on_file_reopen: self.read_bool("workbench.editor.restoreViewState"),
862 restore_on_startup: None,
863 window_decorations: None,
864 show_call_status_icon: None,
865 use_system_path_prompts: self.read_bool("files.simpleDialog.enable"),
866 use_system_prompts: None,
867 use_system_window_tabs: self.read_bool("window.nativeTabs"),
868 when_closing_with_no_tabs: self.read_bool("window.closeWhenEmpty").map(|b| {
869 if b {
870 CloseWindowWhenNoItems::CloseWindow
871 } else {
872 CloseWindowWhenNoItems::KeepWindowOpen
873 }
874 }),
875 zoomed_padding: None,
876 }
877 }
878
879 fn active_pane_modifiers(&self) -> Option<ActivePaneModifiers> {
880 if self.read_bool("accessibility.dimUnfocused.enabled") == Some(true)
881 && let Some(opacity) = self.read_f32("accessibility.dimUnfocused.opacity")
882 {
883 Some(ActivePaneModifiers {
884 border_size: None,
885 inactive_opacity: Some(InactiveOpacity(opacity)),
886 })
887 } else {
888 None
889 }
890 }
891
892 fn worktree_settings_content(&self) -> WorktreeSettingsContent {
893 WorktreeSettingsContent {
894 project_name: None,
895 prevent_sharing_in_public_channels: false,
896 file_scan_exclusions: self
897 .read_value("files.watcherExclude")
898 .and_then(|v| v.as_array())
899 .map(|v| {
900 v.iter()
901 .filter_map(|n| n.as_str().map(str::to_owned))
902 .collect::<Vec<_>>()
903 })
904 .filter(|r| !r.is_empty()),
905 file_scan_inclusions: self
906 .read_value("files.watcherInclude")
907 .and_then(|v| v.as_array())
908 .map(|v| {
909 v.iter()
910 .filter_map(|n| n.as_str().map(str::to_owned))
911 .collect::<Vec<_>>()
912 })
913 .filter(|r| !r.is_empty()),
914 private_files: None,
915 hidden_files: None,
916 read_only_files: self
917 .read_value("files.readonlyExclude")
918 .and_then(|v| v.as_object())
919 .map(|v| {
920 v.iter()
921 .filter_map(|(k, v)| {
922 if v.as_bool().unwrap_or(false) {
923 Some(k.to_owned())
924 } else {
925 None
926 }
927 })
928 .collect::<Vec<_>>()
929 })
930 .filter(|r| !r.is_empty()),
931 }
932 }
933}
934
935fn skip_default<T: Default + PartialEq>(value: T) -> Option<T> {
936 if value == T::default() {
937 None
938 } else {
939 Some(value)
940 }
941}