mirror of
https://github.com/gitbutlerapp/gitbutler.git
synced 2024-12-27 03:22:15 +03:00
Merge branch 'master' of https://github.com/gitbutlerapp/gitbutler-client
This commit is contained in:
commit
3a8074fa3b
2
.github/workflows/publish.yaml
vendored
2
.github/workflows/publish.yaml
vendored
@ -18,7 +18,7 @@ jobs:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
platform:
|
||||
# - macos-latest
|
||||
- macos-latest
|
||||
- macos-aarch64
|
||||
|
||||
runs-on: ${{ matrix.platform }}
|
||||
|
@ -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>
|
||||
|
@ -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>
|
@ -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>
|
||||
|
93
src/lib/components/CommandPalette/commands.ts
Normal file
93
src/lib/components/CommandPalette/commands.ts
Normal 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];
|
||||
};
|
@ -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
|
||||
|
@ -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">
|
||||
|
Loading…
Reference in New Issue
Block a user