Skip to repository content1167 lines · 47.3 KB · rust
tenant.openagents/omega
No repository description is available.
OpenAgents Git authority 2026-07-28T02:55:05.459Z Public web read
NIP-34 coordinate
30617:7649603503856e5148d571eac2766b288a8ff1e9e35d380337a1d2b0015b4f92:omegaMaintainersHidden in public view
References2 branches · 1 tag
Read-only clone
git clone https://openagents.com/git/tenant.openagents/omega.gitBrowse files
vscode_import.rs
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 credentials_url: None,
183 debugger: None,
184 diagnostics: None,
185 editor: self.editor_settings_content(),
186 extension: ExtensionSettingsContent::default(),
187 file_finder: None,
188 git: self.git_settings_content(),
189 git_panel: self.git_panel_settings_content(),
190 global_lsp_settings: skip_default(GlobalLspSettingsContent {
191 semantic_token_rules: self.semantic_token_rules(),
192 ..GlobalLspSettingsContent::default()
193 }),
194 helix_mode: None,
195 hide_mouse: None,
196 image_viewer: None,
197 markdown_preview: None,
198 journal: None,
199 language_models: None,
200 line_indicator_format: None,
201 log: None,
202 node: self.node_binary_settings(),
203
204 outline_panel: self.outline_panel_settings_content(),
205 preview_tabs: self.preview_tabs_settings_content(),
206 project: self.project_settings_content(),
207 project_panel: self.project_panel_settings_content(),
208 proxy: self.read_string("http.proxy"),
209 reduce_motion: self.read_enum("workbench.reduceMotion", |s| match s {
210 "on" => Some(ReduceMotionMode::On),
211 "off" => Some(ReduceMotionMode::Off),
212 _ => None,
213 }),
214 remote: RemoteSettingsContent::default(),
215 repl: None,
216 server_url: None,
217 session: None,
218 status_bar: self.status_bar_settings_content(),
219 tab_bar: self.tab_bar_settings_content(),
220 tabs: self.item_settings_content(),
221 telemetry: self.telemetry_settings_content(),
222 terminal: self.terminal_settings_content(),
223 theme: Box::new(self.theme_settings_content()),
224 title_bar: None,
225 vim: None,
226 vim_mode: None,
227 workspace: self.workspace_settings_content(),
228 which_key: None,
229 modeline_lines: None,
230 feature_flags: None,
231 instrumentation: None,
232 }
233 }
234
235 fn agent_settings_content(&self) -> Option<AgentSettingsContent> {
236 let enabled = self.read_bool("chat.agent.enabled");
237 skip_default(AgentSettingsContent {
238 enabled: enabled,
239 button: enabled,
240 ..Default::default()
241 })
242 }
243
244 fn editor_settings_content(&self) -> EditorSettingsContent {
245 EditorSettingsContent {
246 auto_signature_help: self.read_bool("editor.parameterHints.enabled"),
247 autoscroll_on_clicks: None,
248 cursor_blink: self.read_enum("editor.cursorBlinking", |s| match s {
249 "blink" | "phase" | "expand" | "smooth" => Some(true),
250 "solid" => Some(false),
251 _ => None,
252 }),
253 cursor_shape: self.read_enum("editor.cursorStyle", |s| match s {
254 "block" => Some(CursorShape::Block),
255 "block-outline" => Some(CursorShape::Hollow),
256 "line" | "line-thin" => Some(CursorShape::Bar),
257 "underline" | "underline-thin" => Some(CursorShape::Underline),
258 _ => None,
259 }),
260 current_line_highlight: self.read_enum("editor.renderLineHighlight", |s| match s {
261 "gutter" => Some(CurrentLineHighlight::Gutter),
262 "line" => Some(CurrentLineHighlight::Line),
263 "all" => Some(CurrentLineHighlight::All),
264 _ => None,
265 }),
266 diagnostics_max_severity: None,
267 double_click_in_multibuffer: None,
268 drag_and_drop_selection: None,
269 excerpt_context_lines: None,
270 expand_excerpt_lines: None,
271 fast_scroll_sensitivity: self.read_f32("editor.fastScrollSensitivity"),
272 sticky_scroll: self.sticky_scroll_content(),
273 go_to_definition_fallback: None,
274 go_to_definition_scroll_strategy: None,
275 lsp_results_location: None,
276 gutter: self.gutter_content(),
277 horizontal_scroll_margin: None,
278 hover_popover_delay: self.read_u64("editor.hover.delay").map(Into::into),
279 hover_popover_enabled: self.read_bool("editor.hover.enabled"),
280 hover_popover_sticky: self.read_bool("editor.hover.sticky"),
281 hover_popover_hiding_delay: self.read_u64("editor.hover.hidingDelay").map(Into::into),
282 inline_code_actions: None,
283 code_lens: None,
284 jupyter: None,
285 lsp_document_colors: None,
286 lsp_document_links: self.read_bool("editor.links"),
287 lsp_highlight_debounce: None,
288 middle_click_paste: None,
289 minimap: self.minimap_content(),
290 minimum_contrast_for_highlights: None,
291 multi_cursor_modifier: self.read_enum("editor.multiCursorModifier", |s| match s {
292 "ctrlCmd" => Some(MultiCursorModifier::CmdOrCtrl),
293 "alt" => Some(MultiCursorModifier::Alt),
294 _ => None,
295 }),
296 redact_private_values: None,
297 relative_line_numbers: self.read_enum("editor.lineNumbers", |s| match s {
298 "relative" => Some(RelativeLineNumbers::Enabled),
299 _ => None,
300 }),
301 rounded_selection: self.read_bool("editor.roundedSelection"),
302 scroll_beyond_last_line: None,
303 mouse_wheel_zoom: self.read_bool("editor.mouseWheelZoom"),
304 scroll_sensitivity: self.read_f32("editor.mouseWheelScrollSensitivity"),
305 scrollbar: self.scrollbar_content(),
306 search: self.search_content(),
307 search_wrap: None,
308 seed_search_query_from_cursor: self.read_enum(
309 "editor.find.seedSearchStringFromSelection",
310 |s| match s {
311 "always" => Some(SeedQuerySetting::Always),
312 "selection" => Some(SeedQuerySetting::Selection),
313 "never" => Some(SeedQuerySetting::Never),
314 _ => None,
315 },
316 ),
317 selection_highlight: self.read_bool("editor.selectionHighlight"),
318 show_signature_help_after_edits: self.read_bool("editor.parameterHints.enabled"),
319 snippet_sort_order: None,
320 toolbar: None,
321 use_smartcase_search: self.read_bool("search.smartCase"),
322 vertical_scroll_margin: self.read_f32("editor.cursorSurroundingLines"),
323 completion_menu_scrollbar: None,
324 completion_detail_alignment: None,
325 completion_menu_item_kind: None,
326 diff_view_style: None,
327 minimum_split_diff_width: None,
328 }
329 }
330
331 fn sticky_scroll_content(&self) -> Option<StickyScrollContent> {
332 skip_default(StickyScrollContent {
333 enabled: self.read_bool("editor.stickyScroll.enabled"),
334 })
335 }
336
337 fn gutter_content(&self) -> Option<GutterContent> {
338 skip_default(GutterContent {
339 line_numbers: self.read_enum("editor.lineNumbers", |s| match s {
340 "on" | "relative" => Some(true),
341 "off" => Some(false),
342 _ => None,
343 }),
344 min_line_number_digits: None,
345 runnables: None,
346 breakpoints: None,
347 bookmarks: None,
348 folds: self.read_enum("editor.showFoldingControls", |s| match s {
349 "always" | "mouseover" => Some(true),
350 "never" => Some(false),
351 _ => None,
352 }),
353 })
354 }
355
356 fn scrollbar_content(&self) -> Option<ScrollbarContent> {
357 let scrollbar_axes = skip_default(ScrollbarAxesContent {
358 horizontal: self.read_enum("editor.scrollbar.horizontal", |s| match s {
359 "auto" | "visible" => Some(true),
360 "hidden" => Some(false),
361 _ => None,
362 }),
363 vertical: self.read_enum("editor.scrollbar.vertical", |s| match s {
364 "auto" | "visible" => Some(true),
365 "hidden" => Some(false),
366 _ => None,
367 }),
368 })?;
369
370 Some(ScrollbarContent {
371 axes: Some(scrollbar_axes),
372 ..Default::default()
373 })
374 }
375
376 fn search_content(&self) -> Option<SearchSettingsContent> {
377 skip_default(SearchSettingsContent {
378 include_ignored: self.read_bool("search.useIgnoreFiles"),
379 ..Default::default()
380 })
381 }
382
383 fn semantic_token_rules(&self) -> Option<SemanticTokenRules> {
384 let customizations = self
385 .read_value("editor.semanticTokenColorCustomizations")?
386 .as_object()?;
387
388 skip_default(SemanticTokenRules {
389 rules: customizations
390 .get("rules")
391 .and_then(|v| {
392 Some(
393 v.as_object()?
394 .iter()
395 .filter_map(|(k, v)| {
396 let v = v.as_object()?;
397
398 let mut underline = v
399 .get("underline")
400 .and_then(|b| b.as_bool())
401 .unwrap_or(false);
402 let strikethrough = v
403 .get("strikethrough")
404 .and_then(|b| b.as_bool())
405 .unwrap_or(false);
406 let mut font_weight =
407 v.get("bold").and_then(|b| b.as_bool()).map(|b| {
408 if b {
409 SemanticTokenFontWeight::Bold
410 } else {
411 SemanticTokenFontWeight::Normal
412 }
413 });
414 let mut font_style =
415 v.get("italic").and_then(|b| b.as_bool()).map(|b| {
416 if b {
417 SemanticTokenFontStyle::Italic
418 } else {
419 SemanticTokenFontStyle::Normal
420 }
421 });
422
423 match v.get("fontStyle").and_then(|s| s.as_str()).unwrap_or("") {
424 "bold" => {
425 font_style = Some(SemanticTokenFontStyle::Normal);
426 font_weight = Some(SemanticTokenFontWeight::Bold);
427 }
428 "italic" => {
429 font_style = Some(SemanticTokenFontStyle::Italic);
430 font_weight = Some(SemanticTokenFontWeight::Normal);
431 }
432 "underline" => {
433 underline = true;
434 }
435 "bold italic" | "italic bold" => {
436 font_style = Some(SemanticTokenFontStyle::Italic);
437 font_weight = Some(SemanticTokenFontWeight::Bold);
438 }
439 "normal" => {
440 font_style = Some(SemanticTokenFontStyle::Normal);
441 font_weight = Some(SemanticTokenFontWeight::Normal);
442 }
443 _ => {}
444 }
445
446 let foreground = v
447 .get("foreground")
448 .and_then(|v| Rgba::try_from(v.as_str()?).ok())
449 .map(|s| s.to_owned());
450 let background = v
451 .get("background")
452 .and_then(|v| Rgba::try_from(v.as_str()?).ok())
453 .map(|s| s.to_owned());
454
455 Some(SemanticTokenRule {
456 token_type: Some(k.clone()),
457 token_modifiers: vec![],
458 style: vec![],
459 underline: if underline {
460 Some(SemanticTokenColorOverride::InheritForeground(true))
461 } else {
462 None
463 },
464 strikethrough: if strikethrough {
465 Some(SemanticTokenColorOverride::InheritForeground(true))
466 } else {
467 None
468 },
469 foreground_color: foreground,
470 background_color: background,
471 font_weight,
472 font_style,
473 })
474 })
475 .collect(),
476 )
477 })
478 .unwrap_or_default(),
479 })
480 }
481
482 fn minimap_content(&self) -> Option<MinimapContent> {
483 let minimap_enabled = self.read_bool("editor.minimap.enabled").unwrap_or(true);
484 let autohide = self.read_bool("editor.minimap.autohide").unwrap_or(false);
485 let show = match (minimap_enabled, autohide) {
486 (true, false) => Some(ShowMinimap::Always),
487 (true, true) => Some(ShowMinimap::Auto),
488 (false, _) => Some(ShowMinimap::Never),
489 };
490
491 skip_default(MinimapContent {
492 show,
493 thumb: self.read_enum("editor.minimap.showSlider", |s| match s {
494 "always" => Some(MinimapThumb::Always),
495 "mouseover" => Some(MinimapThumb::Hover),
496 _ => None,
497 }),
498 max_width_columns: self
499 .read_u32("editor.minimap.maxColumn")
500 .and_then(|v| NonZeroU32::new(v)),
501 ..Default::default()
502 })
503 }
504
505 fn git_panel_settings_content(&self) -> Option<GitPanelSettingsContent> {
506 skip_default(GitPanelSettingsContent {
507 button: self.read_bool("git.enabled"),
508 fallback_branch_name: self.read_string("git.defaultBranchName"),
509 ..Default::default()
510 })
511 }
512
513 fn project_settings_content(&self) -> ProjectSettingsContent {
514 ProjectSettingsContent {
515 all_languages: AllLanguageSettingsContent {
516 edit_predictions: self.edit_predictions_settings_content(),
517 defaults: self.default_language_settings_content(),
518 languages: Default::default(),
519 file_types: self.file_types(),
520 },
521 worktree: self.worktree_settings_content(),
522 lsp: Default::default(),
523 terminal: None,
524 dap: Default::default(),
525 context_servers: self.context_servers(),
526 context_server_timeout: None,
527 load_direnv: None,
528 git_hosting_providers: None,
529 disable_ai: None,
530 }
531 }
532
533 fn default_language_settings_content(&self) -> LanguageSettingsContent {
534 LanguageSettingsContent {
535 allow_rewrap: None,
536 always_treat_brackets_as_autoclosed: None,
537 auto_indent: None,
538 auto_indent_on_paste: self.read_bool("editor.formatOnPaste"),
539 code_actions_on_format: None,
540 completions: skip_default(CompletionSettingsContent {
541 words: self.read_bool("editor.suggest.showWords").map(|b| {
542 if b {
543 WordsCompletionMode::Enabled
544 } else {
545 WordsCompletionMode::Disabled
546 }
547 }),
548 ..Default::default()
549 }),
550 debuggers: None,
551 edit_predictions_disabled_in: None,
552 enable_language_server: None,
553 ensure_final_newline_on_save: self.read_bool("files.insertFinalNewline"),
554 line_ending: self.read_enum("files.eol", |s| match s {
555 "\n" => Some(LineEndingSetting::PreferLf),
556 "\r\n" => Some(LineEndingSetting::PreferCrlf),
557 "auto" => Some(LineEndingSetting::Detect),
558 _ => None,
559 }),
560 extend_comment_on_newline: None,
561 extend_list_on_newline: None,
562 indent_list_on_tab: None,
563 // In VS Code, `editor.formatOnSaveMode` only applies when `editor.formatOnSave` is enabled.
564 format_on_save: self.read_bool("editor.formatOnSave").map(|enabled| {
565 if enabled {
566 self.read_enum("editor.formatOnSaveMode", |s| match s {
567 "modificationsIfAvailable" => Some(FormatOnSave::ModificationsIfAvailable),
568 "modifications" => Some(FormatOnSave::Modifications),
569 _ => None,
570 })
571 .unwrap_or(FormatOnSave::On)
572 } else {
573 FormatOnSave::Off
574 }
575 }),
576 formatter: None,
577 hard_tabs: self.read_bool("editor.insertSpaces").map(|v| !v),
578 indent_guides: skip_default(IndentGuideSettingsContent {
579 enabled: self.read_bool("editor.guides.indentation"),
580 ..Default::default()
581 }),
582 inlay_hints: None,
583 jsx_tag_auto_close: None,
584 language_servers: None,
585 semantic_tokens: self
586 .read_bool("editor.semanticHighlighting.enabled")
587 .map(|enabled| {
588 if enabled {
589 SemanticTokens::Full
590 } else {
591 SemanticTokens::Off
592 }
593 }),
594 document_folding_ranges: None,
595 document_symbols: None,
596 linked_edits: self.read_bool("editor.linkedEditing"),
597 preferred_line_length: self.read_u32("editor.wordWrapColumn"),
598 prettier: None,
599 remove_trailing_whitespace_on_save: self.read_bool("editor.trimAutoWhitespace"),
600 show_completion_documentation: None,
601 colorize_brackets: self.read_bool("editor.bracketPairColorization.enabled"),
602 show_completions_on_input: self.read_bool("editor.suggestOnTriggerCharacters"),
603 show_edit_predictions: self.read_bool("editor.inlineSuggest.enabled"),
604 show_whitespaces: self.read_enum("editor.renderWhitespace", |s| {
605 Some(match s {
606 "boundary" => ShowWhitespaceSetting::Boundary,
607 "trailing" => ShowWhitespaceSetting::Trailing,
608 "selection" => ShowWhitespaceSetting::Selection,
609 "all" => ShowWhitespaceSetting::All,
610 _ => ShowWhitespaceSetting::None,
611 })
612 }),
613 show_wrap_guides: None,
614 soft_wrap: self.read_enum("editor.wordWrap", |s| match s {
615 "on" => Some(SoftWrap::EditorWidth),
616 "wordWrapColumn" => Some(SoftWrap::PreferLine),
617 "bounded" => Some(SoftWrap::Bounded),
618 "off" => Some(SoftWrap::None),
619 _ => None,
620 }),
621 tab_size: self
622 .read_u32("editor.tabSize")
623 .and_then(|n| NonZeroU32::new(n)),
624 tasks: None,
625 use_auto_surround: self.read_enum("editor.autoSurround", |s| match s {
626 "languageDefined" | "quotes" | "brackets" => Some(true),
627 "never" => Some(false),
628 _ => None,
629 }),
630 use_autoclose: None,
631 use_on_type_format: self.read_bool("editor.formatOnType"),
632 whitespace_map: None,
633 wrap_guides: self
634 .read_value("editor.rulers")
635 .and_then(|v| v.as_array())
636 .map(|v| {
637 v.iter()
638 .flat_map(|n| n.as_u64().map(|n| n as usize))
639 .collect()
640 }),
641 word_diff_enabled: None,
642 }
643 }
644
645 fn file_types(&self) -> Option<FileTypeMap> {
646 // vscodes file association map is inverted from ours, so we flip the mapping before merging
647 let mut associations: HashMap<Arc<str>, ExtendingSet<String>> = HashMap::default();
648 let map = self.read_value("files.associations")?.as_object()?;
649 for (k, v) in map {
650 let Some(v) = v.as_str() else { continue };
651 associations
652 .entry(v.into())
653 .or_default()
654 .0
655 .insert(k.clone());
656 }
657 skip_default(FileTypeMap(associations))
658 }
659
660 fn edit_predictions_settings_content(&self) -> Option<EditPredictionSettingsContent> {
661 let disabled_globs = self
662 .read_value("cursor.general.globalCursorIgnoreList")?
663 .as_array()?;
664
665 skip_default(EditPredictionSettingsContent {
666 disabled_globs: skip_default(
667 disabled_globs
668 .iter()
669 .filter_map(|glob| glob.as_str())
670 .map(|s| s.to_string())
671 .collect(),
672 ),
673 ..Default::default()
674 })
675 }
676
677 fn outline_panel_settings_content(&self) -> Option<OutlinePanelSettingsContent> {
678 skip_default(OutlinePanelSettingsContent {
679 file_icons: self.read_bool("outline.icons"),
680 folder_icons: self.read_bool("outline.icons"),
681 git_status: self.read_bool("git.decorations.enabled"),
682 ..Default::default()
683 })
684 }
685
686 fn node_binary_settings(&self) -> Option<NodeBinarySettings> {
687 // this just sets the binary name instead of a full path so it relies on path lookup
688 // resolving to the one you want
689 skip_default(NodeBinarySettings {
690 npm_path: self.read_enum("npm.packageManager", |s| match s {
691 v @ ("npm" | "yarn" | "bun" | "pnpm") => Some(v.to_owned()),
692 _ => None,
693 }),
694 ..Default::default()
695 })
696 }
697
698 fn git_settings_content(&self) -> Option<GitSettings> {
699 let inline_blame = self.read_bool("git.blame.editorDecoration.enabled")?;
700 skip_default(GitSettings {
701 inline_blame: Some(InlineBlameSettings {
702 enabled: Some(inline_blame),
703 ..Default::default()
704 }),
705 ..Default::default()
706 })
707 }
708
709 fn context_servers(&self) -> HashMap<Arc<str>, ContextServerSettingsContent> {
710 #[derive(Deserialize)]
711 struct VsCodeContextServerCommand {
712 command: PathBuf,
713 args: Option<Vec<String>>,
714 env: Option<HashMap<String, String>>,
715 // note: we don't support envFile and type
716 }
717 let Some(mcp) = self.read_value("mcp").and_then(|v| v.as_object()) else {
718 return Default::default();
719 };
720 mcp.iter()
721 .filter_map(|(k, v)| {
722 Some((
723 k.clone().into(),
724 ContextServerSettingsContent::Stdio {
725 enabled: true,
726 remote: false,
727 command: serde_json::from_value::<VsCodeContextServerCommand>(v.clone())
728 .ok()
729 .map(|cmd| ContextServerCommand {
730 path: cmd.command,
731 args: cmd.args.unwrap_or_default(),
732 env: cmd.env,
733 timeout: None,
734 })?,
735 },
736 ))
737 })
738 .collect()
739 }
740
741 fn item_settings_content(&self) -> Option<ItemSettingsContent> {
742 skip_default(ItemSettingsContent {
743 git_status: self.read_bool("git.decorations.enabled"),
744 close_position: self.read_enum("workbench.editor.tabActionLocation", |s| match s {
745 "right" => Some(ClosePosition::Right),
746 "left" => Some(ClosePosition::Left),
747 _ => None,
748 }),
749 file_icons: self.read_bool("workbench.editor.showIcons"),
750 activate_on_close: self
751 .read_bool("workbench.editor.focusRecentEditorAfterClose")
752 .map(|b| {
753 if b {
754 ActivateOnClose::History
755 } else {
756 ActivateOnClose::LeftNeighbour
757 }
758 }),
759 show_diagnostics: None,
760 show_close_button: self
761 .read_bool("workbench.editor.tabActionCloseVisibility")
762 .map(|b| {
763 if b {
764 ShowCloseButton::Always
765 } else {
766 ShowCloseButton::Hidden
767 }
768 }),
769 })
770 }
771
772 fn preview_tabs_settings_content(&self) -> Option<PreviewTabsSettingsContent> {
773 skip_default(PreviewTabsSettingsContent {
774 enabled: self.read_bool("workbench.editor.enablePreview"),
775 enable_preview_from_project_panel: None,
776 enable_preview_from_file_finder: self
777 .read_bool("workbench.editor.enablePreviewFromQuickOpen"),
778 enable_preview_from_multibuffer: None,
779 enable_preview_multibuffer_from_code_navigation: None,
780 enable_preview_file_from_code_navigation: None,
781 enable_keep_preview_on_code_navigation: self
782 .read_bool("workbench.editor.enablePreviewFromCodeNavigation"),
783 })
784 }
785
786 fn tab_bar_settings_content(&self) -> Option<TabBarSettingsContent> {
787 skip_default(TabBarSettingsContent {
788 show: self.read_enum("workbench.editor.showTabs", |s| match s {
789 "multiple" => Some(true),
790 "single" | "none" => Some(false),
791 _ => None,
792 }),
793 show_nav_history_buttons: None,
794 show_tab_bar_buttons: self
795 .read_str("workbench.editor.editorActionsLocation")
796 .and_then(|str| if str == "hidden" { Some(false) } else { None }),
797 show_pinned_tabs_in_separate_row: None,
798 })
799 }
800
801 fn status_bar_settings_content(&self) -> Option<StatusBarSettingsContent> {
802 skip_default(StatusBarSettingsContent {
803 show: self.read_bool("workbench.statusBar.visible"),
804 show_active_file: None,
805 active_language_button: None,
806 cursor_position_button: None,
807 line_endings_button: None,
808 active_encoding_button: None,
809 })
810 }
811
812 fn project_panel_settings_content(&self) -> Option<ProjectPanelSettingsContent> {
813 let mut project_panel_settings = ProjectPanelSettingsContent {
814 auto_fold_dirs: self.read_bool("explorer.compactFolders"),
815 auto_reveal_entries: self.read_bool("explorer.autoReveal"),
816 bold_folder_labels: None,
817 button: None,
818 default_width: None,
819 dock: None,
820 drag_and_drop: None,
821 entry_spacing: None,
822 file_icons: None,
823 folder_icons: None,
824 git_status: self.read_bool("git.decorations.enabled"),
825 hide_gitignore: self.read_bool("explorer.excludeGitIgnore"),
826 hide_hidden: None,
827 hide_root: None,
828 indent_guides: None,
829 indent_size: None,
830 scrollbar: self.read_bool("workbench.list.horizontalScrolling").map(
831 |horizontal_scrolling| ProjectPanelScrollbarSettingsContent {
832 show: None,
833 horizontal_scroll: Some(horizontal_scrolling),
834 },
835 ),
836 show_diagnostics: self
837 .read_bool("problems.decorations.enabled")
838 .and_then(|b| if b { Some(ShowDiagnostics::Off) } else { None }),
839 sort_mode: self.read_enum("explorer.sortOrder", |s| match s {
840 "default" | "foldersNestsFiles" => Some(ProjectPanelSortMode::DirectoriesFirst),
841 "mixed" => Some(ProjectPanelSortMode::Mixed),
842 "filesFirst" => Some(ProjectPanelSortMode::FilesFirst),
843 _ => None,
844 }),
845 sort_order: self.read_enum("explorer.sortOrderLexicographicOptions", |s| match s {
846 "default" => Some(ProjectPanelSortOrder::Default),
847 "upper" => Some(ProjectPanelSortOrder::Upper),
848 "lower" => Some(ProjectPanelSortOrder::Lower),
849 "unicode" => Some(ProjectPanelSortOrder::Unicode),
850 _ => None,
851 }),
852 starts_open: None,
853 sticky_scroll: None,
854 auto_open: None,
855 diagnostic_badges: None,
856 git_status_indicator: None,
857 };
858
859 if let (Some(false), Some(false)) = (
860 self.read_bool("explorer.decorations.badges"),
861 self.read_bool("explorer.decorations.colors"),
862 ) {
863 project_panel_settings.git_status = Some(false);
864 project_panel_settings.show_diagnostics = Some(ShowDiagnostics::Off);
865 }
866
867 skip_default(project_panel_settings)
868 }
869
870 fn telemetry_settings_content(&self) -> Option<TelemetrySettingsContent> {
871 self.read_enum("telemetry.telemetryLevel", |level| {
872 let (metrics, diagnostics) = match level {
873 "all" => (true, true),
874 "error" | "crash" => (false, true),
875 "off" => (false, false),
876 _ => return None,
877 };
878 Some(TelemetrySettingsContent {
879 metrics: Some(metrics),
880 diagnostics: Some(diagnostics),
881 anthropic_retention: None,
882 })
883 })
884 }
885
886 fn terminal_settings_content(&self) -> Option<TerminalSettingsContent> {
887 let (font_family, font_fallbacks) = self.read_fonts("terminal.integrated.fontFamily");
888 skip_default(TerminalSettingsContent {
889 alternate_scroll: None,
890 blinking: self
891 .read_bool("terminal.integrated.cursorBlinking")
892 .map(|b| {
893 if b {
894 TerminalBlink::On
895 } else {
896 TerminalBlink::Off
897 }
898 }),
899 button: None,
900 copy_on_select: self.read_bool("terminal.integrated.copyOnSelection"),
901 cursor_shape: self.read_enum("terminal.integrated.cursorStyle", |s| match s {
902 "block" => Some(CursorShapeContent::Block),
903 "line" => Some(CursorShapeContent::Bar),
904 "underline" => Some(CursorShapeContent::Underline),
905 _ => None,
906 }),
907 default_height: None,
908 default_width: None,
909 dock: None,
910 font_fallbacks,
911 font_family,
912 font_features: None,
913 font_size: self
914 .read_f32("terminal.integrated.fontSize")
915 .map(FontSize::from),
916 font_weight: None,
917 keep_selection_on_copy: None,
918 open_links_in_mouse_mode: None,
919 line_height: self
920 .read_f32("terminal.integrated.lineHeight")
921 .map(|lh| TerminalLineHeight::Custom(lh)),
922 max_scroll_history_lines: self.read_usize("terminal.integrated.scrollback"),
923 bell: self
924 .read_value("accessibility.signals.terminalBell")
925 .and_then(|v| Some(v.get("sound")?.as_str()? == "on"))
926 .or_else(|| {
927 // Older deprecated setting, might as well still support it:
928 self.read_value("terminal.integrated.enableBell")
929 .map(|v| v.as_bool() == Some(true) || v.as_str() == Some("both"))
930 })
931 .map(|enabled| {
932 if enabled {
933 TerminalBell::System
934 } else {
935 TerminalBell::Off
936 }
937 }),
938 minimum_contrast: None,
939 option_as_meta: self.read_bool("terminal.integrated.macOptionIsMeta"),
940 project: self.project_terminal_settings_content(),
941 scrollbar: None,
942 scroll_multiplier: None,
943 toolbar: None,
944 show_count_badge: None,
945 flexible: None,
946 })
947 }
948
949 fn project_terminal_settings_content(&self) -> ProjectTerminalSettingsContent {
950 #[cfg(target_os = "windows")]
951 let platform = "windows";
952 #[cfg(target_os = "linux")]
953 let platform = "linux";
954 #[cfg(target_os = "macos")]
955 let platform = "osx";
956 #[cfg(target_os = "freebsd")]
957 let platform = "freebsd";
958 let env = self
959 .read_value(&format!("terminal.integrated.env.{platform}"))
960 .and_then(|v| v.as_object())
961 .map(|v| {
962 v.iter()
963 .map(|(k, v)| (k.clone(), v.to_string()))
964 // zed does not support substitutions, so this can break env vars
965 .filter(|(_, v)| !v.contains('$'))
966 .collect()
967 });
968
969 ProjectTerminalSettingsContent {
970 // TODO: handle arguments
971 shell: self
972 .read_string(&format!("terminal.integrated.{platform}Exec"))
973 .map(|s| Shell::Program(s)),
974 working_directory: None,
975 env,
976 detect_venv: None,
977 path_hyperlink_regexes: None,
978 path_hyperlink_timeout_ms: None,
979 }
980 }
981
982 fn theme_settings_content(&self) -> ThemeSettingsContent {
983 let (buffer_font_family, buffer_font_fallbacks) = self.read_fonts("editor.fontFamily");
984 ThemeSettingsContent {
985 ui_font_size: None,
986 ui_font_family: None,
987 ui_font_fallbacks: None,
988 ui_font_features: None,
989 ui_font_weight: None,
990 buffer_font_family,
991 buffer_font_fallbacks,
992 buffer_font_size: self.read_f32("editor.fontSize").map(FontSize::from),
993 buffer_font_weight: self.read_f32("editor.fontWeight").map(FontWeightContent),
994 buffer_line_height: None,
995 buffer_font_features: None,
996 agent_ui_font_size: None,
997 agent_buffer_font_size: None,
998 git_commit_buffer_font_size: None,
999 markdown_preview_font_family: None,
1000 markdown_preview_code_font_family: None,
1001 markdown_preview_font_size: None,
1002 markdown_preview_theme: None,
1003 theme: None,
1004 icon_theme: None,
1005 ui_density: None,
1006 unnecessary_code_fade: None,
1007 experimental_theme_overrides: None,
1008 theme_overrides: Default::default(),
1009 }
1010 }
1011
1012 fn workspace_settings_content(&self) -> WorkspaceSettingsContent {
1013 WorkspaceSettingsContent {
1014 active_pane_modifiers: self.active_pane_modifiers(),
1015 accessible_mode: None,
1016 text_rendering_mode: None,
1017 autosave: self.read_enum("files.autoSave", |s| match s {
1018 "off" => Some(AutosaveSetting::Off),
1019 "afterDelay" => Some(AutosaveSetting::AfterDelay {
1020 milliseconds: self
1021 .read_value("files.autoSaveDelay")
1022 .and_then(|v| v.as_u64())
1023 .unwrap_or(1000)
1024 .into(),
1025 }),
1026 "onFocusChange" => Some(AutosaveSetting::OnFocusChange),
1027 "onWindowChange" => Some(AutosaveSetting::OnWindowChange),
1028 _ => None,
1029 }),
1030 bottom_dock_layout: None,
1031 centered_layout: None,
1032 cli_default_open_behavior: None,
1033 default_open_behavior: None,
1034 close_on_file_delete: None,
1035 close_panel_on_toggle: None,
1036 command_aliases: Default::default(),
1037 confirm_quit: self.read_enum("window.confirmBeforeClose", |s| match s {
1038 "always" | "keyboardOnly" => Some(true),
1039 "never" => Some(false),
1040 _ => None,
1041 }),
1042 drop_target_size: None,
1043 // workbench.editor.limit contains "enabled", "value", and "perEditorGroup"
1044 // our semantics match if those are set to true, some N, and true respectively.
1045 // we'll ignore "perEditorGroup" for now since we only support a global max
1046 max_tabs: if self.read_bool("workbench.editor.limit.enabled") == Some(true) {
1047 self.read_usize("workbench.editor.limit.value")
1048 .and_then(|n| NonZeroUsize::new(n))
1049 } else {
1050 None
1051 },
1052 on_last_window_closed: None,
1053 pane_split_direction_horizontal: None,
1054 pane_split_direction_vertical: None,
1055 resize_all_panels_in_dock: None,
1056 restore_on_file_reopen: self.read_bool("workbench.editor.restoreViewState"),
1057 restore_on_startup: None,
1058 window_decorations: None,
1059 show_call_status_icon: None,
1060 use_system_path_prompts: self.read_bool("files.simpleDialog.enable").map(|b| !b),
1061 use_system_prompts: None,
1062 use_system_window_tabs: self.read_bool("window.nativeTabs"),
1063 when_closing_with_no_tabs: self.read_bool("window.closeWhenEmpty").map(|b| {
1064 if b {
1065 CloseWindowWhenNoItems::CloseWindow
1066 } else {
1067 CloseWindowWhenNoItems::KeepWindowOpen
1068 }
1069 }),
1070 zoomed_padding: None,
1071 focus_follows_mouse: None,
1072 }
1073 }
1074
1075 fn active_pane_modifiers(&self) -> Option<ActivePaneModifiers> {
1076 if self.read_bool("accessibility.dimUnfocused.enabled") == Some(true)
1077 && let Some(opacity) = self.read_f32("accessibility.dimUnfocused.opacity")
1078 {
1079 Some(ActivePaneModifiers {
1080 border_size: None,
1081 inactive_opacity: Some(InactiveOpacity(opacity)),
1082 })
1083 } else {
1084 None
1085 }
1086 }
1087
1088 fn worktree_settings_content(&self) -> WorktreeSettingsContent {
1089 WorktreeSettingsContent {
1090 prevent_sharing_in_public_channels: false,
1091 file_scan_exclusions: self
1092 .read_value("files.watcherExclude")
1093 .and_then(|v| v.as_array())
1094 .map(|v| {
1095 v.iter()
1096 .filter_map(|n| n.as_str().map(str::to_owned))
1097 .collect::<Vec<_>>()
1098 })
1099 .filter(|r| !r.is_empty()),
1100 file_scan_inclusions: self
1101 .read_value("files.watcherInclude")
1102 .and_then(|v| v.as_array())
1103 .map(|v| {
1104 v.iter()
1105 .filter_map(|n| n.as_str().map(str::to_owned))
1106 .collect::<Vec<_>>()
1107 })
1108 .filter(|r| !r.is_empty()),
1109 scan_symlinks: None,
1110 private_files: None,
1111 hidden_files: None,
1112 read_only_files: self
1113 .read_value("files.readonlyExclude")
1114 .and_then(|v| v.as_object())
1115 .map(|v| {
1116 v.iter()
1117 .filter_map(|(k, v)| {
1118 if v.as_bool().unwrap_or(false) {
1119 Some(k.to_owned())
1120 } else {
1121 None
1122 }
1123 })
1124 .collect::<Vec<_>>()
1125 })
1126 .filter(|r| !r.is_empty()),
1127 }
1128 }
1129}
1130
1131fn skip_default<T: Default + PartialEq>(value: T) -> Option<T> {
1132 if value == T::default() {
1133 None
1134 } else {
1135 Some(value)
1136 }
1137}
1138
1139#[cfg(test)]
1140mod tests {
1141 use super::*;
1142
1143 fn imported_reduce_motion(content: &str) -> Option<ReduceMotionMode> {
1144 VsCodeSettings::from_str(content, VsCodeSettingsSource::VsCode)
1145 .unwrap()
1146 .settings_content()
1147 .reduce_motion
1148 }
1149
1150 #[test]
1151 fn test_import_reduce_motion() {
1152 assert_eq!(
1153 imported_reduce_motion(r#"{ "workbench.reduceMotion": "on" }"#),
1154 Some(ReduceMotionMode::On)
1155 );
1156 assert_eq!(
1157 imported_reduce_motion(r#"{ "workbench.reduceMotion": "off" }"#),
1158 Some(ReduceMotionMode::Off)
1159 );
1160 assert_eq!(
1161 imported_reduce_motion(r#"{ "workbench.reduceMotion": "auto" }"#),
1162 None
1163 );
1164 assert_eq!(imported_reduce_motion("{}"), None);
1165 }
1166}
1167