Skip to content
Closed
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
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,13 @@ All notable changes to AngKorGit are documented here. The format follows

## [Unreleased]

### Added
- **More languages in the diff highlighter.** Diffs for Less, SCSS, Dockerfiles,
Makefiles, CMake, `.ini` / `.properties`, and `.editorconfig` pick up highlight.js
grammars instead of rendering as plain text. Common aliases (`cts`, `pyi`, `mdx`,
`kts`, …) and basenames (`Dockerfile`, `Makefile`, `CMakeLists.txt`) map to the
right grammar. CSS selector token classes are coloured so style diffs are readable.

## [0.18.0] — 2026-09-24

The review release. AI can now review one file from its diff or a whole commit from the
Expand Down
2 changes: 1 addition & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ craftsmanship from Cambodia 🇰🇭.
| Animation | **Framer Motion 11** (subtle; splash logo draw) | |
| Router | react-router-dom 6 (MemoryRouter: `/welcome`, `/repo`) | |
| Icons / fonts | lucide-react · Inter Variable + JetBrains Mono (self-hosted @fontsource) | |
| Key libs | @tanstack/react-virtual, react-resizable-panels, cmdk, sonner, @xterm/xterm, highlight.js (lib/core + 20 langs) | |
| Key libs | @tanstack/react-virtual, react-resizable-panels, cmdk, sonner, @xterm/xterm, highlight.js (lib/core + 26 langs) | |
| Rust deps | tauri-plugin-dialog/opener, portable-pty, keyring 3, notify-debouncer-mini, reqwest (rustls), which 7, base64, thiserror | |

