Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 50 additions & 0 deletions docs/theme-import.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
# VS Code theme import (workbench colors)

Querya can apply a **subset** of VS Code theme JSON / JSONC `colors` to
`QueryaWorkbenchTheme`, `QueryaEditorTheme`, and the shadcn `ColorScheme`.

Syntax highlighting (`tokenColors`) is tracked separately (issue #46).

## Supported `colors` keys

| VS Code key | Querya target |
|-------------|---------------|
| `editor.background` | `workbench.editorBackground`, `editor.background` |
| `editor.foreground` | `editor.foreground`, `ColorScheme.foreground` |
| `sideBar.background` | `workbench.sidebarBackground` |
| `sideBar.foreground` | `workbench.mutedForeground` |
| `activityBar.background` | `workbench.canvas` |
| `tab.activeBackground` | `workbench.surface` |
| `statusBar.background` | `workbench.canvas` |
| `panel.background` | `workbench.surface` |
| `focusBorder` | `workbench.accent`, `ColorScheme.ring` |
| `input.background` | `workbench.surface` |
| `list.hoverBackground` | `ColorScheme.accent` |
| `gitDecoration.modifiedResourceForeground` | `workbench.gitModified` |
| `gitDecoration.untrackedResourceForeground` | `workbench.gitUntracked` |

Implementation: `lib/core/theme/parser/vscode_color_map.dart`,
`lib/core/theme/parser/querya_theme_from_vscode.dart`.

## Behavior

- **`type`**: `"dark"` or `"light"` in the manifest selects brightness and
default fallback (`QueryaTheme.darkDefault` / `lightDefault`).
- **Missing keys**: unchanged from the fallback theme.
- **Unknown keys**: ignored; in debug builds a line is printed to the console.
- **Invalid color values**: skipped for that key only.

## Color formats

Hex strings as in VS Code: `#RRGGBB`, `#RRGGBBAA`, `#RGB`, `#RGBA` (see
`parseVsCodeColor`).

## JSONC

Comments and trailing commas are stripped before parse (`stripJsonc`).

## Fixtures (tests)

- `test/fixtures/themes/dark_subset.json`
- `test/fixtures/themes/light_subset.json`
- `test/fixtures/themes/with_unknown_keys.json`
2 changes: 1 addition & 1 deletion lib/core/theme/parser/color_parser.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import 'dart:ui';
Color parseVsCodeColor(String input) {
var s = input.trim();
if (s.isEmpty) {
throw FormatException('Empty color string');
throw const FormatException('Empty color string');
}
if (s.startsWith('#')) {
s = s.substring(1);
Expand Down
189 changes: 189 additions & 0 deletions lib/core/theme/parser/querya_theme_from_vscode.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,189 @@
import 'dart:ui';

import 'package:flutter/foundation.dart';

import '../querya_editor_theme.dart';
import '../querya_theme.dart';
import '../querya_workbench_theme.dart';
import 'color_parser.dart';
import 'vscode_color_map.dart';
import 'vscode_theme_manifest.dart';

/// Builds a [QueryaTheme] from a parsed VS Code manifest.
///
/// Missing keys keep values from [fallback] (defaults by manifest `type`).
QueryaTheme buildQueryaThemeFromVsCodeManifest(
VsCodeThemeManifest manifest, {
QueryaTheme? fallback,
void Function(String unknownVsCodeKey)? onUnknownColorKey,
}) {
final base = fallback ?? _defaultFallbackFor(manifest);
final brightness = _brightnessFrom(manifest, base);

var workbench = base.workbench;
var editor = base.editor;
Color? schemeForeground;
Color? schemeBackground;
Color? schemeCard;
Color? schemeBorder;
Color? schemeInput;
Color? schemeRing;
Color? schemeMutedForeground;
Color? schemeAccent;

for (final entry in manifest.colors.entries) {
final target = kVsCodeColorMap[entry.key];
if (target == null) {
onUnknownColorKey?.call(entry.key);
if (kDebugMode) {
debugPrint('VsCode theme: ignored color key "${entry.key}"');
}
continue;
}

final Color color;
try {
color = parseVsCodeColor(entry.value);
} on FormatException {
if (kDebugMode) {
debugPrint(
'VsCode theme: invalid color for "${entry.key}": ${entry.value}',
);
}
continue;
}

if (target.workbench != null) {
workbench = _applyWorkbenchField(workbench, target.workbench!, color);
if (target.workbench == VsCodeWorkbenchField.editorBackground) {
editor = editor.copyWith(background: color);
}
} else if (target.editor != null) {
editor = _applyEditorField(editor, target.editor!, color);
} else if (target.colorScheme != null) {
switch (target.colorScheme!) {
case VsCodeColorSchemeField.foreground:
schemeForeground = color;
case VsCodeColorSchemeField.background:
schemeBackground = color;
case VsCodeColorSchemeField.card:
schemeCard = color;
case VsCodeColorSchemeField.border:
schemeBorder = color;
case VsCodeColorSchemeField.input:
schemeInput = color;
case VsCodeColorSchemeField.ring:
schemeRing = color;
case VsCodeColorSchemeField.mutedForeground:
schemeMutedForeground = color;
case VsCodeColorSchemeField.accent:
schemeAccent = color;
}
}
}

if (editor.background != workbench.editorBackground) {
editor = editor.copyWith(background: workbench.editorBackground);
}

var colorScheme = QueryaTheme.colorSchemeFromWorkbench(
workbench,
brightness: brightness,
);

final editorForegroundChanged =
editor.foreground != base.editor.foreground;
if (schemeForeground != null || editorForegroundChanged) {
final fg = schemeForeground ?? editor.foreground;
colorScheme = colorScheme.copyWith(
foreground: () => fg,
cardForeground: () => fg,
popoverForeground: () => fg,
);
}
if (schemeBackground != null) {
colorScheme = colorScheme.copyWith(background: () => schemeBackground!);
}
if (schemeCard != null) {
colorScheme = colorScheme.copyWith(
card: () => schemeCard!,
popover: () => schemeCard!,
);
}
if (schemeBorder != null) {
colorScheme = colorScheme.copyWith(border: () => schemeBorder!);
}
if (schemeInput != null) {
colorScheme = colorScheme.copyWith(input: () => schemeInput!);
}
if (schemeRing != null) {
colorScheme = colorScheme.copyWith(ring: () => schemeRing!);
}
if (schemeMutedForeground != null) {
colorScheme = colorScheme.copyWith(
mutedForeground: () => schemeMutedForeground!,
);
}
if (schemeAccent != null) {
colorScheme = colorScheme.copyWith(accent: () => schemeAccent!);
}

return QueryaTheme(
workbench: workbench,
editor: editor,
brightness: brightness,
colorScheme: colorScheme,
);
}

QueryaTheme _defaultFallbackFor(VsCodeThemeManifest manifest) {
if (manifest.isLight) return QueryaTheme.lightDefault;
if (manifest.isDark) return QueryaTheme.darkDefault;
return QueryaTheme.darkDefault;
}

Brightness _brightnessFrom(VsCodeThemeManifest manifest, QueryaTheme base) {
if (manifest.isLight) return Brightness.light;
if (manifest.isDark) return Brightness.dark;
return base.brightness;
}

QueryaWorkbenchTheme _applyWorkbenchField(
QueryaWorkbenchTheme w,
VsCodeWorkbenchField field,
Color color,
) {
switch (field) {
case VsCodeWorkbenchField.canvas:
return w.copyWith(canvas: color);
case VsCodeWorkbenchField.surface:
return w.copyWith(surface: color);
case VsCodeWorkbenchField.sidebarBackground:
return w.copyWith(sidebarBackground: color);
case VsCodeWorkbenchField.editorBackground:
return w.copyWith(editorBackground: color);
case VsCodeWorkbenchField.borderSubtle:
return w.copyWith(borderSubtle: color);
case VsCodeWorkbenchField.accent:
return w.copyWith(accent: color);
case VsCodeWorkbenchField.mutedForeground:
return w.copyWith(mutedForeground: color);
case VsCodeWorkbenchField.gitModified:
return w.copyWith(gitModified: color);
case VsCodeWorkbenchField.gitUntracked:
return w.copyWith(gitUntracked: color);
}
}

QueryaEditorTheme _applyEditorField(
QueryaEditorTheme e,
VsCodeEditorField field,
Color color,
) {
switch (field) {
case VsCodeEditorField.background:
return e.copyWith(background: color);
case VsCodeEditorField.foreground:
return e.copyWith(foreground: color);
}
}
106 changes: 106 additions & 0 deletions lib/core/theme/parser/vscode_color_map.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
// Supported VS Code `colors` keys → Querya workbench / editor tokens.
// Unknown keys are ignored; see kSupportedVsCodeColorKeys and docs/theme-import.md.

/// Workbench token updated from a VS Code color key.
enum VsCodeWorkbenchField {
canvas,
surface,
sidebarBackground,
editorBackground,
borderSubtle,
accent,
mutedForeground,
gitModified,
gitUntracked,
}

/// Editor token updated from a VS Code color key.
enum VsCodeEditorField {
background,
foreground,
}

/// Optional direct [ColorScheme] fields (shadcn) beyond workbench derivation.
enum VsCodeColorSchemeField {
foreground,
background,
card,
border,
input,
ring,
mutedForeground,
accent,
}

/// Maps one VS Code `colors` entry to Querya tokens.
class VsCodeColorTarget {
const VsCodeColorTarget.workbench(this.workbench)
: editor = null,
colorScheme = null;

const VsCodeColorTarget.editor(this.editor)
: workbench = null,
colorScheme = null;

const VsCodeColorTarget.scheme(this.colorScheme)
: workbench = null,
editor = null;

final VsCodeWorkbenchField? workbench;
final VsCodeEditorField? editor;
final VsCodeColorSchemeField? colorScheme;
}

/// VS Code key → Querya target. Keys not listed are ignored.
const Map<String, VsCodeColorTarget> kVsCodeColorMap = {
'editor.background': VsCodeColorTarget.workbench(
VsCodeWorkbenchField.editorBackground,
),
'editor.foreground': VsCodeColorTarget.editor(VsCodeEditorField.foreground),
'sideBar.background': VsCodeColorTarget.workbench(
VsCodeWorkbenchField.sidebarBackground,
),
'sideBar.foreground': VsCodeColorTarget.workbench(
VsCodeWorkbenchField.mutedForeground,
),
'activityBar.background': VsCodeColorTarget.workbench(
VsCodeWorkbenchField.canvas,
),
'tab.activeBackground': VsCodeColorTarget.workbench(
VsCodeWorkbenchField.surface,
),
'statusBar.background': VsCodeColorTarget.workbench(
VsCodeWorkbenchField.canvas,
),
'panel.background': VsCodeColorTarget.workbench(
VsCodeWorkbenchField.surface,
),
'focusBorder': VsCodeColorTarget.workbench(VsCodeWorkbenchField.accent),
'input.background': VsCodeColorTarget.workbench(VsCodeWorkbenchField.surface),
'list.hoverBackground': VsCodeColorTarget.scheme(
VsCodeColorSchemeField.accent,
),
'gitDecoration.modifiedResourceForeground': VsCodeColorTarget.workbench(
VsCodeWorkbenchField.gitModified,
),
'gitDecoration.untrackedResourceForeground': VsCodeColorTarget.workbench(
VsCodeWorkbenchField.gitUntracked,
),
};

/// Documented subset of supported VS Code keys (stable API).
const List<String> kSupportedVsCodeColorKeys = [
'editor.background',
'editor.foreground',
'sideBar.background',
'sideBar.foreground',
'activityBar.background',
'tab.activeBackground',
'statusBar.background',
'panel.background',
'focusBorder',
'input.background',
'list.hoverBackground',
'gitDecoration.modifiedResourceForeground',
'gitDecoration.untrackedResourceForeground',
];
3 changes: 2 additions & 1 deletion test/core/theme/parser/color_parser_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@ void main() {
});

test('8-digit RRGGBBAA', () {
expect(parseVsCodeColor('#11223344').alpha, 0x44);
final c = parseVsCodeColor('#11223344');
expect((c.a * 255).round(), 0x44);
});

test('3-digit shorthand', () {
Expand Down
Loading
Loading