diff --git a/apps/desktop/package.json b/apps/desktop/package.json
index 0e08219b08..e531c84c0f 100644
--- a/apps/desktop/package.json
+++ b/apps/desktop/package.json
@@ -18,28 +18,10 @@
},
"devDependencies": {
"@anthropic-ai/sdk": "^0.27.3",
- "@codemirror/lang-cpp": "^6.0.2",
- "@codemirror/lang-css": "^6.2.1",
- "@codemirror/lang-html": "^6.4.9",
- "@codemirror/lang-java": "^6.0.1",
- "@codemirror/lang-javascript": "^6.2.2",
- "@codemirror/lang-json": "^6.0.1",
- "@codemirror/lang-markdown": "^6.2.5",
- "@codemirror/lang-php": "^6.0.1",
- "@codemirror/lang-python": "^6.1.6",
- "@codemirror/lang-rust": "^6.0.1",
- "@codemirror/lang-vue": "^0.1.3",
- "@codemirror/lang-wast": "^6.0.2",
- "@codemirror/lang-xml": "^6.1.0",
- "@codemirror/language": "^6.10.2",
- "@codemirror/legacy-modes": "^6.4.0",
"@gitbutler/shared": "workspace:*",
"@gitbutler/ui": "workspace:*",
- "@lezer/common": "^1.2.1",
- "@lezer/highlight": "^1.2.0",
"@octokit/rest": "^20.1.1",
"@reduxjs/toolkit": "catalog:redux",
- "@replit/codemirror-lang-svelte": "^6.0.0",
"@sentry/sveltekit": "catalog:svelte",
"@sveltejs/adapter-static": "catalog:svelte",
"@sveltejs/kit": "catalog:svelte",
diff --git a/apps/desktop/src/components/HunkDiff.svelte b/apps/desktop/src/components/HunkDiff.svelte
index 694b9f6791..2364967602 100644
--- a/apps/desktop/src/components/HunkDiff.svelte
+++ b/apps/desktop/src/components/HunkDiff.svelte
@@ -2,13 +2,13 @@
import ScrollableContainer from '$components/ScrollableContainer.svelte';
import { SelectedOwnership } from '$lib/branches/ownership';
import { type Hunk } from '$lib/hunks/hunk';
- import { create } from '$lib/utils/codeHighlight';
import {
type ContentSection,
SectionType,
type Line,
CountColumnSide
} from '$lib/utils/fileSections';
+ import { create } from '@gitbutler/shared/codeHighlight';
import { maybeGetContextStore } from '@gitbutler/shared/context';
import Checkbox from '@gitbutler/ui/Checkbox.svelte';
import Icon from '@gitbutler/ui/Icon.svelte';
diff --git a/apps/desktop/tsconfig.json b/apps/desktop/tsconfig.json
index d4c4e321ff..5f62df7dc3 100644
--- a/apps/desktop/tsconfig.json
+++ b/apps/desktop/tsconfig.json
@@ -36,6 +36,8 @@
"src/**/*.svelte",
"e2e/tests/**/*.js",
"e2e/tests/**/*.ts",
- "e2e/tests/**/*.svelte"
+ "e2e/tests/**/*.svelte",
+ "../../packages/shared/src/lib/codeHighlight.ts",
+ "../../packages/shared/src/lib/codeHighlight.ts"
]
}
diff --git a/apps/web/package.json b/apps/web/package.json
index f261b4513e..ec882f9701 100644
--- a/apps/web/package.json
+++ b/apps/web/package.json
@@ -26,7 +26,9 @@
"svelte": "catalog:svelte",
"svelte-check": "catalog:svelte",
"svelte-french-toast": "^1.2.0",
- "vite": "catalog:"
+ "vite": "catalog:",
+ "diff-match-patch": "^1.0.5",
+ "@types/diff-match-patch": "^1.0.36"
},
"dependencies": {
"@ethercorps/sveltekit-og": "^3.0.0",
diff --git a/apps/web/src/lib/components/HunkDiff.svelte b/apps/web/src/lib/components/HunkDiff.svelte
new file mode 100644
index 0000000000..5ef8f343ea
--- /dev/null
+++ b/apps/web/src/lib/components/HunkDiff.svelte
@@ -0,0 +1,600 @@
+
+
+{#snippet countColumn(row: Row, side: CountColumnSide)}
+
+ {side === CountColumnSide.Before ? row.beforeLineNumber : row.afterLineNumber}
+ |
+{/snippet}
+
+
+
+
diff --git a/apps/web/src/lib/components/review/DiffSection.svelte b/apps/web/src/lib/components/review/DiffSection.svelte
index 3d72cf5f3a..08b1b54224 100644
--- a/apps/web/src/lib/components/review/DiffSection.svelte
+++ b/apps/web/src/lib/components/review/DiffSection.svelte
@@ -1,5 +1,6 @@
{section.newPath}
-
- {#each parsedHunks as hunk}
-
-
-
-
- {#each hunk.lines as line}
-
- {/each}
-
-
-
+ {#each hunks as hunk}
+
{/each}
@@ -66,80 +39,4 @@
font-weight: var(--weight-regular, 400);
line-height: 160%; /* 19.2px */
}
-
- .diff {
- display: flex;
- flex-direction: column;
- align-items: flex-start;
- align-self: stretch;
-
- border-radius: var(--m, 6px);
- border: 1px solid var(--diff-count-border, #d4d0ce);
-
- & pre {
- color: var(--text-1, #1a1614);
- font-family: 'Geist Mono';
- font-size: 12px;
- font-style: normal;
- font-weight: 400;
- line-height: 120%; /* 14.4px */
- padding: 2px 6px;
-
- width: fit-content;
- background: none;
- }
- }
-
- .diff-header {
- display: flex;
- padding: 4px 6px;
- align-items: center;
- gap: 10px;
- flex: 1 0 0;
- align-self: stretch;
- background: var(--bg-1, #fff);
- }
-
- .diff-header-text {
- color: var(--text-2, #867e79);
- font-family: 'Geist Mono';
- font-size: 12px;
- font-style: normal;
- font-weight: 400;
- line-height: 120%; /* 14.4px */
- }
-
- .diff-content-wrapper {
- display: flex;
- flex-direction: column;
- align-items: flex-start;
- align-self: stretch;
-
- width: 100%;
- overflow-x: scroll;
-
- scrollbar-width: none;
- &::-webkit-scrollbar {
- display: none;
- }
- }
-
- .diff-content {
- min-width: 100%;
- border-top: 1px solid var(--diff-count-border, #d4d0ce);
- }
-
- .diff-line-added {
- width: 100%;
- background: var(--clr-diff-addition-count-bg);
- }
-
- .diff-line-removed {
- width: 100%;
- background: var(--clr-diff-deletion-count-bg);
- }
-
- .diff-line-unchanged {
- width: 100%;
- }
diff --git a/apps/web/src/lib/diff/parser.ts b/apps/web/src/lib/diff/parser.ts
deleted file mode 100644
index c1140a0126..0000000000
--- a/apps/web/src/lib/diff/parser.ts
+++ /dev/null
@@ -1,129 +0,0 @@
-interface DiffChunk {
- header: string;
- lines: string[];
-}
-
-function getDiffChunks(diff: string): DiffChunk[] {
- const lines = diff.split('\n');
- const chunks: DiffChunk[] = [];
- let chunk: DiffChunk | null = null;
- let line: string | null = null;
-
- for (let i = 0; i < lines.length; i++) {
- line = lines[i];
-
- if (line.startsWith('diff')) {
- if (chunk) {
- chunks.push(chunk);
- }
-
- chunk = {
- header: line,
- lines: []
- };
-
- continue;
- }
-
- if (line.startsWith('@@')) {
- if (chunk) {
- chunks.push(chunk);
- }
-
- chunk = {
- header: line,
- lines: []
- };
-
- continue;
- }
-
- if (line.startsWith('---') || line.startsWith('+++')) {
- // Ignore
- continue;
- }
-
- if (line.startsWith('index')) {
- // Ignore
- continue;
- }
-
- if (chunk) {
- chunk.lines.push(line);
- }
- }
-
- if (chunk) {
- chunks.push(chunk);
- }
-
- return chunks;
-}
-
-interface ParsedDiffHeader {
- oldStart: number;
- oldLength: number;
- newStart: number;
- newLength: number;
-}
-
-function parseHeader(rawHeader: string): ParsedDiffHeader {
- const parts = rawHeader.split(' ');
- const oldStart = parts[0].split(',')[0].slice(1);
- const oldLength = parts[0].split(',')[1];
- const newStart = parts[1].split(',')[0];
- const newLength = parts[1].split(',')[1];
-
- return {
- oldStart: parseInt(oldStart, 10),
- oldLength: parseInt(oldLength, 10),
- newStart: parseInt(newStart, 10),
- newLength: parseInt(newLength, 10)
- };
-}
-
-export type DiffLineType = 'add' | 'remove' | 'context';
-
-interface ParsedDiffLine {
- type: DiffLineType;
- line: string;
-}
-
-interface ParsedDiffHunk {
- header: ParsedDiffHeader;
- lines: ParsedDiffLine[];
-}
-export function parseDiff(diff: string | undefined): ParsedDiffHunk[] {
- if (!diff) {
- return [];
- }
-
- const chunks = getDiffChunks(diff);
- const parsedChunks: ParsedDiffHunk[] = [];
- for (const chunk of chunks) {
- if (!chunk.header.startsWith('@@')) {
- continue;
- }
-
- const rawHeader = chunk.header.split('@@')[1].trim();
- const parsedHeader = parseHeader(rawHeader);
- const lines: ParsedDiffLine[] = [];
-
- for (const rawLine of chunk.lines) {
- const type = rawLine[0];
- const line = rawLine.slice(1);
-
- lines.push({
- type: type === '+' ? 'add' : type === '-' ? 'remove' : 'context',
- line
- });
- }
-
- parsedChunks.push({
- header: parsedHeader,
- lines
- });
- }
-
- return parsedChunks;
-}
diff --git a/apps/web/src/lib/diffParsing.ts b/apps/web/src/lib/diffParsing.ts
new file mode 100644
index 0000000000..564922b91f
--- /dev/null
+++ b/apps/web/src/lib/diffParsing.ts
@@ -0,0 +1,109 @@
+export enum SectionType {
+ AddedLines,
+ RemovedLines,
+ Context
+}
+
+export enum CountColumnSide {
+ Before,
+ After
+}
+
+export type Line = {
+ readonly beforeLineNumber?: number;
+ readonly afterLineNumber?: number;
+ readonly content: string;
+};
+
+export type ContentSection = {
+ readonly lines: Line[];
+ readonly sectionType: SectionType;
+};
+
+export type Hunk = {
+ readonly oldStart: number;
+ readonly newStart: number;
+ readonly contentSections: ContentSection[];
+};
+
+const headerRegex =
+ /@@ -(?\d+),?(?\d+)? \+(?\d+),?(?\d+) @@(?.+)?/;
+function parseHeader(header: string): { oldStart: number; newStart: number } {
+ const result = headerRegex.exec(header);
+ if (!result?.groups) {
+ throw new Error('Failed to parse diff header');
+ }
+ return {
+ oldStart: parseInt(result.groups['beforeStart']),
+ newStart: parseInt(result.groups['afterStart'])
+ };
+}
+
+function lineType(line: string): SectionType {
+ if (line.startsWith('+')) {
+ return SectionType.AddedLines;
+ } else if (line.startsWith('-')) {
+ return SectionType.RemovedLines;
+ } else {
+ return SectionType.Context;
+ }
+}
+
+export function parsePatch(patch: string) {
+ const lines = patch.trim().split('\n');
+ console.log(lines);
+
+ const hunks = [];
+ let currentHunk: Hunk | undefined;
+ // These zero values will never get used in practice.
+ let lastBefore = 0;
+ let lastAfter = 0;
+
+ for (const line of lines) {
+ if (line.startsWith('@@')) {
+ currentHunk = {
+ ...parseHeader(line),
+ contentSections: []
+ };
+ hunks.push(currentHunk);
+ lastBefore = currentHunk.oldStart;
+ lastAfter = currentHunk.newStart;
+ continue;
+ }
+ if (!currentHunk) {
+ continue;
+ }
+
+ const type = lineType(line);
+ let lastSection = currentHunk.contentSections.at(-1);
+ if (!lastSection) {
+ lastSection = { lines: [], sectionType: type };
+ currentHunk.contentSections.push(lastSection);
+ }
+
+ // If the type has changed, we want to start a new section
+ if (lastSection.sectionType !== type) {
+ lastSection = { lines: [], sectionType: type };
+ currentHunk.contentSections.push(lastSection);
+ }
+
+ if (type === SectionType.AddedLines) {
+ lastAfter += 1;
+ lastSection.lines.push({ afterLineNumber: lastAfter, content: line.slice(1) });
+ } else if (type === SectionType.RemovedLines) {
+ lastBefore += 1;
+ lastSection.lines.push({ beforeLineNumber: lastBefore, content: line.slice(1) });
+ } else {
+ lastAfter += 1;
+ lastBefore += 1;
+ lastSection.lines.push({
+ afterLineNumber: lastAfter,
+ beforeLineNumber: lastBefore,
+ content: line.slice(1)
+ });
+ }
+ }
+
+ console.log(hunks);
+ return hunks;
+}
diff --git a/apps/web/svelte.config.js b/apps/web/svelte.config.js
index bbbd031873..26f1fcab1d 100644
--- a/apps/web/svelte.config.js
+++ b/apps/web/svelte.config.js
@@ -3,7 +3,7 @@ import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';
/** @type {import('@sveltejs/kit').Config} */
const config = {
- preprocess: vitePreprocess(),
+ preprocess: vitePreprocess({ script: true }),
kit: {
adapter: adapter(),
alias: {
diff --git a/apps/web/tsconfig.json b/apps/web/tsconfig.json
index 97f245c2f0..726e6432c2 100644
--- a/apps/web/tsconfig.json
+++ b/apps/web/tsconfig.json
@@ -1,7 +1,7 @@
{
"extends": "./.svelte-kit/tsconfig.json",
"compilerOptions": {
- "target": "es6",
+ "target": "ES2018",
"lib": ["dom", "dom.iterable", "ES2021"],
"allowJs": true,
"checkJs": true,
diff --git a/packages/shared/package.json b/packages/shared/package.json
index ae3702afa1..204a07accf 100644
--- a/packages/shared/package.json
+++ b/packages/shared/package.json
@@ -60,7 +60,25 @@
"svelte-check": "catalog:svelte",
"vite": "catalog:",
"vitest": "^2.0.5",
- "@reduxjs/toolkit": "catalog:redux"
+ "@reduxjs/toolkit": "catalog:redux",
+ "@codemirror/lang-cpp": "^6.0.2",
+ "@codemirror/lang-css": "^6.2.1",
+ "@codemirror/lang-html": "^6.4.9",
+ "@codemirror/lang-java": "^6.0.1",
+ "@codemirror/lang-javascript": "^6.2.2",
+ "@codemirror/lang-json": "^6.0.1",
+ "@codemirror/lang-markdown": "^6.2.5",
+ "@codemirror/lang-php": "^6.0.1",
+ "@codemirror/lang-python": "^6.1.6",
+ "@codemirror/lang-rust": "^6.0.1",
+ "@codemirror/lang-vue": "^0.1.3",
+ "@codemirror/lang-wast": "^6.0.2",
+ "@codemirror/lang-xml": "^6.1.0",
+ "@codemirror/language": "^6.10.2",
+ "@codemirror/legacy-modes": "^6.4.0",
+ "@lezer/common": "^1.2.1",
+ "@lezer/highlight": "^1.2.0",
+ "@replit/codemirror-lang-svelte": "^6.0.0"
},
"type": "module",
"dependencies": {
diff --git a/apps/desktop/src/lib/utils/codeHighlight.ts b/packages/shared/src/lib/codeHighlight.ts
similarity index 100%
rename from apps/desktop/src/lib/utils/codeHighlight.ts
rename to packages/shared/src/lib/codeHighlight.ts
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 293eb13091..c4d0fc312e 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -110,72 +110,18 @@ importers:
'@anthropic-ai/sdk':
specifier: ^0.27.3
version: 0.27.3
- '@codemirror/lang-cpp':
- specifier: ^6.0.2
- version: 6.0.2
- '@codemirror/lang-css':
- specifier: ^6.2.1
- version: 6.2.1(@codemirror/view@6.26.3)
- '@codemirror/lang-html':
- specifier: ^6.4.9
- version: 6.4.9
- '@codemirror/lang-java':
- specifier: ^6.0.1
- version: 6.0.1
- '@codemirror/lang-javascript':
- specifier: ^6.2.2
- version: 6.2.2
- '@codemirror/lang-json':
- specifier: ^6.0.1
- version: 6.0.1
- '@codemirror/lang-markdown':
- specifier: ^6.2.5
- version: 6.2.5
- '@codemirror/lang-php':
- specifier: ^6.0.1
- version: 6.0.1
- '@codemirror/lang-python':
- specifier: ^6.1.6
- version: 6.1.6(@codemirror/view@6.26.3)
- '@codemirror/lang-rust':
- specifier: ^6.0.1
- version: 6.0.1
- '@codemirror/lang-vue':
- specifier: ^0.1.3
- version: 0.1.3
- '@codemirror/lang-wast':
- specifier: ^6.0.2
- version: 6.0.2
- '@codemirror/lang-xml':
- specifier: ^6.1.0
- version: 6.1.0
- '@codemirror/language':
- specifier: ^6.10.2
- version: 6.10.2
- '@codemirror/legacy-modes':
- specifier: ^6.4.0
- version: 6.4.0
'@gitbutler/shared':
specifier: workspace:*
version: link:../../packages/shared
'@gitbutler/ui':
specifier: workspace:*
version: link:../../packages/ui
- '@lezer/common':
- specifier: ^1.2.1
- version: 1.2.1
- '@lezer/highlight':
- specifier: ^1.2.0
- version: 1.2.0
'@octokit/rest':
specifier: ^20.1.1
version: 20.1.1
'@reduxjs/toolkit':
specifier: catalog:redux
version: 2.5.0(react@18.3.1)
- '@replit/codemirror-lang-svelte':
- specifier: ^6.0.0
- version: 6.0.0(@codemirror/autocomplete@6.16.2(@codemirror/language@6.10.2)(@codemirror/state@6.4.1)(@codemirror/view@6.26.3)(@lezer/common@1.2.1))(@codemirror/lang-css@6.2.1(@codemirror/view@6.26.3))(@codemirror/lang-html@6.4.9)(@codemirror/lang-javascript@6.2.2)(@codemirror/language@6.10.2)(@codemirror/state@6.4.1)(@codemirror/view@6.26.3)(@lezer/common@1.2.1)(@lezer/highlight@1.2.0)(@lezer/javascript@1.4.16)(@lezer/lr@1.4.1)
'@sentry/sveltekit':
specifier: catalog:svelte
version: 8.51.0(@opentelemetry/api@1.9.0)(@opentelemetry/core@1.30.0(@opentelemetry/api@1.9.0))(@opentelemetry/instrumentation@0.56.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@1.30.0(@opentelemetry/api@1.9.0))(@opentelemetry/semantic-conventions@1.28.0)(@sveltejs/kit@2.16.1(@sveltejs/vite-plugin-svelte@4.0.4(svelte@5.19.2)(vite@5.4.14(@types/node@22.3.0)(sass-embedded@1.82.0)))(svelte@5.19.2)(vite@5.4.14(@types/node@22.3.0)(sass-embedded@1.82.0)))(svelte@5.19.2)(vite@5.4.14(@types/node@22.3.0)(sass-embedded@1.82.0))
@@ -396,9 +342,15 @@ importers:
'@sveltejs/vite-plugin-svelte':
specifier: catalog:svelte
version: 4.0.4(svelte@5.19.2)(vite@5.4.14(@types/node@22.3.0)(sass-embedded@1.82.0))
+ '@types/diff-match-patch':
+ specifier: ^1.0.36
+ version: 1.0.36
'@types/node':
specifier: ^22.3.0
version: 22.3.0
+ diff-match-patch:
+ specifier: ^1.0.5
+ version: 1.0.5
svelte:
specifier: catalog:svelte
version: 5.19.2
@@ -418,15 +370,69 @@ importers:
specifier: ^1.3.2
version: 1.3.2
devDependencies:
+ '@codemirror/lang-cpp':
+ specifier: ^6.0.2
+ version: 6.0.2
+ '@codemirror/lang-css':
+ specifier: ^6.2.1
+ version: 6.2.1(@codemirror/view@6.26.3)
+ '@codemirror/lang-html':
+ specifier: ^6.4.9
+ version: 6.4.9
+ '@codemirror/lang-java':
+ specifier: ^6.0.1
+ version: 6.0.1
+ '@codemirror/lang-javascript':
+ specifier: ^6.2.2
+ version: 6.2.2
+ '@codemirror/lang-json':
+ specifier: ^6.0.1
+ version: 6.0.1
+ '@codemirror/lang-markdown':
+ specifier: ^6.2.5
+ version: 6.2.5
+ '@codemirror/lang-php':
+ specifier: ^6.0.1
+ version: 6.0.1
+ '@codemirror/lang-python':
+ specifier: ^6.1.6
+ version: 6.1.6(@codemirror/view@6.26.3)
+ '@codemirror/lang-rust':
+ specifier: ^6.0.1
+ version: 6.0.1
+ '@codemirror/lang-vue':
+ specifier: ^0.1.3
+ version: 0.1.3
+ '@codemirror/lang-wast':
+ specifier: ^6.0.2
+ version: 6.0.2
+ '@codemirror/lang-xml':
+ specifier: ^6.1.0
+ version: 6.1.0
+ '@codemirror/language':
+ specifier: ^6.10.2
+ version: 6.10.2
+ '@codemirror/legacy-modes':
+ specifier: ^6.4.0
+ version: 6.4.0
'@csstools/postcss-bundler':
specifier: ^1.0.15
version: 1.0.15(postcss@8.4.39)
'@gitbutler/ui':
specifier: workspace:*
version: link:../ui
+ '@lezer/common':
+ specifier: ^1.2.1
+ version: 1.2.1
+ '@lezer/highlight':
+ specifier: ^1.2.0
+ version: 1.2.0
'@reduxjs/toolkit':
specifier: catalog:redux
version: 2.5.0(react@18.3.1)
+ '@replit/codemirror-lang-svelte':
+ specifier: ^6.0.0
+ version: 6.0.0(@codemirror/autocomplete@6.16.2(@codemirror/language@6.10.2)(@codemirror/state@6.4.1)(@codemirror/view@6.26.3)(@lezer/common@1.2.1))(@codemirror/lang-css@6.2.1(@codemirror/view@6.26.3))(@codemirror/lang-html@6.4.9)(@codemirror/lang-javascript@6.2.2)(@codemirror/language@6.10.2)(@codemirror/state@6.4.1)(@codemirror/view@6.26.3)(@lezer/common@1.2.1)(@lezer/highlight@1.2.0)(@lezer/javascript@1.4.16)(@lezer/lr@1.4.1)
'@sveltejs/adapter-static':
specifier: catalog:svelte
version: 3.0.8(@sveltejs/kit@2.16.1(@sveltejs/vite-plugin-svelte@4.0.4(svelte@5.19.2)(vite@5.4.14(@types/node@22.3.0)(sass-embedded@1.82.0)))(svelte@5.19.2)(vite@5.4.14(@types/node@22.3.0)(sass-embedded@1.82.0)))