## 3. Monorepo layout (pnpm workspaces)
Expand Down
5 changes: 3 additions & 2 deletions apps/desktop/src/app/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -63,8 +63,9 @@ body {
.hljs-number { color: hsl(var(--syntax-number)); }
.hljs-comment { color: hsl(var(--faint-foreground)); font-style: italic; }
.hljs-title, .hljs-function { color: hsl(var(--syntax-function)); }
.hljs-attr, .hljs-property, .hljs-variable { color: hsl(var(--syntax-variable)); }
.hljs-type, .hljs-class { color: hsl(var(--syntax-type)); }
.hljs-attr, .hljs-attribute, .hljs-property, .hljs-variable, .hljs-selector-attr { color: hsl(var(--syntax-variable)); }
.hljs-type, .hljs-class, .hljs-selector-tag, .hljs-selector-class, .hljs-selector-id { color: hsl(var(--syntax-type)); }
.hljs-selector-pseudo { color: hsl(var(--syntax-keyword)); }

/* xterm fills its panel */
.terminal-host .xterm {
Expand Down
74 changes: 74 additions & 0 deletions apps/desktop/src/core/demo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,9 @@ export const demoRepo: RepositoryInfo = {
mainPath: null,
};

export const DEMO_LESS_PATH = 'styles/theme.less';
export const DEMO_DOCKERFILE_PATH = 'Dockerfile';

export const demoRecents: RecentRepository[] = [
{ path: '/Users/demo/projects/angkorgit', name: 'angkorgit', lastOpenedAt: 1754200000 },
{ path: '/Users/demo/projects/temple-ui', name: 'temple-ui', lastOpenedAt: 1754100000 },
Expand Down Expand Up @@ -182,6 +185,8 @@ export const demoStatus: StatusSummary = {
unstaged: 'untracked',
},
{ path: 'src/old-layout.tsx', origPath: null, staged: 'deleted', unstaged: null },
{ path: DEMO_LESS_PATH, origPath: null, staged: null, unstaged: 'modified' },
{ path: DEMO_DOCKERFILE_PATH, origPath: null, staged: null, unstaged: 'untracked' },
],
branch: 'main',
ahead: 2,
Expand Down Expand Up @@ -402,8 +407,77 @@ export const demoLargeFileDiff: FileDiff = {
],
};

function demoStatusDiff(
path: string,
status: FileDiff['status'],
rows: Array<readonly [' ' | '-' | '+', string]>,
): FileDiff {
let oldNo = status === 'new' ? 0 : 1;
let newNo = status === 'deleted' ? 0 : 1;
let additions = 0;
let deletions = 0;
let context = 0;
const lines = rows.map(([mark, content]) => {
if (mark === '-') {
deletions += 1;
return { kind: 'deletion' as const, oldLineNo: oldNo++, newLineNo: null, content };
}
if (mark === '+') {
additions += 1;
return { kind: 'addition' as const, oldLineNo: null, newLineNo: newNo++, content };
}
context += 1;
return { kind: 'context' as const, oldLineNo: oldNo++, newLineNo: newNo++, content };
});
const oldLines = deletions + context;
const newLines = additions + context;
const oldStart = oldLines === 0 ? 0 : 1;
const newStart = newLines === 0 ? 0 : 1;
return {
path,
oldPath: null,
status,
isBinary: false,
isImage: false,
oldImage: null,
newImage: null,
additions,
deletions,
hunks: [
{
header: `@@ -${oldStart},${oldLines} +${newStart},${newLines} @@`,
oldStart,
oldLines,
newStart,
newLines,
lines,
},
],
};
}

export function demoFileDiffFor(path: string): FileDiff {
if (path === demoLargeFileDiff.path) return demoLargeFileDiff;
if (path === DEMO_LESS_PATH) {
return demoStatusDiff(path, 'modified', [
[' ', '@accent: #2563eb;'],
['-', '.btn { color: #111; }'],
['+', '.btn {'],
['+', ' color: @accent;'],
['+', ' &:hover { color: darken(@accent, 8%); }'],
['+', '}'],
]);
}
if (path === DEMO_DOCKERFILE_PATH) {
return demoStatusDiff(path, 'new', [
['+', 'FROM node:20-alpine'],
['+', 'WORKDIR /app'],
['+', 'COPY package.json bun.lock ./'],
['+', 'RUN bun install --frozen-lockfile'],
['+', 'COPY . .'],
['+', 'CMD ["bun", "run", "dev"]'],
]);
}
return { ...demoFileDiff, path };
}

Expand Down
53 changes: 50 additions & 3 deletions apps/desktop/src/shared/highlight.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import java from 'highlight.js/lib/languages/java';
import csharp from 'highlight.js/lib/languages/csharp';
import cpp from 'highlight.js/lib/languages/cpp';
import css from 'highlight.js/lib/languages/css';
import less from 'highlight.js/lib/languages/less';
import scss from 'highlight.js/lib/languages/scss';
import xml from 'highlight.js/lib/languages/xml';
import json from 'highlight.js/lib/languages/json';
import yaml from 'highlight.js/lib/languages/yaml';
Expand All @@ -18,6 +20,11 @@ import ruby from 'highlight.js/lib/languages/ruby';
import php from 'highlight.js/lib/languages/php';
import kotlin from 'highlight.js/lib/languages/kotlin';
import swift from 'highlight.js/lib/languages/swift';
import ini from 'highlight.js/lib/languages/ini';
import properties from 'highlight.js/lib/languages/properties';
import dockerfile from 'highlight.js/lib/languages/dockerfile';
import makefile from 'highlight.js/lib/languages/makefile';
import cmake from 'highlight.js/lib/languages/cmake';

hljs.registerLanguage('typescript', typescript);
hljs.registerLanguage('javascript', javascript);
Expand All @@ -28,6 +35,8 @@ hljs.registerLanguage('java', java);
hljs.registerLanguage('csharp', csharp);
hljs.registerLanguage('cpp', cpp);
hljs.registerLanguage('css', css);
hljs.registerLanguage('less', less);
hljs.registerLanguage('scss', scss);
hljs.registerLanguage('xml', xml);
hljs.registerLanguage('json', json);
hljs.registerLanguage('yaml', yaml);
Expand All @@ -38,17 +47,32 @@ hljs.registerLanguage('ruby', ruby);
hljs.registerLanguage('php', php);
hljs.registerLanguage('kotlin', kotlin);
hljs.registerLanguage('swift', swift);
hljs.registerLanguage('ini', ini);
hljs.registerLanguage('properties', properties);
hljs.registerLanguage('dockerfile', dockerfile);
hljs.registerLanguage('makefile', makefile);
hljs.registerLanguage('cmake', cmake);

const BASENAME_TO_LANG: Record<string, string> = {
dockerfile: 'dockerfile',
makefile: 'makefile',
gnumakefile: 'makefile',
'cmakelists.txt': 'cmake',
'.editorconfig': 'ini',
};

const EXT_TO_LANG: Record<string, string> = {
ts: 'typescript',
tsx: 'typescript',
mts: 'typescript',
cts: 'typescript',
js: 'javascript',
jsx: 'javascript',
mjs: 'javascript',
cjs: 'javascript',
rs: 'rust',
py: 'python',
pyi: 'python',
go: 'go',
java: 'java',
cs: 'csharp',
Expand All @@ -57,9 +81,15 @@ const EXT_TO_LANG: Record<string, string> = {
cc: 'cpp',
cpp: 'cpp',
hpp: 'cpp',
hh: 'cpp',
hxx: 'cpp',
cxx: 'cpp',
ino: 'cpp',
css: 'css',
scss: 'css',
less: 'less',
scss: 'scss',
html: 'xml',
htm: 'xml',
svg: 'xml',
xml: 'xml',
vue: 'xml',
Expand All @@ -70,16 +100,31 @@ const EXT_TO_LANG: Record<string, string> = {
zsh: 'bash',
bash: 'bash',
md: 'markdown',
markdown: 'markdown',
mdx: 'markdown',
sql: 'sql',
rb: 'ruby',
php: 'php',
kt: 'kotlin',
kts: 'kotlin',
swift: 'swift',
ini: 'ini',
properties: 'properties',
cmake: 'cmake',
};

function fileName(path: string): string {
const slash = Math.max(path.lastIndexOf('/'), path.lastIndexOf('\\'));
return (slash >= 0 ? path.slice(slash + 1) : path).toLowerCase();
}

export function languageOf(path: string): string | null {
const ext = path.split('.').pop()?.toLowerCase() ?? '';
return EXT_TO_LANG[ext] ?? null;
const name = fileName(path);
const byName = BASENAME_TO_LANG[name];
if (byName) return byName;
const dot = name.lastIndexOf('.');
if (dot <= 0) return null;
return EXT_TO_LANG[name.slice(dot + 1)] ?? null;
}

const MAX_HIGHLIGHT_LENGTH = 5000;
Expand All @@ -94,6 +139,8 @@ const BLOCK_COMMENT_OPENERS: Record<string, string> = {
csharp: '/*',
cpp: '/*',
css: '/*',
less: '/*',
scss: '/*',
kotlin: '/*',
swift: '/*',
php: '/*',
Expand Down
2 changes: 1 addition & 1 deletion tests/e2e/smoke.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1890,7 +1890,7 @@ test('the All files view shows the whole working tree with changed files still a
await expect(inspector.getByText('README.md')).toHaveCount(0);

await page.getByRole('button', { name: 'All files' }).click();
await expect(inspector.getByText('6 changed')).toBeVisible();
await expect(inspector.getByText('8 changed')).toBeVisible();
await expect(inspector.getByText('README.md')).toBeVisible();
await expect(inspector.getByLabel('Stage src/core/ipc.ts')).toBeVisible();
await expect(inspector.getByLabel('Unstage src/features/graph/CommitGraph.tsx')).toBeVisible();
Expand Down
25 changes: 25 additions & 0 deletions tests/unit/demoHighlightFixtures.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { describe, expect, it } from 'vitest';
import {
DEMO_DOCKERFILE_PATH,
DEMO_LESS_PATH,
demoFileDiffFor,
demoStatus,
} from '../../apps/desktop/src/core/demo';

describe('demo highlight fixtures', () => {
it('keeps less and Dockerfile samples on the main demo working copy', () => {
const paths = demoStatus.files.map((f) => f.path);
expect(paths).toContain(DEMO_LESS_PATH);
expect(paths).toContain(DEMO_DOCKERFILE_PATH);
});

it('serves highlightable diffs for less and dockerfile', () => {
const less = demoFileDiffFor(DEMO_LESS_PATH);
expect(less.hunks[0]?.header).toBe('@@ -1,2 +1,5 @@');
expect(less.hunks[0]?.lines.some((l) => l.content.includes('@accent'))).toBe(true);
expect(demoFileDiffFor(DEMO_DOCKERFILE_PATH).status).toBe('new');
expect(
demoFileDiffFor(DEMO_DOCKERFILE_PATH).hunks[0]?.lines.some((l) => l.content.startsWith('FROM ')),
).toBe(true);
});
});
44 changes: 43 additions & 1 deletion tests/unit/highlight.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { describe, expect, it } from 'vitest';
import { highlightLineState, supportsBlockComments } from '@/shared/highlight';
import { highlightLineState, languageOf, supportsBlockComments } from '@/shared/highlight';

const text = (html: string) => html.replace(/<[^>]+>/g, '');
const wholeLineIsComment = (html: string) =>
Expand Down Expand Up @@ -63,6 +63,48 @@ describe('highlightLineState', () => {
expect(highlightLineState('done -->', 'xml', true).endsInComment).toBe(false);
});

it('highlights a less variable and selector', () => {
expect(languageOf('styles/theme.less')).toBe('less');
expect(supportsBlockComments('less')).toBe(true);
const variable = highlightLineState('@color: #fff;', 'less');
expect(variable.html).toContain('hljs-variable');
expect(variable.html).toContain('hljs-number');
const rule = highlightLineState('.btn { color: red; }', 'less');
expect(rule.html).toContain('hljs-selector-class');
expect(rule.html).toContain('hljs-attribute');
});

it('maps aliases and basenames for common config and style files', () => {
expect(languageOf('src/util.cts')).toBe('typescript');
expect(languageOf('readme.markdown')).toBe('markdown');
expect(languageOf('types/foo.pyi')).toBe('python');
expect(languageOf('App.kts')).toBe('kotlin');
expect(languageOf('board.ino')).toBe('cpp');
expect(languageOf('index.htm')).toBe('xml');
expect(languageOf('styles/app.scss')).toBe('scss');
expect(languageOf('config/app.ini')).toBe('ini');
expect(languageOf('messages.properties')).toBe('properties');
expect(languageOf('repo/.gitignore')).toBeNull();
expect(languageOf('.gitattributes')).toBeNull();
expect(languageOf('.editorconfig')).toBe('ini');
expect(languageOf('nginx.conf')).toBeNull();
expect(languageOf('app.cfg')).toBeNull();
expect(languageOf('Dockerfile')).toBe('dockerfile');
expect(languageOf('path/Makefile')).toBe('makefile');
expect(languageOf('CMakeLists.txt')).toBe('cmake');
expect(languageOf('tool.cmake')).toBe('cmake');
});

it('highlights scss variables and dockerfile keywords', () => {
expect(supportsBlockComments('scss')).toBe(true);
const scssLine = highlightLineState('$color: #fff;', 'scss');
expect(scssLine.html).toContain('hljs-variable');
const docker = highlightLineState('FROM node:20-alpine', 'dockerfile');
expect(docker.html).toContain('hljs-keyword');
const editorconfig = highlightLineState('[*]', 'ini');
expect(editorconfig.html).toContain('hljs-section');
});

it('ignores the continuation flag for languages without block comments', () => {
expect(supportsBlockComments('python')).toBe(false);
const line = highlightLineState('x = 1', 'python', true);
Expand Down
Loading