@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 ADDED
@@ -0,0 +1,6 @@
1
+ # History changelog
2
+
3
+ ## 0.1.0
4
+
5
+ - Initial independent public package extracted from Lapis Notes without changing
6
+ the runtime plugin ID.
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,3 @@
1
+ import type { HistoryCaptureEventType } from "./history-plugin";
2
+ export declare function formatHistoryTimestamp(value: number): string;
3
+ export declare function formatHistoryEvent(eventType: HistoryCaptureEventType): string;
@@ -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
+ }