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