@lapis-notes/history 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +6 -0
- package/LICENSE.md +8 -0
- package/README.md +20 -0
- package/dist/history-capture.d.ts +7 -0
- package/dist/history-capture.js +21 -0
- package/dist/history-compare-panel.css +126 -0
- package/dist/history-compare-panel.svelte +314 -0
- package/dist/history-compare-panel.svelte.d.ts +10 -0
- package/dist/history-compare-view.d.ts +24 -0
- package/dist/history-compare-view.js +97 -0
- package/dist/history-format.d.ts +3 -0
- package/dist/history-format.js +19 -0
- package/dist/history-panel.css +200 -0
- package/dist/history-panel.svelte +302 -0
- package/dist/history-panel.svelte.d.ts +8 -0
- package/dist/history-plugin.d.ts +88 -0
- package/dist/history-plugin.js +435 -0
- package/dist/history-settings-tab.d.ts +9 -0
- package/dist/history-settings-tab.js +72 -0
- package/dist/history-settings.d.ts +14 -0
- package/dist/history-settings.js +47 -0
- package/dist/history-tracking.d.ts +4 -0
- package/dist/history-tracking.js +49 -0
- package/dist/history-view-type.d.ts +2 -0
- package/dist/history-view-type.js +2 -0
- package/dist/history-view.d.ts +12 -0
- package/dist/history-view.js +36 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.js +9 -0
- package/dist/public-components.d.ts +2 -0
- package/dist/public-components.js +2 -0
- package/dist/register-history-settings.d.ts +105 -0
- package/dist/register-history-settings.js +199 -0
- package/dist/styles.css +2 -0
- package/manifest.json +27 -0
- package/package.json +72 -0
- package/styles.css +1 -0
package/CHANGELOG.md
ADDED
package/LICENSE.md
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
# License
|
|
2
|
+
|
|
3
|
+
Copyright (c) LapisMD contributors.
|
|
4
|
+
|
|
5
|
+
This software is licensed under the GNU Affero General Public License, version
|
|
6
|
+
3 or (at your option) any later version (`AGPL-3.0-or-later`). The complete
|
|
7
|
+
license text is available from the
|
|
8
|
+
[GNU Project](https://www.gnu.org/licenses/agpl-3.0.html).
|
package/README.md
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# History
|
|
2
|
+
|
|
3
|
+
File history
|
|
4
|
+
|
|
5
|
+
## Install for static composition
|
|
6
|
+
|
|
7
|
+
```sh
|
|
8
|
+
pnpm add @lapis-notes/history
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Register the exported plugin class in the application's `PluginProfile`. Import
|
|
12
|
+
`@lapis-notes/history/styles.css?inline` and pass the CSS through the static
|
|
13
|
+
registration so the host owns its lifecycle.
|
|
14
|
+
|
|
15
|
+
The runtime plugin ID is `history`. A matching signed
|
|
16
|
+
`history-0.1.0.lapis-plugin` archive is attached to the
|
|
17
|
+
package-scoped GitHub release for manual or registry installation.
|
|
18
|
+
|
|
19
|
+
See the [repository README](https://github.com/lapismd/lapis-plugins#readme) for
|
|
20
|
+
development, validation, and release-gate details.
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export declare function shouldReplaceLatestModify(eventType: string, latestEventType: string | undefined, lastModifyStoredAt: number | undefined, mergeWindowMs: number, now?: number): boolean;
|
|
2
|
+
export declare function consumeSuppressedHash(suppressed: Map<string, string>, path: string, contentHash: string): boolean;
|
|
3
|
+
export declare function historyIdentityStoreInput(eventType: "rename" | "delete", path: string, previousPath?: string): {
|
|
4
|
+
path: string;
|
|
5
|
+
previousPath?: string;
|
|
6
|
+
eventType: "rename" | "delete";
|
|
7
|
+
};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export function shouldReplaceLatestModify(eventType, latestEventType, lastModifyStoredAt, mergeWindowMs, now = Date.now()) {
|
|
2
|
+
return (eventType === "modify" &&
|
|
3
|
+
latestEventType === "modify" &&
|
|
4
|
+
lastModifyStoredAt != null &&
|
|
5
|
+
now - lastModifyStoredAt < mergeWindowMs);
|
|
6
|
+
}
|
|
7
|
+
export function consumeSuppressedHash(suppressed, path, contentHash) {
|
|
8
|
+
if (suppressed.get(path) !== contentHash)
|
|
9
|
+
return false;
|
|
10
|
+
suppressed.delete(path);
|
|
11
|
+
return true;
|
|
12
|
+
}
|
|
13
|
+
export function historyIdentityStoreInput(eventType, path, previousPath) {
|
|
14
|
+
return {
|
|
15
|
+
path,
|
|
16
|
+
eventType,
|
|
17
|
+
...(eventType === "rename" && previousPath
|
|
18
|
+
? { previousPath }
|
|
19
|
+
: {}),
|
|
20
|
+
};
|
|
21
|
+
}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
.history-compare {
|
|
2
|
+
--ui-diff-frame-border: none;
|
|
3
|
+
--ui-diff-frame-radius: 0;
|
|
4
|
+
/* Prefer Design Core's brand mono. Host `--font-mono` can be reset to
|
|
5
|
+
system UI mono by other plugin stylesheets. */
|
|
6
|
+
--ui-diff-mono: var(
|
|
7
|
+
--studio-font-mono,
|
|
8
|
+
"Source Code Pro Variable",
|
|
9
|
+
"Source Code Pro",
|
|
10
|
+
ui-monospace,
|
|
11
|
+
SFMono-Regular,
|
|
12
|
+
Menlo,
|
|
13
|
+
Monaco,
|
|
14
|
+
Consolas,
|
|
15
|
+
"Liberation Mono",
|
|
16
|
+
monospace
|
|
17
|
+
);
|
|
18
|
+
display: flex;
|
|
19
|
+
box-sizing: border-box;
|
|
20
|
+
width: 100%;
|
|
21
|
+
height: 100%;
|
|
22
|
+
min-width: 0;
|
|
23
|
+
min-height: 0;
|
|
24
|
+
flex-direction: column;
|
|
25
|
+
overflow: hidden;
|
|
26
|
+
color: var(--ui-workspace-view-foreground, inherit);
|
|
27
|
+
background: var(--ui-workspace-view-background, transparent);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
.history-compare__chrome {
|
|
31
|
+
display: flex;
|
|
32
|
+
flex: 0 0 auto;
|
|
33
|
+
flex-direction: column;
|
|
34
|
+
align-items: stretch;
|
|
35
|
+
gap: 0;
|
|
36
|
+
padding: 0.25rem 0.25rem 0;
|
|
37
|
+
border-bottom: 1px solid
|
|
38
|
+
var(--ui-workspace-border, color-mix(in srgb, currentColor 14%, transparent));
|
|
39
|
+
background: var(--ui-workspace-view-background, transparent);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
.history-compare__revision {
|
|
43
|
+
display: flex;
|
|
44
|
+
min-width: 0;
|
|
45
|
+
align-items: center;
|
|
46
|
+
justify-content: space-between;
|
|
47
|
+
gap: 0.5rem;
|
|
48
|
+
padding: 0.35rem 0.5rem 0.15rem;
|
|
49
|
+
color: var(--ui-workspace-muted-foreground, var(--muted-foreground));
|
|
50
|
+
font-size: 0.75rem;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
.history-compare__revision > span:first-child {
|
|
54
|
+
min-width: 0;
|
|
55
|
+
overflow: hidden;
|
|
56
|
+
text-overflow: ellipsis;
|
|
57
|
+
white-space: nowrap;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
.history-compare__revision [data-ui-component="file-change-stats"] {
|
|
61
|
+
flex: 0 0 auto;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
.history-compare__toolbar {
|
|
65
|
+
display: flex;
|
|
66
|
+
align-items: center;
|
|
67
|
+
justify-content: center;
|
|
68
|
+
gap: 0.125rem;
|
|
69
|
+
padding: 0.15rem 0 0.25rem;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
.history-compare__toolbar
|
|
73
|
+
button[data-ui-component="button"].history-compare__toolbar-action {
|
|
74
|
+
color: inherit;
|
|
75
|
+
background: transparent;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
.history-compare__toolbar
|
|
79
|
+
button[data-ui-component="button"].history-compare__toolbar-action:hover,
|
|
80
|
+
.history-compare__toolbar
|
|
81
|
+
button[data-ui-component="button"].history-compare__toolbar-action:focus-visible {
|
|
82
|
+
color: var(
|
|
83
|
+
--ui-workspace-panel-action-hover-foreground,
|
|
84
|
+
var(--sidebar-accent-foreground, inherit)
|
|
85
|
+
);
|
|
86
|
+
background: var(
|
|
87
|
+
--ui-workspace-panel-action-hover-background,
|
|
88
|
+
var(--sidebar-accent, #e3e3e3)
|
|
89
|
+
);
|
|
90
|
+
outline: none;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
.history-compare__toolbar
|
|
94
|
+
button[data-ui-component="button"].history-compare__toolbar-action[aria-pressed="true"],
|
|
95
|
+
.history-compare__toolbar
|
|
96
|
+
button[data-ui-component="button"].history-compare__toolbar-action[aria-pressed="true"]:hover {
|
|
97
|
+
color: var(--ui-workspace-accent, #7c3aed);
|
|
98
|
+
background: var(
|
|
99
|
+
--ui-workspace-selection,
|
|
100
|
+
color-mix(in srgb, var(--ui-workspace-accent, #7c3aed) 14%, transparent)
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
.history-compare__body {
|
|
105
|
+
display: flex;
|
|
106
|
+
min-width: 0;
|
|
107
|
+
min-height: 0;
|
|
108
|
+
flex: 1 1 auto;
|
|
109
|
+
flex-direction: column;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
.history-compare__body [data-ui-component="merge-editor"],
|
|
113
|
+
.history-compare__body [data-ui-component="file-diff"] {
|
|
114
|
+
flex: 1 1 auto;
|
|
115
|
+
height: 100%;
|
|
116
|
+
min-height: 0;
|
|
117
|
+
border: none;
|
|
118
|
+
border-radius: 0;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
.history-compare__empty,
|
|
122
|
+
.history-compare__binary {
|
|
123
|
+
margin: 0;
|
|
124
|
+
padding: 0.75rem;
|
|
125
|
+
color: var(--ui-workspace-muted-foreground, var(--muted-foreground));
|
|
126
|
+
}
|
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import "./history-compare-panel.css";
|
|
3
|
+
import { type App } from "@lapis-notes/api";
|
|
4
|
+
import {
|
|
5
|
+
FileChangeStats,
|
|
6
|
+
FileDiff,
|
|
7
|
+
MergeEditor,
|
|
8
|
+
buildUnifiedDiffRows,
|
|
9
|
+
isBinaryFilePath,
|
|
10
|
+
type FileDiffViewMode,
|
|
11
|
+
type MergeResolvedChange,
|
|
12
|
+
} from "@lapismd/design-core/diff";
|
|
13
|
+
import { Button } from "@lapismd/design-core/shadcn/button";
|
|
14
|
+
import * as Tooltip from "@lapismd/design-core/shadcn/tooltip";
|
|
15
|
+
import ArchiveRestore from "@lucide/svelte/icons/archive-restore";
|
|
16
|
+
import Check from "@lucide/svelte/icons/check";
|
|
17
|
+
import Columns2 from "@lucide/svelte/icons/columns-2";
|
|
18
|
+
import GitCompare from "@lucide/svelte/icons/git-compare";
|
|
19
|
+
import GitCompareArrows from "@lucide/svelte/icons/git-compare-arrows";
|
|
20
|
+
import Rows2 from "@lucide/svelte/icons/rows-2";
|
|
21
|
+
import WrapText from "@lucide/svelte/icons/wrap-text";
|
|
22
|
+
import { onMount, untrack } from "svelte";
|
|
23
|
+
import type {
|
|
24
|
+
HistoryCompareViewState,
|
|
25
|
+
HistoryComparisonModel,
|
|
26
|
+
HistoryRevision,
|
|
27
|
+
} from "./history-plugin";
|
|
28
|
+
|
|
29
|
+
type HistoryComparePanelPlugin = {
|
|
30
|
+
getHistoryComparisonModel(
|
|
31
|
+
state: HistoryCompareViewState,
|
|
32
|
+
): Promise<HistoryComparisonModel>;
|
|
33
|
+
getCompareAnchor(): { revisionId: string } | null;
|
|
34
|
+
openHistoryCompareView(state: HistoryCompareViewState): Promise<void>;
|
|
35
|
+
closeHistoryCompareView(sourceLeafId?: string): void;
|
|
36
|
+
saveCurrentFileContent(filePath: string, content: string): Promise<void>;
|
|
37
|
+
restoreRevision(filePath: string, revision: HistoryRevision): Promise<void>;
|
|
38
|
+
onHistoryChanged(listener: () => void): () => void;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
let {
|
|
42
|
+
app,
|
|
43
|
+
compareState,
|
|
44
|
+
}: {
|
|
45
|
+
app: App;
|
|
46
|
+
compareState: HistoryCompareViewState;
|
|
47
|
+
} = $props();
|
|
48
|
+
|
|
49
|
+
const plugin = untrack(() => {
|
|
50
|
+
const registered = app.plugins.plugins.get("history") as
|
|
51
|
+
| HistoryComparePanelPlugin
|
|
52
|
+
| undefined;
|
|
53
|
+
if (!registered) throw new Error("History plugin is not registered");
|
|
54
|
+
return registered;
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
let model = $state<HistoryComparisonModel | null>(null);
|
|
58
|
+
let error = $state<string | null>(null);
|
|
59
|
+
let viewMode = $state<FileDiffViewMode>("unified");
|
|
60
|
+
let wrap = $state(false);
|
|
61
|
+
let pendingContent = $state<string | null>(null);
|
|
62
|
+
|
|
63
|
+
const viewModeLabel = $derived(
|
|
64
|
+
viewMode === "split" ? "Split diff" : "Unified diff",
|
|
65
|
+
);
|
|
66
|
+
const binary = $derived(isBinaryFilePath(compareState.filePath));
|
|
67
|
+
const stats = $derived.by(() => {
|
|
68
|
+
if (!model) return { additions: 0, deletions: 0 };
|
|
69
|
+
const rows = buildUnifiedDiffRows(
|
|
70
|
+
model.compareBaseText,
|
|
71
|
+
model.compareNewText,
|
|
72
|
+
);
|
|
73
|
+
return {
|
|
74
|
+
additions: rows.filter((row) => row.variant === "added").length,
|
|
75
|
+
deletions: rows.filter((row) => row.variant === "removed").length,
|
|
76
|
+
};
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
async function refresh(): Promise<void> {
|
|
80
|
+
try {
|
|
81
|
+
model = await plugin.getHistoryComparisonModel(compareState);
|
|
82
|
+
error = null;
|
|
83
|
+
pendingContent = null;
|
|
84
|
+
} catch (cause) {
|
|
85
|
+
model = null;
|
|
86
|
+
error = cause instanceof Error ? cause.message : String(cause);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function close(): void {
|
|
91
|
+
plugin.closeHistoryCompareView(compareState.sourceLeafId);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function openMode(compareMode: HistoryCompareViewState["compareMode"]): void {
|
|
95
|
+
if (!model) return;
|
|
96
|
+
void plugin.openHistoryCompareView({
|
|
97
|
+
...compareState,
|
|
98
|
+
revisionId: model.selectedRevision.revisionId,
|
|
99
|
+
compareMode,
|
|
100
|
+
otherRevisionId:
|
|
101
|
+
compareMode === "selected"
|
|
102
|
+
? (plugin.getCompareAnchor()?.revisionId ??
|
|
103
|
+
compareState.otherRevisionId)
|
|
104
|
+
: undefined,
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
async function applyMerged(): Promise<void> {
|
|
109
|
+
if (!model || pendingContent == null) return;
|
|
110
|
+
await plugin.saveCurrentFileContent(model.filePath, pendingContent);
|
|
111
|
+
pendingContent = null;
|
|
112
|
+
await refresh();
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
async function restoreSelected(): Promise<void> {
|
|
116
|
+
if (!model) return;
|
|
117
|
+
await plugin.restoreRevision(model.filePath, model.selectedRevision);
|
|
118
|
+
await refresh();
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function onResolvedChange(next: MergeResolvedChange): void {
|
|
122
|
+
pendingContent = next.content;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function onKeydown(event: KeyboardEvent): void {
|
|
126
|
+
if (event.key !== "Escape") return;
|
|
127
|
+
event.preventDefault();
|
|
128
|
+
close();
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
onMount(() => {
|
|
132
|
+
const unsubscribe = plugin.onHistoryChanged(() => void refresh());
|
|
133
|
+
document.addEventListener("keydown", onKeydown);
|
|
134
|
+
void refresh();
|
|
135
|
+
return () => {
|
|
136
|
+
unsubscribe();
|
|
137
|
+
document.removeEventListener("keydown", onKeydown);
|
|
138
|
+
};
|
|
139
|
+
});
|
|
140
|
+
</script>
|
|
141
|
+
|
|
142
|
+
<div
|
|
143
|
+
class="history-compare"
|
|
144
|
+
data-ui-component="history-compare-panel"
|
|
145
|
+
data-testid="history-compare-panel"
|
|
146
|
+
data-compare-mode={model?.compareMode ?? compareState.compareMode}
|
|
147
|
+
data-wrap={wrap ? "true" : "false"}
|
|
148
|
+
aria-label="File history comparison"
|
|
149
|
+
>
|
|
150
|
+
<div class="history-compare__chrome" data-ui-part="chrome">
|
|
151
|
+
{#if model}
|
|
152
|
+
<div class="history-compare__revision">
|
|
153
|
+
<span>{model.beforeLabel} → {model.afterLabel}</span>
|
|
154
|
+
<FileChangeStats
|
|
155
|
+
additions={stats.additions}
|
|
156
|
+
deletions={stats.deletions}
|
|
157
|
+
showZero
|
|
158
|
+
/>
|
|
159
|
+
</div>
|
|
160
|
+
{/if}
|
|
161
|
+
<Tooltip.Provider delayDuration={0}>
|
|
162
|
+
<div class="history-compare__toolbar" data-ui-part="toolbar">
|
|
163
|
+
<Tooltip.Root>
|
|
164
|
+
<Tooltip.Trigger>
|
|
165
|
+
{#snippet child({ props })}
|
|
166
|
+
<Button
|
|
167
|
+
{...props}
|
|
168
|
+
variant="ghost"
|
|
169
|
+
size="icon-sm"
|
|
170
|
+
class="history-compare__toolbar-action"
|
|
171
|
+
aria-label="Compare with current"
|
|
172
|
+
aria-pressed={model?.compareMode === "current"}
|
|
173
|
+
onclick={() => openMode("current")}
|
|
174
|
+
>
|
|
175
|
+
<GitCompare aria-hidden="true" />
|
|
176
|
+
</Button>
|
|
177
|
+
{/snippet}
|
|
178
|
+
</Tooltip.Trigger>
|
|
179
|
+
<Tooltip.Content side="bottom">Compare with current</Tooltip.Content>
|
|
180
|
+
</Tooltip.Root>
|
|
181
|
+
<Tooltip.Root>
|
|
182
|
+
<Tooltip.Trigger>
|
|
183
|
+
{#snippet child({ props })}
|
|
184
|
+
<Button
|
|
185
|
+
{...props}
|
|
186
|
+
variant="ghost"
|
|
187
|
+
size="icon-sm"
|
|
188
|
+
class="history-compare__toolbar-action"
|
|
189
|
+
aria-label="Compare with previous"
|
|
190
|
+
aria-pressed={model?.compareMode === "previous"}
|
|
191
|
+
disabled={!model?.previousRevision}
|
|
192
|
+
onclick={() => openMode("previous")}
|
|
193
|
+
>
|
|
194
|
+
<GitCompareArrows aria-hidden="true" />
|
|
195
|
+
</Button>
|
|
196
|
+
{/snippet}
|
|
197
|
+
</Tooltip.Trigger>
|
|
198
|
+
<Tooltip.Content side="bottom">Compare with previous</Tooltip.Content>
|
|
199
|
+
</Tooltip.Root>
|
|
200
|
+
{#if model?.compareMode !== "current"}
|
|
201
|
+
<Tooltip.Root>
|
|
202
|
+
<Tooltip.Trigger>
|
|
203
|
+
{#snippet child({ props })}
|
|
204
|
+
<Button
|
|
205
|
+
{...props}
|
|
206
|
+
variant="ghost"
|
|
207
|
+
size="icon-sm"
|
|
208
|
+
class="history-compare__toolbar-action"
|
|
209
|
+
data-testid="history-compare-view-mode"
|
|
210
|
+
data-view-mode={viewMode}
|
|
211
|
+
aria-label={viewModeLabel}
|
|
212
|
+
onclick={() =>
|
|
213
|
+
(viewMode = viewMode === "split" ? "unified" : "split")}
|
|
214
|
+
>
|
|
215
|
+
{#if viewMode === "split"}
|
|
216
|
+
<Columns2 aria-hidden="true" />
|
|
217
|
+
{:else}
|
|
218
|
+
<Rows2 aria-hidden="true" />
|
|
219
|
+
{/if}
|
|
220
|
+
</Button>
|
|
221
|
+
{/snippet}
|
|
222
|
+
</Tooltip.Trigger>
|
|
223
|
+
<Tooltip.Content side="bottom">{viewModeLabel}</Tooltip.Content>
|
|
224
|
+
</Tooltip.Root>
|
|
225
|
+
{/if}
|
|
226
|
+
<Tooltip.Root>
|
|
227
|
+
<Tooltip.Trigger>
|
|
228
|
+
{#snippet child({ props })}
|
|
229
|
+
<Button
|
|
230
|
+
{...props}
|
|
231
|
+
variant="ghost"
|
|
232
|
+
size="icon-sm"
|
|
233
|
+
class="history-compare__toolbar-action"
|
|
234
|
+
data-testid="history-compare-wrap"
|
|
235
|
+
aria-label="Wrap text"
|
|
236
|
+
aria-pressed={wrap ? "true" : "false"}
|
|
237
|
+
onclick={() => (wrap = !wrap)}
|
|
238
|
+
>
|
|
239
|
+
<WrapText aria-hidden="true" />
|
|
240
|
+
</Button>
|
|
241
|
+
{/snippet}
|
|
242
|
+
</Tooltip.Trigger>
|
|
243
|
+
<Tooltip.Content side="bottom">Wrap text</Tooltip.Content>
|
|
244
|
+
</Tooltip.Root>
|
|
245
|
+
{#if model?.compareMode === "current"}
|
|
246
|
+
<Tooltip.Root>
|
|
247
|
+
<Tooltip.Trigger>
|
|
248
|
+
{#snippet child({ props })}
|
|
249
|
+
<Button
|
|
250
|
+
{...props}
|
|
251
|
+
variant="ghost"
|
|
252
|
+
size="icon-sm"
|
|
253
|
+
class="history-compare__toolbar-action"
|
|
254
|
+
aria-label="Apply"
|
|
255
|
+
disabled={pendingContent == null}
|
|
256
|
+
onclick={() => void applyMerged()}
|
|
257
|
+
>
|
|
258
|
+
<Check aria-hidden="true" />
|
|
259
|
+
</Button>
|
|
260
|
+
{/snippet}
|
|
261
|
+
</Tooltip.Trigger>
|
|
262
|
+
<Tooltip.Content side="bottom">Apply</Tooltip.Content>
|
|
263
|
+
</Tooltip.Root>
|
|
264
|
+
{/if}
|
|
265
|
+
<Tooltip.Root>
|
|
266
|
+
<Tooltip.Trigger>
|
|
267
|
+
{#snippet child({ props })}
|
|
268
|
+
<Button
|
|
269
|
+
{...props}
|
|
270
|
+
variant="ghost"
|
|
271
|
+
size="icon-sm"
|
|
272
|
+
class="history-compare__toolbar-action"
|
|
273
|
+
aria-label="Restore this revision"
|
|
274
|
+
onclick={() => void restoreSelected()}
|
|
275
|
+
>
|
|
276
|
+
<ArchiveRestore aria-hidden="true" />
|
|
277
|
+
</Button>
|
|
278
|
+
{/snippet}
|
|
279
|
+
</Tooltip.Trigger>
|
|
280
|
+
<Tooltip.Content side="bottom">Restore this revision</Tooltip.Content>
|
|
281
|
+
</Tooltip.Root>
|
|
282
|
+
</div>
|
|
283
|
+
</Tooltip.Provider>
|
|
284
|
+
</div>
|
|
285
|
+
<div class="history-compare__body">
|
|
286
|
+
{#if error}
|
|
287
|
+
<p class="history-compare__empty">{error}</p>
|
|
288
|
+
{:else if !model}
|
|
289
|
+
<p class="history-compare__empty">Loading comparison…</p>
|
|
290
|
+
{:else if binary}
|
|
291
|
+
<p class="history-compare__binary">Binary file not shown.</p>
|
|
292
|
+
{:else if model.compareMode === "current"}
|
|
293
|
+
<MergeEditor
|
|
294
|
+
mode="one-way"
|
|
295
|
+
left={model.selectedRevision.content}
|
|
296
|
+
right={model.currentContent}
|
|
297
|
+
leftLabel="Selected revision"
|
|
298
|
+
rightLabel={model.fileExists ? "Current file" : "Latest"}
|
|
299
|
+
path={model.filePath}
|
|
300
|
+
syncHorizontalScroll
|
|
301
|
+
{wrap}
|
|
302
|
+
onResolvedChange={onResolvedChange}
|
|
303
|
+
/>
|
|
304
|
+
{:else}
|
|
305
|
+
<FileDiff
|
|
306
|
+
path={model.filePath}
|
|
307
|
+
oldText={model.compareBaseText}
|
|
308
|
+
newText={model.compareNewText}
|
|
309
|
+
{viewMode}
|
|
310
|
+
{wrap}
|
|
311
|
+
/>
|
|
312
|
+
{/if}
|
|
313
|
+
</div>
|
|
314
|
+
</div>
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import "./history-compare-panel.css";
|
|
2
|
+
import { type App } from "@lapis-notes/api";
|
|
3
|
+
import type { HistoryCompareViewState } from "./history-plugin";
|
|
4
|
+
type $$ComponentProps = {
|
|
5
|
+
app: App;
|
|
6
|
+
compareState: HistoryCompareViewState;
|
|
7
|
+
};
|
|
8
|
+
declare const HistoryComparePanel: import("svelte").Component<$$ComponentProps, {}, "">;
|
|
9
|
+
type HistoryComparePanel = ReturnType<typeof HistoryComparePanel>;
|
|
10
|
+
export default HistoryComparePanel;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { View, type WorkspaceLeaf } from "@lapis-notes/api";
|
|
2
|
+
import type { HistoryPlugin } from "./history-plugin";
|
|
3
|
+
export declare class HistoryCompareView extends View {
|
|
4
|
+
private readonly historyPlugin;
|
|
5
|
+
private component;
|
|
6
|
+
private compareState;
|
|
7
|
+
constructor(historyPlugin: HistoryPlugin, leaf: WorkspaceLeaf);
|
|
8
|
+
getViewType(): string;
|
|
9
|
+
getDisplayText(): string;
|
|
10
|
+
getIcon(): string;
|
|
11
|
+
getBreadcrumbFilePath(): string | null;
|
|
12
|
+
getBreadcrumbs(): {
|
|
13
|
+
id: string;
|
|
14
|
+
label: string;
|
|
15
|
+
onSelect: () => void;
|
|
16
|
+
}[];
|
|
17
|
+
protected onOpen(): Promise<void>;
|
|
18
|
+
protected onClose(): Promise<void>;
|
|
19
|
+
onload(): void;
|
|
20
|
+
setState(state: Record<string, unknown>): Promise<void>;
|
|
21
|
+
onunload(): void;
|
|
22
|
+
private mountPanel;
|
|
23
|
+
private unmountPanel;
|
|
24
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { View } from "@lapis-notes/api";
|
|
2
|
+
import { mount, unmount } from "svelte";
|
|
3
|
+
import HistoryComparePanel from "./history-compare-panel.svelte";
|
|
4
|
+
import { HistoryCompareViewType } from "./history-view-type";
|
|
5
|
+
function readCompareState(state) {
|
|
6
|
+
if (typeof state["filePath"] !== "string")
|
|
7
|
+
return null;
|
|
8
|
+
if (typeof state["revisionId"] !== "string")
|
|
9
|
+
return null;
|
|
10
|
+
const compareMode = state["compareMode"];
|
|
11
|
+
if (compareMode !== "current" &&
|
|
12
|
+
compareMode !== "previous" &&
|
|
13
|
+
compareMode !== "selected") {
|
|
14
|
+
return null;
|
|
15
|
+
}
|
|
16
|
+
return {
|
|
17
|
+
filePath: state["filePath"],
|
|
18
|
+
revisionId: state["revisionId"],
|
|
19
|
+
compareMode,
|
|
20
|
+
otherRevisionId: typeof state["otherRevisionId"] === "string"
|
|
21
|
+
? state["otherRevisionId"]
|
|
22
|
+
: undefined,
|
|
23
|
+
sourceLeafId: typeof state["sourceLeafId"] === "string"
|
|
24
|
+
? state["sourceLeafId"]
|
|
25
|
+
: undefined,
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
export class HistoryCompareView extends View {
|
|
29
|
+
historyPlugin;
|
|
30
|
+
component = null;
|
|
31
|
+
compareState = null;
|
|
32
|
+
constructor(historyPlugin, leaf) {
|
|
33
|
+
super(leaf);
|
|
34
|
+
this.historyPlugin = historyPlugin;
|
|
35
|
+
}
|
|
36
|
+
getViewType() {
|
|
37
|
+
return HistoryCompareViewType;
|
|
38
|
+
}
|
|
39
|
+
getDisplayText() {
|
|
40
|
+
return this.compareState
|
|
41
|
+
? `History: ${this.compareState.filePath}`
|
|
42
|
+
: "History compare";
|
|
43
|
+
}
|
|
44
|
+
getIcon() {
|
|
45
|
+
return "git-compare";
|
|
46
|
+
}
|
|
47
|
+
getBreadcrumbFilePath() {
|
|
48
|
+
return this.compareState?.filePath ?? null;
|
|
49
|
+
}
|
|
50
|
+
getBreadcrumbs() {
|
|
51
|
+
return [
|
|
52
|
+
{
|
|
53
|
+
id: "history",
|
|
54
|
+
label: "History",
|
|
55
|
+
onSelect: () => {
|
|
56
|
+
void this.historyPlugin.openHistoryView(this.compareState?.filePath);
|
|
57
|
+
},
|
|
58
|
+
},
|
|
59
|
+
];
|
|
60
|
+
}
|
|
61
|
+
onOpen() {
|
|
62
|
+
return Promise.resolve();
|
|
63
|
+
}
|
|
64
|
+
onClose() {
|
|
65
|
+
return Promise.resolve();
|
|
66
|
+
}
|
|
67
|
+
onload() {
|
|
68
|
+
this.compareState = readCompareState(this.getState());
|
|
69
|
+
this.mountPanel();
|
|
70
|
+
}
|
|
71
|
+
async setState(state) {
|
|
72
|
+
await super.setState(state);
|
|
73
|
+
this.compareState = readCompareState(state);
|
|
74
|
+
if (this.loaded)
|
|
75
|
+
this.mountPanel();
|
|
76
|
+
}
|
|
77
|
+
onunload() {
|
|
78
|
+
this.unmountPanel();
|
|
79
|
+
}
|
|
80
|
+
mountPanel() {
|
|
81
|
+
this.unmountPanel();
|
|
82
|
+
if (!this.compareState)
|
|
83
|
+
return;
|
|
84
|
+
this.component = mount(HistoryComparePanel, {
|
|
85
|
+
target: this.containerEl,
|
|
86
|
+
props: {
|
|
87
|
+
app: this.app,
|
|
88
|
+
compareState: this.compareState,
|
|
89
|
+
},
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
unmountPanel() {
|
|
93
|
+
if (this.component)
|
|
94
|
+
void unmount(this.component);
|
|
95
|
+
this.component = null;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export function formatHistoryTimestamp(value) {
|
|
2
|
+
return new Date(value).toLocaleString();
|
|
3
|
+
}
|
|
4
|
+
export function formatHistoryEvent(eventType) {
|
|
5
|
+
switch (eventType) {
|
|
6
|
+
case "baseline":
|
|
7
|
+
return "Opened";
|
|
8
|
+
case "create":
|
|
9
|
+
return "Created";
|
|
10
|
+
case "modify":
|
|
11
|
+
return "Edited";
|
|
12
|
+
case "rename":
|
|
13
|
+
return "Renamed";
|
|
14
|
+
case "delete":
|
|
15
|
+
return "Deleted";
|
|
16
|
+
case "restore":
|
|
17
|
+
return "Restored";
|
|
18
|
+
}
|
|
19
|
+
}
|