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} + +
+ + + + + + + + + {#each renderRows as row} + + {@render countColumn(row, CountColumnSide.Before)} + {@render countColumn(row, CountColumnSide.After)} + + + {/each} + +
+
+ + {`@@ -${hunkLineInfo.beforLineStart},${hunkLineInfo.beforeLineCount} +${hunkLineInfo.afterLineStart},${hunkLineInfo.afterLineCount} @@`} + +
+
+ {@html row.tokens.join('')} +
+
+ + 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} -
-
-

- @@ -{hunk.header.oldStart},{hunk.header.oldLength} +{hunk.header.newStart},{hunk.header - .newLength} @@ -

-
-
-
- {#each hunk.lines as line} -
-
{line.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)))