This commit is contained in:
Ian Donahue 2023-03-17 12:18:08 +01:00
commit 3a8074fa3b
7 changed files with 283 additions and 95 deletions

View File

@ -18,7 +18,7 @@ jobs:
fail-fast: false
matrix:
platform:
# - macos-latest
- macos-latest
- macos-aarch64
runs-on: ${{ matrix.platform }}

View File

@ -1,7 +1,11 @@
<script lang="ts">
import { type Delta, Operation } from '$lib/deltas';
import { lineDiff } from './diff';
import DiffView from './DiffView.svelte';
import { create } from './CodeHighlighter';
import { buildDiffRows, documentMap, RowType, type Row } from './renderer';
import './diff.css';
import './colors/gruvbox.css';
export let doc: string;
export let deltas: Delta[];
@ -28,6 +32,74 @@
$: 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);
$: originalHighlighter = create(diffRows.originalLines.join('\n'), filepath);
$: originalMap = documentMap(diffRows.originalLines);
$: currentHighlighter = create(diffRows.currentLines.join('\n'), filepath);
$: currentMap = documentMap(diffRows.currentLines);
const renderRowContent = (row: Row) => {
if (row.type === RowType.Spacer) {
return row.tokens.map((tok) => `${tok.text}`);
}
const [doc, startPos] =
row.type === RowType.Deletion
? [originalHighlighter, originalMap.get(row.originalLineNumber) as number]
: [currentHighlighter, currentMap.get(row.currentLineNumber) as number];
const content: string[] = [];
let pos = startPos;
const sanitize = (text: string) => {
var element = document.createElement('div');
element.innerText = text;
return element.innerHTML;
};
for (const token of row.tokens) {
let tokenContent = '';
doc.highlightRange(pos, pos + token.text.length, (text, style) => {
tokenContent += style ? `<span class=${style}>${sanitize(text)}</span>` : sanitize(text);
});
content.push(
token.className
? `<span class=${token.className}>${tokenContent}</span>`
: `${tokenContent}`
);
pos += token.text.length;
}
return content;
};
</script>
<DiffView {diff} {filepath} />
<div class="diff-listing w-full select-text whitespace-pre font-mono">
{#each diffRows.rows as row}
{@const baseNumber =
row.type === RowType.Equal || row.type === RowType.Deletion
? String(row.originalLineNumber)
: ''}
{@const curNumber =
row.type === RowType.Equal || row.type === RowType.Addition
? String(row.currentLineNumber)
: ''}
<div class="select-none pr-1 pl-2.5 text-right text-[#665c54]">{baseNumber}</div>
<div class="select-none pr-1 pl-2.5 text-right text-[#665c54]">{curNumber}</div>
<div
class="diff-line-marker"
class:diff-line-addition={row.type === RowType.Addition}
class:diff-line-deletion={row.type === RowType.Deletion}
>
{row.type === RowType.Addition ? '+' : row.type === RowType.Deletion ? '-' : ''}
</div>
<div
class:line-changed={row.type === RowType.Addition || row.type === RowType.Deletion}
class="px-1 diff-line-{row.type}"
data-line-number={curNumber}
>
{#each renderRowContent(row) as content}
{@html content}
{/each}
</div>
{/each}
</div>

View File

@ -1,80 +0,0 @@
<script lang="ts">
import type { DiffArray } from './diff';
import { create } from './CodeHighlighter';
import { buildDiffRows, documentMap, RowType, type Row } from './renderer';
import './diff.css';
import './colors/gruvbox.css';
export let diff: DiffArray;
export let filepath: string;
$: diffRows = buildDiffRows(diff);
$: originalHighlighter = create(diffRows.originalLines.join('\n'), filepath);
$: originalMap = documentMap(diffRows.originalLines);
$: currentHighlighter = create(diffRows.currentLines.join('\n'), filepath);
$: currentMap = documentMap(diffRows.currentLines);
const renderRowContent = (row: Row) => {
if (row.type === RowType.Spacer) {
return row.tokens.map((tok) => `${tok.text}`);
}
const [doc, startPos] =
row.type === RowType.Deletion
? [originalHighlighter, originalMap.get(row.originalLineNumber) as number]
: [currentHighlighter, currentMap.get(row.currentLineNumber) as number];
const content: string[] = [];
let pos = startPos;
const sanitize = (text: string) => {
var element = document.createElement('div');
element.innerText = text;
return element.innerHTML;
};
for (const token of row.tokens) {
let tokenContent = '';
doc.highlightRange(pos, pos + token.text.length, (text, style) => {
tokenContent += style ? `<span class=${style}>${sanitize(text)}</span>` : sanitize(text);
});
content.push(
token.className
? `<span class=${token.className}>${tokenContent}</span>`
: `${tokenContent}`
);
pos += token.text.length;
}
return content;
};
</script>
<div class="diff-listing w-full select-text whitespace-pre font-mono">
{#each diffRows.rows as row}
{@const baseNumber =
row.type === RowType.Equal || row.type === RowType.Deletion
? String(row.originalLineNumber)
: ''}
{@const curNumber =
row.type === RowType.Equal || row.type === RowType.Addition
? String(row.currentLineNumber)
: ''}
<div class="select-none pr-1 pl-2.5 text-right text-[#665c54]">{baseNumber}</div>
<div class="select-none pr-1 pl-2.5 text-right text-[#665c54]">{curNumber}</div>
<div
class="diff-line-marker"
class:diff-line-addition={row.type === RowType.Addition}
class:diff-line-deletion={row.type === RowType.Deletion}
>
{row.type === RowType.Addition ? '+' : row.type === RowType.Deletion ? '-' : ''}
</div>
<div
class:line-changed={row.type === RowType.Addition || row.type === RowType.Deletion}
class="px-1 diff-line-{row.type}"
data-line-number={curNumber}
>
{#each renderRowContent(row) as content}
{@html content}
{/each}
</div>
{/each}
</div>

View File

@ -1,8 +1,14 @@
<script lang="ts">
import { onDestroy, onMount, afterUpdate } from 'svelte';
import { onDestroy, onMount } from 'svelte';
import { goto } from '$app/navigation';
import { getContext } from 'svelte';
import type { Readable } from 'svelte/store';
import { currentProject } from '$lib/current_project';
import { IconCircleCancel } from '$lib/components/icons';
import type { Project } from '$lib/projects';
import tinykeys from 'tinykeys';
import type { CommandGroup } from './commands';
import { Action, previousCommand, nextCommand, firstVisibleCommand } from './commands';
$: scopeToProject = $currentProject ? true : false;
@ -10,11 +16,58 @@
let dialog: HTMLDialogElement;
let userInput: string;
let projects: Readable<any> = getContext('projects');
let selection: [number, number] = [0, 0];
// if the group or the command are no longer visible, select the first visible group and first visible command
$: if (
!commandGroups[selection[0]]?.visible ||
!commandGroups[selection[0]].commands[selection[1]]?.visible
) {
selection = firstVisibleCommand(commandGroups);
}
$: commandGroups = [
{
name: 'Repositories',
visible: !scopeToProject,
commands: $projects.map((project: Project) => {
return {
title: project.title,
description: 'Repository',
selected: false,
action: {
href: `/projects/${project.id}/`
},
visible: project.title.toLowerCase().includes(userInput?.toLowerCase())
};
})
}
] as CommandGroup[];
const resetState = () => {
userInput = '';
scopeToProject = $currentProject ? true : false;
selection = [0, 0];
};
const handleEnter = () => {
if (!commandGroups[0].visible || !commandGroups[0].commands[0].visible) {
return;
}
const command = commandGroups[selection[0]].commands[selection[1]];
if (Action.isLink(command.action)) {
toggleCommandPalette();
goto(command.action.href);
}
};
const toggleCommandPalette = () => {
if (dialog && dialog.open) {
dialog.close();
showingCommandPalette = false;
} else {
resetState();
dialog.showModal();
showingCommandPalette = true;
}
@ -23,7 +76,7 @@
let unsubscribeKeyboardHandler: () => void;
onMount(() => {
toggleCommandPalette();
// toggleCommandPalette(); // developmnet only
unsubscribeKeyboardHandler = tinykeys(window, {
'Meta+k': () => {
toggleCommandPalette();
@ -32,6 +85,21 @@
if (!userInput) {
scopeToProject = false;
}
},
Enter: () => {
handleEnter();
},
ArrowDown: () => {
selection = nextCommand(commandGroups, selection);
},
ArrowUp: () => {
selection = previousCommand(commandGroups, selection);
},
'Control+n': () => {
selection = nextCommand(commandGroups, selection);
},
'Control+p': () => {
selection = previousCommand(commandGroups, selection);
}
});
});
@ -53,29 +121,62 @@
<div class="min-h-[640px] w-[640px] rounded text-zinc-400" on:click|stopPropagation>
<!-- Search input area -->
<div class="flex h-14 items-center border-b border-zinc-400/20">
<div class="ml-4 flex flex-grow items-center">
<div class="ml-4 mr-2 flex flex-grow items-center">
<!-- Project scope -->
{#if scopeToProject}
<div class="flex items-center">
<div class="flex items-center mr-1">
<span class="font-semibold text-zinc-300">{$currentProject?.title}</span>
<span class="ml-1 text-lg">/</span>
</div>
{/if}
<!-- Search input -->
<div class="mx-1 flex-grow">
<div class="flex-grow mr-1">
<!-- svelte-ignore a11y-autofocus -->
<input
class="w-full bg-transparent text-zinc-300 focus:outline-none"
bind:value={userInput}
type="text"
autofocus
placeholder={scopeToProject
? 'Search for commands, files and code changes...'
: 'Search for projects'}
: 'Search for repositories'}
/>
</div>
<div class="mr-4 text-red-50">
<button on:click={toggleCommandPalette} class="hover:bg-zinc-600 p-2 rounded">
<IconCircleCancel class="fill-zinc-400" />
</div>
</button>
</div>
</div>
<!-- Main part -->
<div>
{#each commandGroups as group, groupIdx}
{#if group.visible}
<div class="mx-2 cursor-default select-none">
<p class="mx-2 py-2 text-sm text-zinc-300/80 font-semibold select-none cursor-default">
{group.name}
</p>
<ul class="">
{#each group.commands as command, commandIdx}
{#if command.visible}
{#if Action.isLink(command.action)}
<a
on:mouseover={() => (selection = [groupIdx, commandIdx])}
on:focus={() => (selection = [groupIdx, commandIdx])}
href={command.action.href}
class="{selection[0] === groupIdx && selection[1] === commandIdx
? 'bg-zinc-700/70'
: ''} px-2 flex rounded-lg p-2 items-center cursor-default outline-none"
>
<span class="flex-grow">{command.title}</span>
<span>{command.description}</span>
</a>
{/if}
{/if}
{/each}
</ul>
</div>
{/if}
{/each}
</div>
</div>
</dialog>

View File

@ -0,0 +1,93 @@
export type ActionLink = { href: string };
export type ActionInPalette = { action: () => void }; // todo
export type Action = ActionLink | ActionInPalette;
export namespace Action {
export const isLink = (action: Action): action is ActionLink => 'href' in action;
export const isActionInPalette = (action: Action): action is ActionInPalette => 'todo' in action;
}
export type Command = {
title: string;
description: string;
// icon: string;
action: Action;
selected: boolean;
visible: boolean;
};
export type CommandGroup = {
name: string;
visible: boolean;
commands: Command[];
};
export const firstVisibleCommand = (commandGroups: CommandGroup[]): [number, number] => {
const firstVisibleGroup = commandGroups.findIndex((group) => group.visible);
if (firstVisibleGroup === -1) {
return [0, 0];
}
const firstVisibleCommand = commandGroups[firstVisibleGroup]?.commands.findIndex(
(command) => command.visible
);
if (firstVisibleCommand === -1) {
return [0, 0];
}
return [firstVisibleGroup, firstVisibleCommand];
};
export const nextCommand = (
commandGroups: CommandGroup[],
selection: [number, number]
): [number, number] => {
const { commands } = commandGroups[selection[0]];
const nextVisibleCommandIndex = commands
.slice(selection[1] + 1)
.findIndex((command) => command.visible);
if (nextVisibleCommandIndex !== -1) {
return [selection[0], selection[1] + 1 + nextVisibleCommandIndex];
}
const nextVisibleGroupIndex = commandGroups
.slice(selection[0] + 1)
.findIndex((group) => group.visible);
if (nextVisibleGroupIndex !== -1) {
const { commands } = commandGroups[selection[0] + 1 + nextVisibleGroupIndex];
const nextVisibleCommandIndex = commands.findIndex((command) => command.visible);
return [selection[0] + 1 + nextVisibleGroupIndex, nextVisibleCommandIndex];
}
return [0, 0];
};
export const previousCommand = (
commandGroups: CommandGroup[],
selection: [number, number]
): [number, number] => {
const { commands } = commandGroups[selection[0]];
const previousVisibleCommandIndex = commands
.slice(0, selection[1])
.reverse()
.findIndex((command) => command.visible);
if (previousVisibleCommandIndex !== -1) {
return [selection[0], selection[1] - 1 - previousVisibleCommandIndex];
}
const previousVisibleGroupIndex = commandGroups
.slice(0, selection[0])
.reverse()
.findIndex((group) => group.visible);
if (previousVisibleGroupIndex !== -1) {
const { commands } = commandGroups[selection[0] - 1 - previousVisibleGroupIndex];
const previousVisibleCommandIndex = commands
.slice()
.reverse()
.findIndex((command) => command.visible);
return [selection[0] - 1 - previousVisibleGroupIndex, previousVisibleCommandIndex];
}
return [0, 0];
};

View File

@ -135,7 +135,7 @@
projectId: data.projectId,
sessionId: sid
}).then((files) => {
Object.entries(sessionFiles[sid]).forEach(([filepath, _]) => {
Object.entries(sessionFiles[sid] ?? {}).forEach(([filepath, _]) => {
if (files[filepath] !== undefined) {
sessionFiles[sid][filepath] = files[filepath];
}
@ -462,9 +462,7 @@
class="m-2 flex-auto overflow-auto rounded border border-zinc-700 bg-[#2F2F33] "
>
<div class="relative flex h-full w-full flex-col gap-2 ">
<div id="code"
class="h-full w-full flex-auto overflow-auto px-2 pb-[120px]"
>
<div id="code" class="h-full w-full flex-auto overflow-auto px-2 pb-[120px]">
{#if dayPlaylist[currentDay] !== undefined}
{#if currentEdit !== null}
<CodeViewer

View File

@ -44,6 +44,10 @@
<p class="mb-2 text-xl text-[#D4D4D8]">Results for "{$searchTerm}"</p>
<p class="text-lg text-[#717179]">{processedResults.length} change instances</p>
</div>
{:else}
<div class="mb-10 mt-14">
<p class="mb-2 text-xl text-[#D4D4D8]">No results for "{$searchTerm}"</p>
</div>
{/if}
<ul class="flex flex-col gap-4">