Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 3 additions & 4 deletions denops/fall/extension/previewer/session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,16 @@ import type { PreviewItem } from "jsr:@vim-fall/core@^0.3.0/item";
import type { Previewer } from "jsr:@vim-fall/core@^0.3.0/previewer";
import { definePreviewer } from "jsr:@vim-fall/std@^0.10.0/previewer";
import type { Detail } from "../source/session.ts";
import { decompressPickerSession } from "../../session.ts";

export function session(): Previewer<Detail> {
return definePreviewer(async (_denops, { item }, { signal }) => {
return definePreviewer((_denops, { item }, { signal }) => {
if (!item || signal?.aborted) {
return undefined;
}

try {
// Decompress the session to access its data
const session = await decompressPickerSession(item.detail);
// Access the session data directly
const session = item.detail;

const lines: string[] = [];

Expand Down
4 changes: 2 additions & 2 deletions denops/fall/extension/source/session.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import type { Source } from "jsr:@vim-fall/core@^0.3.0/source";
import type { DetailUnit, IdItem } from "jsr:@vim-fall/core@^0.3.0/item";
import type { PickerSessionCompressed } from "../../session.ts";
import type { PickerSession } from "../../session.ts";
import { listPickerSessions } from "../../session.ts";

export type Detail = PickerSessionCompressed<DetailUnit>;
export type Detail = PickerSession<DetailUnit>;

export function session(): Source<Detail> {
return {
Expand Down
6 changes: 3 additions & 3 deletions denops/fall/main/picker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -149,7 +149,7 @@ async function resumePicker(
// Parse filter ({name}#{indexFromLatest})
const [filterName, filterNumberStr = "1"] = filter.split("#", 2);
const filterNumber = Number(filterNumberStr);
const session = await loadPickerSession({
const session = loadPickerSession({
name: filterName,
number: filterNumber,
});
Expand Down Expand Up @@ -212,12 +212,12 @@ async function startPicker<T extends Detail>(
stack.defer(() => {
zindex -= Picker.ZINDEX_ALLOCATION;
});
stack.defer(async () => {
stack.defer(() => {
const name = pickerParams.name;
if (SESSION_EXCLUDE_SOURCES.includes(name)) {
return;
}
await savePickerSession({
savePickerSession({
name,
args,
context: itemPicker.context,
Expand Down
97 changes: 14 additions & 83 deletions denops/fall/session.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,13 @@
import type { Detail } from "jsr:@vim-fall/core@^0.3.0/item";
import { brotli } from "jsr:@deno-library/compress@^0.5.6";

import type { PickerContext } from "./picker.ts";

/**
* In-memory storage for compressed picker sessions.
* In-memory storage for picker sessions.
* Sessions are stored in chronological order (oldest first).
*/
// deno-lint-ignore no-explicit-any
const sessions: PickerSessionCompressed<any>[] = [];
const sessions: PickerSession<any>[] = [];

/**
* Maximum number of sessions to keep in memory.
Expand All @@ -28,88 +27,24 @@ export type PickerSession<T extends Detail> = {
readonly context: PickerContext<T>;
};

/**
* Compressed version of PickerSession where the context is stored as binary data.
* This reduces memory usage when storing multiple sessions.
* @template T - The type of item detail in the picker
*/
export type PickerSessionCompressed<T extends Detail> =
& Omit<PickerSession<T>, "context">
& {
/** Brotli-compressed binary representation of the context */
context: Uint8Array;
};

/**
* Compresses a picker session by converting its context to brotli-compressed binary data.
* This is used internally to reduce memory usage when storing sessions.
* @template T - The type of item detail in the picker
* @param session - The session to compress
* @returns A promise that resolves to the compressed session
*/
async function compressPickerSession<T extends Detail>(
session: PickerSession<T>,
): Promise<PickerSessionCompressed<T>> {
const encoder = new TextEncoder();
// Convert Set to Array for JSON serialization
const contextForSerialization = {
...session.context,
selection: Array.from(session.context.selection),
};
return {
...session,
context: await brotli.compress(
encoder.encode(JSON.stringify(contextForSerialization)),
),
};
}

/**
* Decompresses a picker session by converting its binary context back to structured data.
* @template T - The type of item detail in the picker
* @param compressed - The compressed session to decompress
* @returns A promise that resolves to the decompressed session
*/
export async function decompressPickerSession<T extends Detail>(
compressed: PickerSessionCompressed<T>,
): Promise<PickerSession<T>> {
const decoder = new TextDecoder();
const decompressedContext = JSON.parse(
decoder.decode(await brotli.uncompress(compressed.context)),
);
// Convert selection array back to Set
return {
...compressed,
context: {
...decompressedContext,
selection: new Set(decompressedContext.selection),
},
};
}

/**
* Lists all stored picker sessions in reverse chronological order (newest first).
* @returns A readonly array of compressed sessions
* @returns A readonly array of sessions
*/
export function listPickerSessions(): readonly PickerSessionCompressed<
Detail
>[] {
export function listPickerSessions(): readonly PickerSession<Detail>[] {
return sessions.slice().reverse(); // Return a copy in reverse order
}

/**
* Saves a picker session to the in-memory storage.
* The session is compressed before storage to reduce memory usage.
* If the storage exceeds MAX_SESSION_COUNT, the oldest session is removed.
* @template T - The type of item detail in the picker
* @param session - The session to save
* @returns A promise that resolves when the session is saved
*/
export async function savePickerSession<T extends Detail>(
export function savePickerSession<T extends Detail>(
session: PickerSession<T>,
): Promise<void> {
const compressed = await compressPickerSession(session);
sessions.push(compressed);
): void {
sessions.push(session);
if (sessions.length > MAX_SESSION_COUNT) {
sessions.shift(); // Keep only the last MAX_SESSION_COUNT sessions
}
Expand All @@ -130,29 +65,25 @@ export type LoadPickerSessionOptions = {
* @template T - The type of item detail in the picker
* @param indexFromLatest - The index from the latest session (0 = most recent, 1 = second most recent, etc.)
* @param options - Options to filter sessions
* @returns A promise that resolves to the decompressed session, or undefined if not found
* @returns The session, or undefined if not found
* @example
* ```ts
* // Load the most recent session
* const session1 = await loadPickerSession();
* const session1 = loadPickerSession();
*
* // Load the second most recent session
* const session2 = await loadPickerSession({ number: 2 });
* const session2 = loadPickerSession({ number: 2 });
*
* // Load the most recent session with name "file"
* const session3 = await loadPickerSession({ name: "file", number: 1 });
* const session3 = loadPickerSession({ name: "file", number: 1 });
* ```
*/
export async function loadPickerSession<T extends Detail>(
export function loadPickerSession<T extends Detail>(
{ name, number: indexFromLatest }: LoadPickerSessionOptions = {},
): Promise<PickerSession<T> | undefined> {
): PickerSession<T> | undefined {
const filteredSessions = name
? sessions.filter((s) => s.name === name)
: sessions;
const index = filteredSessions.length - (indexFromLatest ?? 1);
const compressed = filteredSessions.at(index);
if (!compressed) {
return undefined;
}
return await decompressPickerSession(compressed);
return filteredSessions.at(index) as PickerSession<T> | undefined;
}
Loading