use default differ component to render commit diff

This commit is contained in:
Nikita Galaiko 2023-04-28 09:40:52 +02:00
parent c6ec0d9b77
commit 7efdf0a5e7
20 changed files with 189 additions and 147 deletions

View File

@ -344,7 +344,7 @@ impl App {
pub fn git_wd_diff(
&self,
project_id: &str,
max_lines: usize,
context_lines: usize,
) -> Result<HashMap<String, String>> {
let project = self
.projects_storage
@ -353,7 +353,7 @@ impl App {
.ok_or_else(|| anyhow::anyhow!("project wd not found"))?;
let project_repository = project_repository::Repository::open(&project)
.context("failed to open project repository")?;
project_repository.git_wd_diff(max_lines)
project_repository.git_wd_diff(context_lines)
}
pub fn git_match_paths(&self, project_id: &str, pattern: &str) -> Result<Vec<String>> {

View File

@ -157,7 +157,7 @@ impl<'repository> Repository<'repository> {
Ok(statuses)
}
pub fn git_wd_diff(&self, max_lines: usize) -> Result<HashMap<String, String>> {
pub fn git_wd_diff(&self, context_lines: usize) -> Result<HashMap<String, String>> {
let head = self.git_repository.head()?;
let tree = head.peel_to_tree()?;
@ -166,6 +166,11 @@ impl<'repository> Repository<'repository> {
opts.recurse_untracked_dirs(true)
.include_untracked(true)
.show_untracked_content(true)
.context_lines(if context_lines == 0 {
3
} else {
context_lines.try_into()?
})
.include_ignored(true);
let diff = self
@ -192,14 +197,12 @@ impl<'repository> Repository<'repository> {
current_line_count = 0;
last_path = new_path.clone();
}
if current_line_count <= max_lines {
match line.origin() {
'+' | '-' | ' ' => results.push_str(&format!("{}", line.origin())),
_ => {}
}
results.push_str(&format!("{}", std::str::from_utf8(line.content()).unwrap()));
current_line_count += 1;
match line.origin() {
'+' | '-' | ' ' => results.push_str(&format!("{}", line.origin())),
_ => {}
}
results.push_str(&format!("{}", std::str::from_utf8(line.content()).unwrap()));
current_line_count += 1;
true
})?;
result.insert(last_path.clone(), results.clone());

View File

@ -334,10 +334,11 @@ async fn git_status(
async fn git_wd_diff(
handle: tauri::AppHandle,
project_id: &str,
context_lines: usize,
) -> Result<HashMap<String, String>, Error> {
let app = handle.state::<app::App>();
let diff = app
.git_wd_diff(project_id, 100)
.git_wd_diff(project_id, context_lines)
.with_context(|| format!("failed to get git wd diff for project {}", project_id))?;
Ok(diff)
}

View File

@ -2,8 +2,11 @@ import { invoke } from '$lib/ipc';
import { writable } from 'svelte/store';
import { sessions, git } from '$lib/api';
const list = (params: { projectId: string }) =>
invoke<Record<string, string>>('git_wd_diff', params);
const list = (params: { projectId: string; contextLines?: number }) =>
invoke<Record<string, string>>('git_wd_diff', {
projectId: params.projectId,
contextLines: params.contextLines ?? 10000
});
export const Diffs = async (params: { projectId: string }) => {
const store = writable(await list(params));

View File

@ -1,42 +0,0 @@
import { CharacterIdMap } from './characterIdMap';
import { diff_match_patch } from 'diff-match-patch';
export const charDiff = (
text1: string,
text2: string,
cleanup?: boolean
): { 0: number; 1: string }[] => {
const differ = new diff_match_patch();
const diff = differ.diff_main(text1, text2);
if (cleanup) {
differ.diff_cleanupSemantic(diff);
}
return diff;
};
export const lineDiff = (lines1: string[], lines2: string[]): DiffArray => {
const idMap = new CharacterIdMap<string>();
const text1 = lines1.map((line) => idMap.toChar(line)).join('');
const text2 = lines2.map((line) => idMap.toChar(line)).join('');
const diff = charDiff(text1, text2);
const lineDiff = [];
for (let i = 0; i < diff.length; i++) {
const lines = [];
for (let j = 0; j < diff[i][1].length; j++) {
lines.push(idMap.fromChar(diff[i][1][j]) || '');
}
lineDiff.push({ 0: diff[i][0], 1: lines });
}
return lineDiff;
};
export enum Operation {
Equal = 0,
Insert = 1,
Delete = -1,
Edit = 2
}
export type DiffArray = { 0: Operation; 1: string[] }[];

View File

@ -1,3 +0,0 @@
import { default as CodeViewer } from './CodeViewer.svelte';
export default CodeViewer;

View File

@ -0,0 +1,35 @@
<script lang="ts">
import { type Delta, Operation } from '$lib/api';
import { Differ } from '$lib/components';
import { lineDiff } from './Differ/diff';
export let doc: string;
export let deltas: Delta[];
export let filepath: string;
export let highlight: string[] = [];
export let paddingLines = 10000;
const applyDeltas = (text: string, deltas: Delta[]) => {
const operations = deltas.flatMap((delta) => delta.operations);
operations.forEach((operation) => {
if (Operation.isInsert(operation)) {
text =
text.slice(0, operation.insert[0]) +
operation.insert[1] +
text.slice(operation.insert[0]);
} else if (Operation.isDelete(operation)) {
text =
text.slice(0, operation.delete[0]) +
text.slice(operation.delete[0] + operation.delete[1]);
}
});
return text;
};
$: left = deltas.length > 0 ? applyDeltas(doc, deltas.slice(0, deltas.length - 1)) : doc;
$: right = deltas.length > 0 ? applyDeltas(left, deltas.slice(deltas.length - 1)) : left;
$: diff = lineDiff(left.split('\n'), right.split('\n'));
</script>
<Differ {diff} {filepath} {highlight} {paddingLines} />

View File

@ -1,56 +1,12 @@
<script lang="ts">
import { create } from '$lib/components/CodeViewer/CodeHighlighter';
import { parseDiff } from './Differ/diff';
import { Differ } from '$lib/components';
export let diff: string;
export let path: string;
export let paddingLines = 3;
const sanitize = (text: string) => {
var element = document.createElement('div');
element.innerText = text;
return element.innerHTML;
};
let currentDiff = '';
let middleDiff = '';
let currentOffset = 0;
let htmlTagRegex = /(<([^>]+)>)/gi;
$: if (diff) {
middleDiff = '';
currentDiff = '';
currentOffset = 0;
let lineClass = '';
let doc = create(diff, path);
doc.highlightRange(0, diff.length, (text, style) => {
middleDiff += style ? `<span class=${style}>${sanitize(text)}</span>` : sanitize(text);
});
let diffLines = middleDiff.split('<br>');
diffLines.forEach((line, index) => {
lineClass = 'lineContext ';
let firstChar = line.replace(htmlTagRegex, '').slice(0, 1);
if (index < 4) {
lineClass = 'lineDiff bg-zinc-800 text-zinc-500';
} else if (line.slice(0, 2) == '@@') {
lineClass =
'lineSplit bg-zinc-900 bg-opacity-60 pt-1 pb-1 border-t border-b border-zinc-700 mt-1 mb-1';
} else if (firstChar == '+') {
if (!line.includes('+++')) {
lineClass = 'lineSplit bg-green-500 bg-opacity-20';
}
} else if (firstChar == '-') {
if (!line.includes('---')) {
lineClass = 'lineSplit bg-red-600 bg-opacity-20';
}
}
currentDiff += `<div class="${lineClass}">`;
currentDiff += line;
currentDiff += '</div>';
currentOffset += line.length;
});
}
$: parsedDiff = parseDiff(diff);
</script>
<pre class="h-full w-full">{@html currentDiff}</pre>
<Differ diff={parsedDiff} filepath={path} {paddingLines} />

View File

@ -1,35 +1,15 @@
<script lang="ts">
import { type Delta, Operation } from '$lib/api';
import { lineDiff } from './diff';
import { create } from './CodeHighlighter';
import { buildDiffRows, documentMap, RowType, type Row } from './renderer';
import './diff.css';
import './colors/gruvbox.css';
import type { DiffArray } from './diff';
export let doc: string;
export let deltas: Delta[];
export let filepath: string;
export let highlight: string[] = [];
export let paddingLines = 10000;
const applyDeltas = (text: string, deltas: Delta[]) => {
const operations = deltas.flatMap((delta) => delta.operations);
operations.forEach((operation) => {
if (Operation.isInsert(operation)) {
text =
text.slice(0, operation.insert[0]) +
operation.insert[1] +
text.slice(operation.insert[0]);
} else if (Operation.isDelete(operation)) {
text =
text.slice(0, operation.delete[0]) +
text.slice(operation.delete[0] + operation.delete[1]);
}
});
return text;
};
export let diff: DiffArray;
const sanitize = (text: string) => {
var element = document.createElement('div');
@ -37,10 +17,6 @@
return element.innerHTML;
};
$: left = deltas.length > 0 ? applyDeltas(doc, deltas.slice(0, deltas.length - 1)) : doc;
$: right = deltas.length > 0 ? applyDeltas(left, deltas.slice(deltas.length - 1)) : left;
$: diff = lineDiff(left.split('\n'), right.split('\n'));
$: diffRows = buildDiffRows(diff, { paddingLines });
$: originalHighlighter = create(diffRows.originalLines.join('\n'), filepath);
$: originalMap = documentMap(diffRows.originalLines);
@ -179,7 +155,7 @@
changedLines[0].scrollIntoView({ behavior: 'smooth', block: 'center' });
}
};
$: deltas.length && scrollToChangedLine();
$: diff && scrollToChangedLine();
</script>
<div class="flex h-full w-full select-text whitespace-pre font-mono">

View File

@ -0,0 +1,103 @@
import { CharacterIdMap } from './characterIdMap';
import { diff_match_patch } from 'diff-match-patch';
export const charDiff = (
text1: string,
text2: string,
cleanup?: boolean
): { 0: number; 1: string }[] => {
const differ = new diff_match_patch();
const diff = differ.diff_main(text1, text2);
if (cleanup) {
differ.diff_cleanupSemantic(diff);
}
return diff;
};
export const lineDiff = (lines1: string[], lines2: string[]): DiffArray => {
const idMap = new CharacterIdMap<string>();
const text1 = lines1.map((line) => idMap.toChar(line)).join('');
const text2 = lines2.map((line) => idMap.toChar(line)).join('');
const diff = charDiff(text1, text2);
const lineDiff = [];
for (let i = 0; i < diff.length; i++) {
const lines = [];
for (let j = 0; j < diff[i][1].length; j++) {
lines.push(idMap.fromChar(diff[i][1][j]) || '');
}
lineDiff.push({ 0: diff[i][0], 1: lines });
}
return lineDiff;
};
export const parseDiff = (rawDiff: string): DiffArray => {
const lines = rawDiff.split('\n');
// skip header lines
while (isHeaderLine(lines[0])) lines.shift();
const diff: DiffArray = [];
for (const line of lines) {
const gutter = line.substring(0, 1);
const operation = gutterToOperation(gutter);
const content = line.substring(1);
if (diff.length === 0) {
diff.push([operation, [content]]);
} else {
const last = diff[diff.length - 1];
if (last[0] === operation) {
last[1].push(content);
} else {
diff.push([operation, [content]]);
}
}
}
return diff;
function gutterToOperation(gutter: string): Operation {
switch (gutter) {
case '+':
return Operation.Insert;
case '-':
return Operation.Delete;
default:
return Operation.Equal;
}
}
function isHeaderLine(line: string): boolean {
const headers = [
'---',
'+++',
'@@',
'index',
'diff',
'rename',
'similarity',
'new',
'deleted',
'old',
'copy',
'dissimilarity'
];
for (let i = 0; i < headers.length; i++) {
if (line.startsWith(headers[i])) {
return true;
}
}
return false;
}
};
export enum Operation {
Equal = 0,
Insert = 1,
Delete = -1,
Edit = 2
}
export type DiffArray = { 0: Operation; 1: string[] }[];

View File

@ -0,0 +1,2 @@
import { default as Differ } from './Differ.svelte';
export default Differ;

View File

@ -20,14 +20,14 @@ export const enum RowType {
Spacer = 'spacer'
}
export function buildDiffRows(
export const buildDiffRows = (
diff: DiffArray,
opts = { paddingLines: 10000 }
): {
originalLines: readonly string[];
currentLines: readonly string[];
rows: readonly Row[];
} {
} => {
const { paddingLines } = opts;
let currentLineNumber = 0;
@ -153,13 +153,13 @@ export function buildDiffRows(
size
};
}
}
};
export function documentMap(lines: readonly string[]): Map<number, number> {
export const documentMap = (lines: readonly string[]): Map<number, number> => {
const map = new Map<number, number>();
for (let pos = 0, lineNo = 0; lineNo < lines.length; lineNo++) {
map.set(lineNo + 1, pos);
pos += lines[lineNo].length + 1;
}
return map;
}
};

View File

@ -1,7 +1,6 @@
export { default as BackForwardButtons } from './BackForwardButtons.svelte';
export { default as Login } from './Login.svelte';
export { default as Breadcrumbs } from './Breadcrumbs.svelte';
export { default as CodeViewer } from './CodeViewer';
export { default as CommandPalette } from './CommandPalette/CommandPalette.svelte';
export { default as Modal } from './Modal.svelte';
export { default as ButtonGroup } from './ButtonGroup';
@ -10,3 +9,5 @@ export { default as Tooltip } from './Tooltip';
export { default as Button } from './Button';
export { default as Link } from './Link';
export { default as Statuses } from './Statuses.svelte';
export { default as Differ } from './Differ';
export { default as DeltasViewer } from './DeltasViewer.svelte';

View File

@ -24,9 +24,16 @@
let summary = '';
let description = '';
const selectedDiffPath = writable<string | undefined>(Object.keys($statuses).at(0));
const selectedDiffPath = writable<string | undefined>(
Object.keys($statuses)
.sort((a, b) => a.localeCompare(b))
.at(0)
);
statuses.subscribe((statuses) => {
$selectedDiffPath = Object.keys(statuses).at(0);
if ($selectedDiffPath && Object.keys(statuses).includes($selectedDiffPath)) return;
$selectedDiffPath = Object.keys(statuses)
.sort((a, b) => a.localeCompare(b))
.at(0);
});
const selectedDiff = derived([diffs, selectedDiffPath], ([diffs, selectedDiffPath]) =>
selectedDiffPath ? diffs[selectedDiffPath] : undefined
@ -332,7 +339,7 @@
>
{#if $selectedDiffPath !== undefined}
{#if $selectedDiff !== undefined}
<DiffViewer diff={$selectedDiff} path={$selectedDiffPath} />
<DiffViewer diff={$selectedDiff ?? ''} path={$selectedDiffPath} />
{:else}
<div class="flex h-full w-full flex-col items-center justify-center">
<p class="text-lg">Unable to load diff</p>

View File

@ -32,7 +32,7 @@
} from '$lib/components/icons';
import { collapse } from '$lib/paths';
import { page } from '$app/stores';
import { CodeViewer } from '$lib/components';
import { DeltasViewer } from '$lib/components';
import { asyncDerived } from '@square/svelte-store';
import { format } from 'date-fns';
import { onMount } from 'svelte';
@ -375,7 +375,7 @@
</header>
<div id="code" class="flex-auto overflow-auto px-2">
<div class="pb-[200px]">
<CodeViewer
<DeltasViewer
doc={$frame.doc}
deltas={$frame.deltas}
filepath={$frame.filepath}

View File

@ -5,7 +5,7 @@
import { asyncDerived } from '@square/svelte-store';
import { IconLoading } from '$lib/components/icons';
import { format, formatDistanceToNow } from 'date-fns';
import { CodeViewer } from '$lib/components';
import { DeltasViewer } from '$lib/components';
import { page } from '$app/stores';
import { derived } from 'svelte/store';
import { goto } from '$app/navigation';
@ -101,7 +101,7 @@
<div
class="flex-auto overflow-auto rounded-lg border border-zinc-700 bg-[#2F2F33] p-2 text-[#EBDBB2] shadow-lg"
>
<CodeViewer {doc} {deltas} {filepath} paddingLines={2} {highlight} />
<DeltasViewer {doc} {deltas} {filepath} paddingLines={2} {highlight} />
</div>
</a>
</li>