diff --git a/.github/workflows/jsr.yml b/.github/workflows/jsr.yml index fd35a6b..e18ca19 100644 --- a/.github/workflows/jsr.yml +++ b/.github/workflows/jsr.yml @@ -19,7 +19,7 @@ jobs: - uses: actions/checkout@v4 with: fetch-depth: 0 - - uses: denoland/setup-deno@v1 + - uses: denoland/setup-deno@v2 with: deno-version: ${{ env.DENO_VERSION }} - name: Publish diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 0e5c817..a58640a 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -26,14 +26,13 @@ jobs: runner: - ubuntu-latest deno_version: - - "1.x" - "2.x" runs-on: ${{ matrix.runner }} steps: - run: git config --global core.autocrlf false if: runner.os == 'Windows' - uses: actions/checkout@v4 - - uses: denoland/setup-deno@v1.1.4 + - uses: denoland/setup-deno@v2 with: deno-version: "${{ matrix.deno_version }}" - uses: actions/cache@v4 @@ -61,7 +60,6 @@ jobs: - macos-latest - ubuntu-latest deno_version: - - "1.x" - "2.x" host_version: - vim: "v9.1.0448" @@ -74,7 +72,7 @@ jobs: - uses: actions/checkout@v4 - - uses: denoland/setup-deno@v1.1.4 + - uses: denoland/setup-deno@v2 with: deno-version: "${{ matrix.deno_version }}" @@ -132,9 +130,9 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - uses: denoland/setup-deno@v1 + - uses: denoland/setup-deno@v2 with: - deno-version: ${{ env.DENO_VERSION }} + deno-version: "2.x" - name: Publish (dry-run) run: | deno publish --dry-run diff --git a/README.md b/README.md index b5ce520..9b44fc3 100644 --- a/README.md +++ b/README.md @@ -1,172 +1,72 @@ # 🍂 fall-std [![JSR](https://jsr.io/badges/@vim-fall/std)](https://jsr.io/@vim-fall/std) +[![Deno](https://img.shields.io/badge/Deno%202.x-333?logo=deno&logoColor=fff)](#) [![Test](https://github.com/vim-fall/fall-std/actions/workflows/test.yml/badge.svg)](https://github.com/vim-fall/fall-std/actions/workflows/test.yml) [![codecov](https://codecov.io/gh/vim-fall/fall-std/graph/badge.svg?token=FWTFEJT1X1)](https://codecov.io/gh/vim-fall/fall-std) [![MIT License](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE) -Standard library for using [Fall](https://github.com/vim-fall/fall), a +A standard library for using [Fall](https://github.com/vim-fall/fall), a Vim/Neovim Fuzzy Finder plugin powered by -[Denops](https://github.com/vim-denops/denops.vim). - -It is also used to develop extensions of Fall. +[Denops](https://github.com/vim-denops/denops.vim). Users should import this +library in Fall's configuration file (`fall/config.ts`) to use the built-in +extensions and utility functions. ## Usage -```ts -// Import Fall standard library functions and built-in utilities -import { - composeRenderers, - type Entrypoint, - pipeProjectors, -} from "jsr:@vim-fall/std@^0.1.0"; // Fall standard library -import * as builtin from "jsr:@vim-fall/std@^0.1.0/builtin"; // Built-in Fall utilities - -// Define custom actions for file handling, quickfix, and other operations -const myPathActions = { - ...builtin.action.defaultOpenActions, - ...builtin.action.defaultSystemopenActions, - ...builtin.action.defaultCdActions, -}; - -const myQuickfixActions = { - ...builtin.action.defaultQuickfixActions, - "quickfix:qfreplace": builtin.action.quickfix({ - after: "Qfreplace", // Using the "Qfreplace" plugin for replacing text in quickfix - }), -}; - -const myMiscActions = { - ...builtin.action.defaultEchoActions, - ...builtin.action.defaultYankActions, - ...builtin.action.defaultSubmatchActions, -}; - -// Main entry point function for configuring the Fall interface -export const main: Entrypoint = ( - { - defineItemPickerFromSource, // Define item pickers from source data - defineItemPickerFromCurator, // Define item pickers from curators (e.g., Git grep) - refineGlobalConfig, // Refine global settings (e.g., theme and layout) - }, -) => { - // Set up global configuration (layout and theme) - refineGlobalConfig({ - coordinator: builtin.coordinator.separate, // Use the "separate" layout style - theme: builtin.theme.ASCII_THEME, // Apply ASCII-themed UI - }); - - // Configure item pickers for "git-grep", "rg", and "file" sources - defineItemPickerFromCurator( - "git-grep", // Picker for `git grep` results - pipeProjectors( - builtin.curator.gitGrep, // Uses Git to search - builtin.modifier.relativePath, // Show relative paths - ), - { - previewers: [builtin.previewer.file], // Preview file contents - actions: { - ...myPathActions, - ...myQuickfixActions, - ...myMiscActions, - }, - defaultAction: "open", // Default action to open the file - }, - ); - - defineItemPickerFromCurator( - "rg", // Picker for `rg` (ripgrep) results - pipeProjectors( - builtin.curator.rg, // Uses `rg` for searching - builtin.modifier.relativePath, // Modify results to show relative paths - ), - { - previewers: [builtin.previewer.file], // Preview file contents - actions: { - ...myPathActions, - ...myQuickfixActions, - ...myMiscActions, - }, - defaultAction: "open", // Default action to open the file - }, - ); - - // File picker configuration with exclusion filters for unwanted directories - defineItemPickerFromSource( - "file", // Picker for files with exclusions - pipeProjectors( - builtin.source.file({ - excludes: [ - /.*\/node_modules\/.*/, // Exclude node_modules - /.*\/.git\/.*/, // Exclude Git directories - /.*\/.svn\/.*/, // Exclude SVN directories - /.*\/.hg\/.*/, // Exclude Mercurial directories - /.*\/.DS_Store$/, // Exclude macOS .DS_Store files - ], - }), - builtin.modifier.relativePath, // Show relative paths - ), - { - matchers: [builtin.matcher.fzf], // Use fuzzy search matcher - renderers: [composeRenderers( - builtin.renderer.smartPath, // Render smart paths - builtin.renderer.nerdfont, // Render with NerdFont (requires NerdFont in terminal) - )], - previewers: [builtin.previewer.file], // Preview file contents - actions: { - ...myPathActions, - ...myQuickfixActions, - ...myMiscActions, - }, - defaultAction: "open", // Default action to open the file - }, - ); - - // Configure "line" picker for selecting lines in a file - defineItemPickerFromSource("line", builtin.source.line, { - matchers: [builtin.matcher.fzf], // Use fuzzy search matcher - previewers: [builtin.previewer.buffer], // Preview the buffer content - actions: { - ...myQuickfixActions, - ...myMiscActions, - ...builtin.action.defaultOpenActions, - ...builtin.action.defaultBufferActions, - }, - defaultAction: "open", // Default action to open the line - }); - - // Configure "buffer" picker for loaded buffers - defineItemPickerFromSource( - "buffer", - builtin.source.buffer({ filter: "bufloaded" }), // Show only loaded buffers - { - matchers: [builtin.matcher.fzf], // Use fuzzy search matcher - previewers: [builtin.previewer.buffer], // Preview the buffer content - actions: { - ...myQuickfixActions, - ...myMiscActions, - ...builtin.action.defaultOpenActions, - ...builtin.action.defaultBufferActions, - }, - defaultAction: "open", // Default action to open the buffer - }, - ); - - // Configure "help" picker for help tags - defineItemPickerFromSource("help", builtin.source.helptag, { - matchers: [builtin.matcher.fzf], // Use fuzzy search matcher - previewers: [builtin.previewer.helptag], // Preview help tag content - actions: { - ...myMiscActions, - ...builtin.action.defaultHelpActions, // Help actions - }, - defaultAction: "help", // Default action is to show help - }); -}; +### Extensions + +Extensions are available in the `builtin` directory. You can access them like +this: + +```typescript +import * as builtin from "jsr:@vim-fall/std/builtin"; + +// Display all curators +console.log(builtin.curator); + +// Display all sources +console.log(builtin.source); + +// Display all actions +console.log(builtin.action); + +// ... +``` + +### Utility Functions + +Utility functions are defined in the root directory. You can access them like +this: + +```typescript +import * as builtin from "jsr:@vim-fall/std/builtin"; +import * as std from "jsr:@vim-fall/std"; + +// Refine a source with refiners +const refinedSource = std.refineSource( + // File source + builtin.source.file, + // Refiner to filter files based on the current working directory + builtin.refiner.cwd, + // Refiner to modify item paths to be relative from the current working directory + builtin.refiner.relativePath, + // ... +); ``` +### More Extensions + +For more extensions (including integrations with other Vim plugins, non-standard +workflows, etc.), check out +[vim-fall/fall-extra](https://github.com/vim-fall/fall-extra) +([@vim-fall/extra](https://jsr.io/@vim-fall/extra)). + ## License -The code in this repository follows the MIT license, as detailed in -[LICENSE](./LICENSE). Contributors must agree that any modifications submitted -to this repository also adhere to the license. +The code in this repository follows the MIT license, as detailed in the LICENSE. +Contributors must agree that any modifications submitted to this repository also +adhere to the license. + +This Markdown version will render properly when used in a Markdown environment. +Let me know if you'd like to adjust anything further! diff --git a/action.ts b/action.ts index 1041c01..c6e8b04 100644 --- a/action.ts +++ b/action.ts @@ -1,6 +1,9 @@ +export type * from "@vim-fall/core/action"; + import type { Denops } from "@denops/std"; import type { Action, InvokeParams } from "@vim-fall/core/action"; +import type { Detail, DetailUnit } from "./item.ts"; import type { Promish } from "./util/_typeutil.ts"; import { type DerivableArray, deriveArray } from "./util/derivable.ts"; @@ -10,16 +13,14 @@ import { type DerivableArray, deriveArray } from "./util/derivable.ts"; * @param invoke - The function to invoke the action. * @returns The defined action. */ -export function defineAction( +export function defineAction( invoke: ( denops: Denops, params: InvokeParams, options: { signal?: AbortSignal }, ) => Promish, ): Action { - return { - invoke, - }; + return { invoke }; } /** @@ -30,10 +31,9 @@ export function defineAction( * @param actions - The actions to compose. * @returns The composed action. */ -export function composeActions< - T, - A extends DerivableArray<[Action, ...Action[]]>, ->(...actions: A): Action { +export function composeActions( + ...actions: DerivableArray<[Action, ...Action[]]> +): Action { return { invoke: async (denops, params, options) => { for (const action of deriveArray(actions)) { @@ -42,5 +42,3 @@ export function composeActions< }, }; } - -export type * from "@vim-fall/core/action"; diff --git a/action_test.ts b/action_test.ts index c30c381..783ea13 100644 --- a/action_test.ts +++ b/action_test.ts @@ -1,32 +1,99 @@ import { assertEquals } from "@std/assert"; -import { assertType, type IsExact } from "@std/testing/types"; +import { type AssertTrue, assertType, type IsExact } from "@std/testing/types"; import { DenopsStub } from "@denops/test/stub"; -import { type Action, composeActions, defineAction } from "./action.ts"; +import type { DetailUnit } from "./item.ts"; +import { + type Action, + composeActions, + defineAction, + type InvokeParams, +} from "./action.ts"; -Deno.test("defineAction", () => { - const action = defineAction(async () => {}); - assertEquals(typeof action.invoke, "function"); - assertType>>(true); -}); +Deno.test("defineAction", async (t) => { + await t.step("without type contraint", () => { + const action = defineAction((_denops, params) => { + type _ = AssertTrue>>; + }); + assertType>>(true); + }); -Deno.test("composeActions", async () => { - const results: string[] = []; - const action1 = defineAction(() => { - results.push("action1"); + await t.step("with type contraint", () => { + type C = { a: string }; + const action = defineAction((_denops, params) => { + type _ = AssertTrue>>; + }); + assertType>>(true); }); - const action2 = defineAction(() => { - results.push("action2"); +}); + +Deno.test("composeActions", async (t) => { + await t.step("with bear actions", async (t) => { + await t.step("actions are invoked in order", async () => { + const results: string[] = []; + const action1 = defineAction(() => void results.push("action1")); + const action2 = defineAction(() => void results.push("action2")); + const action3 = defineAction(() => void results.push("action3")); + const action = composeActions(action2, action1, action3); + const denops = new DenopsStub(); + const params = { + item: undefined, + selectedItems: [], + filteredItems: [], + }; + await action.invoke(denops, params, {}); + assertEquals(results, ["action2", "action1", "action3"]); + }); + + await t.step("without type contraint", () => { + const action1 = defineAction(() => {}); + const action2 = defineAction(() => {}); + const action3 = defineAction(() => {}); + const action = composeActions(action2, action1, action3); + assertType>>(true); + }); + + await t.step("with type contraint", () => { + type C = { a: string }; + const action1 = defineAction(() => {}); + const action2 = defineAction(() => {}); + const action3 = defineAction(() => {}); + const action = composeActions(action2, action1, action3); + assertType>>(true); + }); }); - const action3 = defineAction(() => { - results.push("action3"); + + await t.step("with derivable actions", async (t) => { + await t.step("actions are invoked in order", async () => { + const results: string[] = []; + const action1 = () => defineAction(() => void results.push("action1")); + const action2 = () => defineAction(() => void results.push("action2")); + const action3 = () => defineAction(() => void results.push("action3")); + const action = composeActions(action2, action1, action3); + const denops = new DenopsStub(); + const params = { + item: undefined, + selectedItems: [], + filteredItems: [], + }; + await action.invoke(denops, params, {}); + assertEquals(results, ["action2", "action1", "action3"]); + }); + + await t.step("without type contraint", () => { + const action1 = () => defineAction(() => {}); + const action2 = () => defineAction(() => {}); + const action3 = () => defineAction(() => {}); + const action = composeActions(action2, action1, action3); + assertType>>(true); + }); + + await t.step("with type contraint", () => { + type C = { a: string }; + const action1 = () => defineAction(() => {}); + const action2 = () => defineAction(() => {}); + const action3 = () => defineAction(() => {}); + const action = composeActions(action2, action1, action3); + assertType>>(true); + }); }); - const action = composeActions(action2, action1, action3); - const denops = new DenopsStub(); - const params = { - item: undefined, - selectedItems: [], - filteredItems: [], - }; - await action.invoke(denops, params, {}); - assertEquals(results, ["action2", "action1", "action3"]); }); diff --git a/builtin/action/buffer.ts b/builtin/action/buffer.ts index 3ea6418..294e0a5 100644 --- a/builtin/action/buffer.ts +++ b/builtin/action/buffer.ts @@ -8,12 +8,6 @@ type Detail = { path: string; }; -/** - * Retrieves the appropriate attribute (either `bufname` or `path`) from the item's detail. - * - * @param item - The item containing either a `bufname` or a `path`. - * @returns The `path` if present; otherwise, the `bufname`. - */ function attrGetter({ detail }: IdItem): string { if ("path" in detail) { return detail.path; @@ -22,9 +16,6 @@ function attrGetter({ detail }: IdItem): string { } } -/** - * Unloads the buffer without deleting it. - */ export const bunload: Action = cmd({ attrGetter, immediate: true, @@ -33,9 +24,6 @@ export const bunload: Action = cmd({ fnameescape: true, }); -/** - * Deletes the buffer, removing it from the buffer list. - */ export const bdelete: Action = cmd({ attrGetter, immediate: true, @@ -44,9 +32,6 @@ export const bdelete: Action = cmd({ fnameescape: true, }); -/** - * Wipes out the buffer, clearing it from memory. - */ export const bwipeout: Action = cmd({ attrGetter, immediate: true, @@ -55,9 +40,6 @@ export const bwipeout: Action = cmd({ fnameescape: true, }); -/** - * Opens the buffer in a new tab, writes any changes, and then closes the tab. - */ export const write: Action = cmd({ attrGetter, immediate: true, @@ -66,9 +48,6 @@ export const write: Action = cmd({ fnameescape: true, }); -/** - * A collection of default actions for buffer management. - */ export const defaultBufferActions: { bunload: Action; bdelete: Action; diff --git a/builtin/action/cmd.ts b/builtin/action/cmd.ts index 532d174..7d427f6 100644 --- a/builtin/action/cmd.ts +++ b/builtin/action/cmd.ts @@ -3,12 +3,12 @@ import * as fn from "@denops/std/function"; import { input } from "@denops/std/helper/input"; import { dirname } from "@std/path/dirname"; -import type { IdItem } from "../../item.ts"; +import type { Detail, DetailUnit, IdItem } from "../../item.ts"; import { type Action, defineAction } from "../../action.ts"; type Restriction = "file" | "directory" | "directory-or-parent" | "buffer"; -type Options = { +export type CmdOptions = { /** * Function to retrieve the attribute from an item. Defaults to `item.value`. */ @@ -41,7 +41,9 @@ type Options = { * @param options - Configuration options for the command execution. * @returns An action that executes the command. */ -export function cmd(options: Options = {}): Action { +export function cmd( + options: CmdOptions = {}, +): Action { const attrGetter = options.attrGetter ?? ((item) => item.value); const immediate = options.immediate ?? false; const template = options.template ?? "{}"; @@ -152,7 +154,7 @@ async function execute( * Default command actions. */ export const defaultCmdActions: { - cmd: Action; + cmd: Action; } = { cmd: cmd(), }; diff --git a/builtin/action/echo.ts b/builtin/action/echo.ts index 1b716c4..a2aab41 100644 --- a/builtin/action/echo.ts +++ b/builtin/action/echo.ts @@ -7,7 +7,7 @@ import { type Action, defineAction } from "../../action.ts"; * * @returns An action that logs the item. */ -export function echo(): Action { +export function echo(): Action { return defineAction((_denops, { item }, _options) => { console.log(JSON.stringify(item, null, 2)); }); @@ -17,7 +17,7 @@ export function echo(): Action { * Default action for echoing items to the console. */ export const defaultEchoActions: { - echo: Action; + echo: Action; } = { echo: echo(), }; diff --git a/builtin/action/noop.ts b/builtin/action/noop.ts index 9cf9bb5..f1bfe6b 100644 --- a/builtin/action/noop.ts +++ b/builtin/action/noop.ts @@ -7,7 +7,7 @@ import { type Action, defineAction } from "../../action.ts"; * * @returns An action that does nothing. */ -export function noop(): Action { +export function noop(): Action { return defineAction(() => {}); } @@ -15,7 +15,7 @@ export function noop(): Action { * Default action set containing the noop action. */ export const defaultNoopActions: { - noop: Action; + noop: Action; } = { noop: noop(), }; diff --git a/builtin/action/open.ts b/builtin/action/open.ts index 637c376..9887813 100644 --- a/builtin/action/open.ts +++ b/builtin/action/open.ts @@ -3,7 +3,17 @@ import * as fn from "@denops/std/function"; import { type Action, defineAction } from "../../action.ts"; -type Options = { +type Detail = { + path: string; + line?: number; + column?: number; +} | { + bufname: string; + line?: number; + column?: number; +}; + +export type OpenOptions = { /** * Specifies if the command should be executed with `!`. */ @@ -26,59 +36,51 @@ type Options = { splitter?: string; }; -type Detail = { - path: string; - line?: number; - column?: number; -} | { - bufname: string; - line?: number; - column?: number; -}; - /** * Creates an action that opens a file or buffer in a specified way. * * @param options - Configuration options for opening files or buffers. * @returns An action that opens the specified items. */ -export function open(options: Options = {}): Action { +export function open(options: OpenOptions = {}): Action { const bang = options.bang ?? false; const mods = options.mods ?? ""; const cmdarg = options.cmdarg ?? ""; const opener = options.opener ?? "edit"; const splitter = options.splitter ?? opener; - return defineAction(async (denops, { item, selectedItems }, { signal }) => { - const items = selectedItems ?? [item]; - let currentOpener = opener; + return defineAction( + async (denops, { item, selectedItems }, { signal }) => { + const items = selectedItems ?? [item]; + let currentOpener = opener; - for (const item of items.filter((v) => !!v)) { - const expr = "bufname" in item.detail - ? item.detail.bufname - : item.detail.path; + for (const item of items.filter((v) => !!v)) { + const expr = "bufname" in item.detail + ? item.detail.bufname + : item.detail.path; - const info = await buffer.open(denops, expr, { - bang, - mods, - cmdarg, - opener: currentOpener, - }); - signal?.throwIfAborted(); + const info = await buffer.open(denops, expr, { + bang, + mods, + cmdarg, + opener: currentOpener, + }); + signal?.throwIfAborted(); - currentOpener = splitter; + currentOpener = splitter; - if (item.detail.line || item.detail.column) { - const line = item.detail.line ?? 1; - const column = item.detail.column ?? 1; - await fn.win_execute( - denops, - info.winid, - `silent! normal! ${line}G${column}|zv`, - ); + if (item.detail.line || item.detail.column) { + const line = item.detail.line ?? 1; + const column = item.detail.column ?? 1; + await fn.win_execute( + denops, + info.winid, + `silent! normal! ${line}G${column}|zv`, + ); + } } - } - }); + }, + ); } /** diff --git a/builtin/action/quickfix.ts b/builtin/action/quickfix.ts index d7a037e..95cede6 100644 --- a/builtin/action/quickfix.ts +++ b/builtin/action/quickfix.ts @@ -1,6 +1,20 @@ import * as fn from "@denops/std/function"; import { type Action, defineAction } from "../../action.ts"; +type Detail = { + path: string; + line?: number; + column?: number; + length?: number; + context?: string; +} | { + bufname: string; + line?: number; + column?: number; + length?: number; + context?: string; +}; + type What = { context?: unknown; id?: number; @@ -9,7 +23,7 @@ type What = { title?: string; }; -type Options = { +export type QuickfixOptions = { /** * Specifies additional parameters for the quickfix list, such as `id`, `idx`, `nr`, etc. */ @@ -28,34 +42,20 @@ type Options = { after?: string; }; -type Detail = { - path: string; - line?: number; - column?: number; - length?: number; - context?: string; -} | { - bufname: string; - line?: number; - column?: number; - length?: number; - context?: string; -}; - /** * Creates an action that populates the quickfix list with specified items. * * @param options - Configuration options for setting the quickfix list. * @returns An action that sets the quickfix list and optionally opens it. */ -export function quickfix( - options: Options = {}, -): Action { +export function quickfix( + options: QuickfixOptions = {}, +): Action { const what = options.what ?? {}; const action = options.action ?? " "; const after = options.after ?? "copen"; - return defineAction( + return defineAction( async (denops, { selectedItems, filteredItems }, { signal }) => { const source = selectedItems ?? filteredItems; diff --git a/builtin/action/submatch.ts b/builtin/action/submatch.ts index 0239de3..8746342 100644 --- a/builtin/action/submatch.ts +++ b/builtin/action/submatch.ts @@ -1,5 +1,7 @@ import type { Coordinator, + Detail, + DetailUnit, Matcher, Previewer, Renderer, @@ -23,22 +25,7 @@ import { fzf } from "../matcher/fzf.ts"; import { substring } from "../matcher/substring.ts"; import { regexp } from "../matcher/regexp.ts"; -type Context = { - /** - * The screen size. - */ - readonly screen: Size; - /** - * The global configuration. - */ - readonly globalConfig: GlobalConfig; - /** - * The picker parameters. - */ - readonly pickerParams: ItemPickerParams & GlobalConfig; -}; - -type Options = { +export type SubmatchOptions = { /** * Actions available for the submatch picker. */ @@ -69,6 +56,21 @@ type Options = { theme?: Derivable | null; }; +type Context = { + /** + * The screen size. + */ + readonly screen: Size; + /** + * The global configuration. + */ + readonly globalConfig: GlobalConfig; + /** + * The picker parameters. + */ + readonly pickerParams: ItemPickerParams & GlobalConfig; +}; + /** * Creates an action to perform submatching on items using specified matchers. * @@ -79,9 +81,9 @@ type Options = { * @param options - Additional configuration options for the picker. * @returns An action that performs submatching. */ -export function submatch( +export function submatch( matchers: DerivableArray<[Matcher, ...Matcher[]]>, - options: Options = {}, + options: SubmatchOptions = {}, ): Action { return defineAction( async (denops, { selectedItems, filteredItems, ...params }, { signal }) => { @@ -146,7 +148,9 @@ export function submatch( * @returns The extracted context. * @throws If the required context is not present. */ -function getContext(params: unknown): Context { +function getContext( + params: unknown, +): Context { if (params && typeof params === "object" && "_submatchContext" in params) { return params._submatchContext as Context; } @@ -159,9 +163,9 @@ function getContext(params: unknown): Context { * Default submatching actions with common matchers. */ export const defaultSubmatchActions: { - "sub:fzf": Action; - "sub:substring": Action; - "sub:regexp": Action; + "sub:fzf": Action; + "sub:substring": Action; + "sub:regexp": Action; } = { "sub:fzf": submatch([fzf]), "sub:substring": submatch([substring]), diff --git a/builtin/action/systemopen.ts b/builtin/action/systemopen.ts index a135a09..c19d779 100644 --- a/builtin/action/systemopen.ts +++ b/builtin/action/systemopen.ts @@ -13,15 +13,17 @@ type Detail = { * * @returns An action that opens each selected item's path. */ -export function systemopen(): Action { - return defineAction(async (_denops, { item, selectedItems }, { signal }) => { - const items = selectedItems ?? [item]; +export function systemopen(): Action { + return defineAction( + async (_denops, { item, selectedItems }, { signal }) => { + const items = selectedItems ?? [item]; - for (const item of items.filter((v) => !!v)) { - await systemopen_(item.detail.path); - signal?.throwIfAborted(); - } - }); + for (const item of items.filter((v) => !!v)) { + await systemopen_(item.detail.path); + signal?.throwIfAborted(); + } + }, + ); } /** diff --git a/builtin/action/yank.ts b/builtin/action/yank.ts index 47c1c28..c0b4f8e 100644 --- a/builtin/action/yank.ts +++ b/builtin/action/yank.ts @@ -7,7 +7,7 @@ import { type Action, defineAction } from "../../action.ts"; * * @returns An action that yanks the values of selected items. */ -export function yank(): Action { +export function yank(): Action { return defineAction(async (denops, { item, selectedItems }, { signal }) => { const items = selectedItems ?? [item]; const value = items.filter((v) => !!v).map((item) => item.value).join("\n"); @@ -22,7 +22,7 @@ export function yank(): Action { * Default yank action set, including the `yank` action. */ export const defaultYankActions: { - yank: Action; + yank: Action; } = { yank: yank(), }; diff --git a/builtin/coordinator/compact.ts b/builtin/coordinator/compact.ts index eba37f2..a8cd799 100644 --- a/builtin/coordinator/compact.ts +++ b/builtin/coordinator/compact.ts @@ -13,7 +13,7 @@ const HEIGHT_MIN = 5; const HEIGHT_MAX = 70; const PREVIEW_RATIO = 0.6; -type Options = { +export type CompactOptions = { /** * If true, hides the preview component. */ @@ -82,7 +82,7 @@ type Options = { * @returns A coordinator with specified layout and style functions. */ export function compact( - options: Options = {}, + options: CompactOptions = {}, ): Coordinator { const { hidePreview = false, diff --git a/builtin/coordinator/modern.ts b/builtin/coordinator/modern.ts index 83e6902..8cfa9d8 100644 --- a/builtin/coordinator/modern.ts +++ b/builtin/coordinator/modern.ts @@ -13,7 +13,7 @@ const HEIGHT_MIN = 5; const HEIGHT_MAX = 70; const PREVIEW_RATIO = 0.6; -type Options = { +export type ModernOptions = { /** * If true, hides the preview component. */ @@ -79,7 +79,7 @@ type Options = { * mainWidth previewWidth * ``` */ -export function modern(options: Options = {}): Coordinator { +export function modern(options: ModernOptions = {}): Coordinator { const { hidePreview = false, widthRatio = WIDTH_RATIO, diff --git a/builtin/coordinator/separate.ts b/builtin/coordinator/separate.ts index 48f8dcd..539d042 100644 --- a/builtin/coordinator/separate.ts +++ b/builtin/coordinator/separate.ts @@ -9,7 +9,7 @@ const HEIGHT_MIN = 5; const HEIGHT_MAX = 70; const PREVIEW_RATIO = 0.6; -type Options = { +export type SeparateOptions = { /** * If true, hides the preview component. */ @@ -74,7 +74,7 @@ type Options = { * mainWidth previewWidth * ``` */ -export function separate(options: Options = {}): Coordinator { +export function separate(options: SeparateOptions = {}): Coordinator { const { hidePreview = false, widthRatio = WIDTH_RATIO, diff --git a/builtin/curator/git_grep.ts b/builtin/curator/git_grep.ts index bd185d8..d1ff9f5 100644 --- a/builtin/curator/git_grep.ts +++ b/builtin/curator/git_grep.ts @@ -3,10 +3,7 @@ import * as fn from "@denops/std/function"; import { type Curator, defineCurator } from "../../curator.ts"; -/** - * Detail information for each result returned by `git grep`. - */ -type GitGrepDetail = { +type Detail = { path: string; line: number; column: number; @@ -28,8 +25,8 @@ const pattern = new RegExp("^(.*?):(\\d+):(\\d+):(.*)$"); * * @returns A Curator that yields search results in the form of `GitGrepDetail`. */ -export function gitGrep(): Curator { - return defineCurator( +export function gitGrep(): Curator { + return defineCurator( async function* (denops, { query }, { signal }) { // Get the current working directory in Vim/Neovim const cwd = await fn.getcwd(denops); diff --git a/builtin/curator/grep.ts b/builtin/curator/grep.ts index 40a3043..a12422a 100644 --- a/builtin/curator/grep.ts +++ b/builtin/curator/grep.ts @@ -7,7 +7,7 @@ import { type Curator, defineCurator } from "../../curator.ts"; /** * Detail information for each result returned by `grep`. */ -type GrepDetail = { +type Detail = { path: string; line: number; context: string; @@ -28,9 +28,9 @@ const pattern = new RegExp("^(.*?):(\\d+):(.*)$"); * * @returns A Curator that yields search results in the form of `GrepDetail`. */ -export function grep(): Curator { +export function grep(): Curator { let root: string; - return defineCurator( + return defineCurator( async function* (denops, { args, query }, { signal }) { // Determine the root directory for the grep command root ??= await getAbsolutePathOf(denops, args[0] ?? ".", signal); diff --git a/builtin/curator/noop.ts b/builtin/curator/noop.ts index cfcfa0d..72b8b95 100644 --- a/builtin/curator/noop.ts +++ b/builtin/curator/noop.ts @@ -1,3 +1,4 @@ +import type { DetailUnit } from "../../item.ts"; import { type Curator, defineCurator } from "../../curator.ts"; /** @@ -8,6 +9,6 @@ import { type Curator, defineCurator } from "../../curator.ts"; * * @returns A Curator that yields nothing. */ -export function noop(): Curator { - return defineCurator(async function* () {}); +export function noop(): Curator { + return defineCurator(async function* () {}); } diff --git a/builtin/curator/rg.ts b/builtin/curator/rg.ts index e9681a6..e999e5d 100644 --- a/builtin/curator/rg.ts +++ b/builtin/curator/rg.ts @@ -4,10 +4,7 @@ import { TextLineStream } from "@std/streams/text-line-stream"; import { type Curator, defineCurator } from "../../curator.ts"; -/** - * Detail information for each result returned by `rg` (ripgrep). - */ -type RgDetail = { +type Detail = { path: string; line: number; column: number; @@ -29,9 +26,9 @@ const pattern = new RegExp("^(.*?):(\\d+):(\\d+):(.*)$"); * * @returns A Curator that yields search results in the form of `RgDetail`. */ -export function rg(): Curator { +export function rg(): Curator { let root: string; - return defineCurator( + return defineCurator( async function* (denops, { args, query }, { signal }) { // Determine the root directory for the rg command root ??= await getAbsolutePathOf(denops, args[0] ?? ".", signal); diff --git a/builtin/filter/cwd.ts b/builtin/filter/cwd.ts deleted file mode 100644 index 5c0238a..0000000 --- a/builtin/filter/cwd.ts +++ /dev/null @@ -1,33 +0,0 @@ -import * as fn from "@denops/std/function"; - -import { defineProjector, type Projector } from "../../projector.ts"; - -/** - * Represents detailed information for each item, specifically the file path. - */ -type Detail = { - path: string; -}; - -/** - * Creates a Projector that filters items based on the current working directory. - * - * This Projector yields only those items whose `path` is within the current working directory. - * - * @returns A Projector that filters items according to the current working directory. - */ -export function cwd(): Projector { - return defineProjector(async function* (denops, { items }, { signal }) { - // Retrieve the current working directory - const cwd = await fn.getcwd(denops); - signal?.throwIfAborted(); - - // Yield each item that matches the current working directory - for await (const item of items) { - signal?.throwIfAborted(); - if (item.detail.path.startsWith(cwd)) { - yield item; - } - } - }); -} diff --git a/builtin/filter/exists.ts b/builtin/filter/exists.ts deleted file mode 100644 index 3e80015..0000000 --- a/builtin/filter/exists.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { exists as exists_ } from "@std/fs/exists"; - -import { defineProjector, type Projector } from "../../projector.ts"; - -/** - * Represents detailed information for each item, specifically the file path. - */ -type Detail = { - path: string; -}; - -/** - * Creates a Projector that filters items based on file existence. - * - * This Projector checks each item's `path` and yields only those items - * where the path exists in the filesystem. - * - * @returns A Projector that filters items according to file existence. - */ -export function exists(): Projector { - return defineProjector(async function* (_denops, { items }, { signal }) { - // Check each item's path for existence and yield it if the file exists - for await (const item of items) { - if (await exists_(item.detail.path)) { - yield item; - } - // Abort the iteration if the signal is triggered - signal?.throwIfAborted(); - } - }); -} diff --git a/builtin/filter/noop.ts b/builtin/filter/noop.ts deleted file mode 100644 index ec8b5f5..0000000 --- a/builtin/filter/noop.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { defineProjector, type Projector } from "../../projector.ts"; - -/** - * A no-operation (noop) Projector. - * - * This Projector does nothing and yields no items. It can be used as a placeholder - * or a default value where a Projector is required but no action is needed. - * - * @returns A Projector that yields nothing. - */ -export function noop(): Projector { - return defineProjector(async function* () {}); -} diff --git a/builtin/matcher/fzf.ts b/builtin/matcher/fzf.ts index 54bb93a..547fd62 100644 --- a/builtin/matcher/fzf.ts +++ b/builtin/matcher/fzf.ts @@ -11,7 +11,7 @@ import { defineMatcher, type Matcher } from "../../matcher.ts"; * - `sort`: Enables sorting of results. * - `forward`: Controls the search direction (forward or backward). */ -type Options = { +export type FzfOptions = { casing?: "smart-case" | "case-sensitive" | "case-insensitive"; normalize?: boolean; sort?: boolean; @@ -28,7 +28,7 @@ type Options = { * @param options - Configuration options for FZF matching. * @returns A Matcher that performs fuzzy matching on items. */ -export function fzf(options: Options = {}): Matcher { +export function fzf(options: FzfOptions = {}): Matcher { const casing = options.casing ?? "smart-case"; const normalize = options.normalize ?? true; const sort = options.sort ?? true; @@ -38,8 +38,8 @@ export function fzf(options: Options = {}): Matcher { // Split query into individual terms, ignoring empty strings const terms = query.split(/\s+/).filter((v) => v.length > 0); - // Function to filter items for a given term - const filter = async (items: readonly IdItem[], term: string) => { + // deno-lint-ignore no-explicit-any + const filter = async (items: readonly IdItem[], term: string) => { const fzf = new AsyncFzf(items, { selector: (v) => v.value, casing, diff --git a/builtin/matcher/noop.ts b/builtin/matcher/noop.ts index d7d3ff1..3165905 100644 --- a/builtin/matcher/noop.ts +++ b/builtin/matcher/noop.ts @@ -8,6 +8,6 @@ import { defineMatcher, type Matcher } from "../../matcher.ts"; * * @returns A Matcher that yields nothing. */ -export function noop(): Matcher { - return defineMatcher(async function* () {}); +export function noop(): Matcher { + return defineMatcher(async function* () {}); } diff --git a/builtin/matcher/regexp.ts b/builtin/matcher/regexp.ts index 3070c7e..37756c9 100644 --- a/builtin/matcher/regexp.ts +++ b/builtin/matcher/regexp.ts @@ -10,7 +10,7 @@ import { getByteLength } from "../../util/stringutil.ts"; * * @returns A Matcher that applies a regular expression filter with decorations. */ -export function regexp(): Matcher { +export function regexp(): Matcher { return defineMatcher(async function* (_denops, { query, items }, { signal }) { // Create a RegExp from the query with global matching enabled const pattern = new RegExp(query, "g"); diff --git a/builtin/matcher/substring.ts b/builtin/matcher/substring.ts index 46f8780..4638703 100644 --- a/builtin/matcher/substring.ts +++ b/builtin/matcher/substring.ts @@ -9,7 +9,7 @@ import { getByteLength } from "../../util/stringutil.ts"; * * If both `smartCase` and `ignoreCase` are true, `ignoreCase` takes precedence. */ -type Options = { +export type SubstringOptions = { smartCase?: boolean; ignoreCase?: boolean; }; @@ -24,7 +24,7 @@ type Options = { * @param options - Matching options to control case sensitivity. * @returns A Matcher that applies substring filtering with decorations. */ -export function substring(options: Options = {}): Matcher { +export function substring(options: SubstringOptions = {}): Matcher { // Determine case sensitivity mode based on options const case_ = options.ignoreCase ? "ignore" @@ -44,7 +44,7 @@ export function substring(options: Options = {}): Matcher { } }; - return defineMatcher( + return defineMatcher( async function* (_denops, { query, items }, { signal }) { const ignoreCase = shouldIgnoreCase(query); const norm = (v: string): string => (ignoreCase ? v.toLowerCase() : v); diff --git a/builtin/mod.ts b/builtin/mod.ts index 6652514..742ec86 100644 --- a/builtin/mod.ts +++ b/builtin/mod.ts @@ -2,10 +2,9 @@ export * as action from "./action/mod.ts"; export * as coordinator from "./coordinator/mod.ts"; export * as curator from "./curator/mod.ts"; -export * as filter from "./filter/mod.ts"; export * as matcher from "./matcher/mod.ts"; -export * as modifier from "./modifier/mod.ts"; export * as previewer from "./previewer/mod.ts"; +export * as refiner from "./refiner/mod.ts"; export * as renderer from "./renderer/mod.ts"; export * as sorter from "./sorter/mod.ts"; export * as source from "./source/mod.ts"; diff --git a/builtin/modifier/mod.ts b/builtin/modifier/mod.ts deleted file mode 100644 index 4e8eb41..0000000 --- a/builtin/modifier/mod.ts +++ /dev/null @@ -1,3 +0,0 @@ -// This file is generated by gen-mod.ts -export * from "./noop.ts"; -export * from "./relative_path.ts"; diff --git a/builtin/modifier/noop.ts b/builtin/modifier/noop.ts deleted file mode 100644 index ec8b5f5..0000000 --- a/builtin/modifier/noop.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { defineProjector, type Projector } from "../../projector.ts"; - -/** - * A no-operation (noop) Projector. - * - * This Projector does nothing and yields no items. It can be used as a placeholder - * or a default value where a Projector is required but no action is needed. - * - * @returns A Projector that yields nothing. - */ -export function noop(): Projector { - return defineProjector(async function* () {}); -} diff --git a/builtin/modifier/relative_path.ts b/builtin/modifier/relative_path.ts deleted file mode 100644 index 0a48e55..0000000 --- a/builtin/modifier/relative_path.ts +++ /dev/null @@ -1,56 +0,0 @@ -import * as fn from "@denops/std/function"; -import { relative } from "@std/path/relative"; - -import type { IdItem } from "../../item.ts"; -import { defineProjector, type Projector } from "../../projector.ts"; - -/** - * Represents item details with a file path. - */ -type Detail = { - path: string; -}; - -/** - * Represents item details after processing, with absolute path added. - */ -type DetailAfter = { - abspath: string; -}; - -/** - * Creates a Projector that converts file paths to relative paths. - * - * This Projector transforms each item's `path` property to a relative path - * based on the current working directory. It also preserves the original - * absolute path in a new `abspath` field in `DetailAfter`. - * - * @returns A Projector that converts absolute paths to relative paths and includes the absolute path as `abspath`. - */ -export function relativePath< - T extends Detail, - U extends T & DetailAfter, ->(): Projector { - return defineProjector(async function* (denops, { items }, { signal }) { - // Get the current working directory - const cwd = await fn.getcwd(denops); - signal?.throwIfAborted(); - - // Convert each item's path to a relative path - for await (const item of items) { - const relpath = relative(cwd, item.detail.path); - const value = item.value.replace(item.detail.path, relpath); - - // Yield item with updated relative path and original absolute path - yield { - ...item, - value, - detail: { - ...item.detail, - path: relpath, - abspath: item.detail.path, - }, - } as IdItem; - } - }); -} diff --git a/builtin/previewer/buffer.ts b/builtin/previewer/buffer.ts index 64bf0b4..ef7ef15 100644 --- a/builtin/previewer/buffer.ts +++ b/builtin/previewer/buffer.ts @@ -4,9 +4,6 @@ import { basename } from "@std/path/basename"; import { definePreviewer, type Previewer } from "../../previewer.ts"; -/** - * Represents details for buffer preview, including buffer number and optional line and column. - */ type Detail = { bufnr: number; line?: number; @@ -21,8 +18,8 @@ type Detail = { * * @returns A Previewer that displays the specified buffer's content. */ -export function buffer(): Previewer { - return definePreviewer(async (denops, { item }, { signal }) => { +export function buffer(): Previewer { + return definePreviewer(async (denops, { item }, { signal }) => { // Retrieve buffer properties in a batch const [bufloaded, bufname, content] = await collect(denops, (denops) => [ fn.bufloaded(denops, item.detail.bufnr), diff --git a/builtin/previewer/file.ts b/builtin/previewer/file.ts index 68e9f01..f6609a2 100644 --- a/builtin/previewer/file.ts +++ b/builtin/previewer/file.ts @@ -7,9 +7,6 @@ import { splitText } from "../../util/stringutil.ts"; const decoder = new TextDecoder("utf-8", { fatal: true }); -/** - * Represents details for file preview, including path and optional line and column. - */ type Detail = { path: string; line?: number; @@ -24,7 +21,7 @@ type Detail = { * * @returns A Previewer that shows the specified file's content or a binary file message. */ -export function file(): Previewer { +export function file(): Previewer { return definePreviewer(async (denops, { item }, { signal }) => { // Resolve the absolute path of the file const abspath = isAbsolute(item.detail.path) diff --git a/builtin/previewer/helptag.ts b/builtin/previewer/helptag.ts index 5f9a30f..fd383ec 100644 --- a/builtin/previewer/helptag.ts +++ b/builtin/previewer/helptag.ts @@ -5,9 +5,6 @@ import { definePreviewer, type Previewer } from "../../previewer.ts"; const helpfileCache = new Map(); -/** - * Represents details for help tag preview, including the helptag and helpfile name. - */ type Detail = { helptag: string; helpfile: string; @@ -22,7 +19,7 @@ type Detail = { * * @returns A Previewer that displays the specified helpfile's content. */ -export function helptag(): Previewer { +export function helptag(): Previewer { return definePreviewer(async (denops, { item }, { signal }) => { // Retrieve runtime paths and load the helpfile content const runtimepaths = (await opt.runtimepath.get(denops)).split(","); diff --git a/builtin/previewer/noop.ts b/builtin/previewer/noop.ts index af50288..63f14bf 100644 --- a/builtin/previewer/noop.ts +++ b/builtin/previewer/noop.ts @@ -8,6 +8,6 @@ import { definePreviewer, type Previewer } from "../../previewer.ts"; * * @returns A Previewer that performs no operation. */ -export function noop(): Previewer { +export function noop(): Previewer { return definePreviewer(() => {}); } diff --git a/builtin/refiner/cwd.ts b/builtin/refiner/cwd.ts new file mode 100644 index 0000000..c73f8e4 --- /dev/null +++ b/builtin/refiner/cwd.ts @@ -0,0 +1,30 @@ +import * as fn from "@denops/std/function"; + +import { defineRefiner, type Refiner } from "../../refiner.ts"; + +type Detail = { + path: string; +}; + +/** + * Creates a Refiner that filters items based on the current working directory. + * + * This Refiner yields only those items whose `path` is within the current working directory. + * + * @returns A Refiner that filters items according to the current working directory. + */ +export function cwd(): Refiner { + return defineRefiner(async function* (denops, { items }, { signal }) { + // Retrieve the current working directory + const cwd = await fn.getcwd(denops); + signal?.throwIfAborted(); + + // Yield each item that matches the current working directory + for await (const item of items) { + signal?.throwIfAborted(); + if (item.detail.path.startsWith(cwd)) { + yield item; + } + } + }); +} diff --git a/builtin/refiner/exists.ts b/builtin/refiner/exists.ts new file mode 100644 index 0000000..bc23241 --- /dev/null +++ b/builtin/refiner/exists.ts @@ -0,0 +1,28 @@ +import { exists as exists_ } from "@std/fs/exists"; + +import { defineRefiner, type Refiner } from "../../refiner.ts"; + +type Detail = { + path: string; +}; + +/** + * Creates a Refiner that filters items based on file existence. + * + * This Refiner checks each item's `path` and yields only those items + * where the path exists in the filesystem. + * + * @returns A Refiner that filters items according to file existence. + */ +export function exists(): Refiner { + return defineRefiner(async function* (_denops, { items }, { signal }) { + // Check each item's path for existence and yield it if the file exists + for await (const item of items) { + if (await exists_(item.detail.path)) { + yield item; + } + // Abort the iteration if the signal is triggered + signal?.throwIfAborted(); + } + }); +} diff --git a/builtin/filter/mod.ts b/builtin/refiner/mod.ts similarity index 80% rename from builtin/filter/mod.ts rename to builtin/refiner/mod.ts index f003489..7900a12 100644 --- a/builtin/filter/mod.ts +++ b/builtin/refiner/mod.ts @@ -3,3 +3,4 @@ export * from "./cwd.ts"; export * from "./exists.ts"; export * from "./noop.ts"; export * from "./regexp.ts"; +export * from "./relative_path.ts"; diff --git a/builtin/refiner/noop.ts b/builtin/refiner/noop.ts new file mode 100644 index 0000000..2e0c52e --- /dev/null +++ b/builtin/refiner/noop.ts @@ -0,0 +1,13 @@ +import { defineRefiner, type Refiner } from "../../refiner.ts"; + +/** + * A no-operation (noop) Refiner. + * + * This Refiner does nothing and yields no items. It can be used as a placeholder + * or a default value where a Refiner is required but no action is needed. + * + * @returns A Refiner that yields nothing. + */ +export function noop(): Refiner { + return defineRefiner(async function* () {}); +} diff --git a/builtin/filter/regexp.ts b/builtin/refiner/regexp.ts similarity index 64% rename from builtin/filter/regexp.ts rename to builtin/refiner/regexp.ts index 75f97df..ab3ec61 100644 --- a/builtin/filter/regexp.ts +++ b/builtin/refiner/regexp.ts @@ -1,4 +1,4 @@ -import { defineProjector, type Projector } from "../../projector.ts"; +import { defineRefiner, type Refiner } from "../../refiner.ts"; /** * Options for filtering items by regular expressions. @@ -8,7 +8,7 @@ import { defineProjector, type Projector } from "../../projector.ts"; * * One of `includes` or `excludes` must be provided, or both can be used together. */ -type Options = { +export type RegexpOptions = { includes: RegExp[]; excludes?: undefined; } | { @@ -20,26 +20,19 @@ type Options = { }; /** - * Represents detailed information for each item, specifically the file path. - */ -type Detail = { - path: string; -}; - -/** - * Creates a Projector that filters items based on regular expression patterns. + * Creates a Refiner that filters items based on regular expression patterns. * - * The `regexp` Projector filters items using `includes` and/or `excludes` patterns. + * The `regexp` Refiner filters items using `includes` and/or `excludes` patterns. * - If `includes` patterns are provided, only items that match at least one pattern are yielded. * - If `excludes` patterns are provided, any item that matches at least one pattern is excluded. * - * @param options - Filtering options specifying `includes` and/or `excludes` patterns. - * @returns A Projector that yields items matching the specified patterns. + * @param options - Refinering options specifying `includes` and/or `excludes` patterns. + * @returns A Refiner that yields items matching the specified patterns. */ -export function regexp( - { includes, excludes }: Readonly, -): Projector { - return defineProjector(async function* (_denops, { items }, { signal }) { +export function regexp( + { includes, excludes }: Readonly, +): Refiner { + return defineRefiner(async function* (_denops, { items }, { signal }) { signal?.throwIfAborted(); // Process each item and yield only those matching the filter conditions diff --git a/builtin/refiner/relative_path.ts b/builtin/refiner/relative_path.ts new file mode 100644 index 0000000..619ca68 --- /dev/null +++ b/builtin/refiner/relative_path.ts @@ -0,0 +1,48 @@ +import * as fn from "@denops/std/function"; +import { relative } from "@std/path/relative"; + +import { defineRefiner, type Refiner } from "../../refiner.ts"; + +type Detail = { + path: string; +}; + +type DetailAfter = { + abspath: string; +}; + +/** + * Creates a Projector that converts file paths to relative paths. + * + * This Projector transforms each item's `path` property to a relative path + * based on the current working directory. It also preserves the original + * absolute path in a new `abspath` field in `DetailAfter`. + * + * @returns A Projector that converts absolute paths to relative paths and includes the absolute path as `abspath`. + */ +export function relativePath(): Refiner { + return defineRefiner( + async function* (denops, { items }, { signal }) { + // Get the current working directory + const cwd = await fn.getcwd(denops); + signal?.throwIfAborted(); + + // Convert each item's path to a relative path + for await (const item of items) { + const relpath = relative(cwd, item.detail.path); + const value = item.value.replace(item.detail.path, relpath); + + // Yield item with updated relative path and original absolute path + yield { + ...item, + value, + detail: { + ...item.detail, + path: relpath, + abspath: item.detail.path, + }, + }; + } + }, + ); +} diff --git a/builtin/renderer/helptag.ts b/builtin/renderer/helptag.ts index 0eabdae..a40ebc2 100644 --- a/builtin/renderer/helptag.ts +++ b/builtin/renderer/helptag.ts @@ -1,5 +1,10 @@ import { defineRenderer, type Renderer } from "../../renderer.ts"; +type Detail = { + helptag: string; + lang?: string; +}; + /** * Creates a Renderer for helptags, adding language suffixes as labels and decorations. * @@ -8,10 +13,8 @@ import { defineRenderer, type Renderer } from "../../renderer.ts"; * * @returns A Renderer that formats helptags with optional language suffixes. */ -export function helptag< - T extends { helptag: string; lang?: string }, ->(): Renderer { - return defineRenderer(async (_denops, { items }, { signal }) => { +export function helptag(): Renderer { + return defineRenderer(async (_denops, { items }, { signal }) => { for await (const item of items) { signal?.throwIfAborted(); // If a language is specified, update the label and add decoration diff --git a/builtin/renderer/nerdfont.ts b/builtin/renderer/nerdfont.ts index d562092..6ae708f 100644 --- a/builtin/renderer/nerdfont.ts +++ b/builtin/renderer/nerdfont.ts @@ -8,9 +8,6 @@ import { extname } from "@std/path/extname"; import { defineRenderer, type Renderer } from "../../renderer.ts"; import { getByteLength } from "../../util/stringutil.ts"; -/** - * Represents details for items that include a file path. - */ type Detail = { path: string; }; @@ -25,8 +22,8 @@ type Detail = { * * @returns A Renderer that adds Nerd Font icons as labels for items based on file properties. */ -export function nerdfont(): Renderer { - return defineRenderer((_denops, { items }) => { +export function nerdfont(): Renderer { + return defineRenderer((_denops, { items }) => { items.forEach((item) => { const { path } = item.detail; diff --git a/builtin/renderer/noop.ts b/builtin/renderer/noop.ts index 1bc6f37..c279a69 100644 --- a/builtin/renderer/noop.ts +++ b/builtin/renderer/noop.ts @@ -8,6 +8,6 @@ import { defineRenderer, type Renderer } from "../../renderer.ts"; * * @returns A Renderer that does nothing. */ -export function noop(): Renderer { - return defineRenderer(() => {}); +export function noop(): Renderer { + return defineRenderer(() => {}); } diff --git a/builtin/renderer/smart_path.ts b/builtin/renderer/smart_path.ts index cae35fd..cb6a17f 100644 --- a/builtin/renderer/smart_path.ts +++ b/builtin/renderer/smart_path.ts @@ -12,8 +12,8 @@ import { getByteLength } from "../../util/stringutil.ts"; * * @returns A Renderer that reformats paths for better readability. */ -export function smartPath(): Renderer { - return defineRenderer((_denops, { items }, { signal }) => { +export function smartPath(): Renderer { + return defineRenderer((_denops, { items }, { signal }) => { for (const item of items) { signal?.throwIfAborted(); diff --git a/builtin/sorter/lexical.ts b/builtin/sorter/lexical.ts index 21fbc9a..0baa7e8 100644 --- a/builtin/sorter/lexical.ts +++ b/builtin/sorter/lexical.ts @@ -1,7 +1,7 @@ -import type { IdItem } from "../../item.ts"; +import type { Detail, IdItem } from "../../item.ts"; import { defineSorter, type Sorter } from "../../sorter.ts"; -type Options = { +export type LexicalOptions = { /** * Function to extract the string attribute used for sorting. * If not provided, the item's `value` will be used. @@ -24,7 +24,9 @@ type Options = { * @param options - Options for customizing the sort behavior. * @returns A Sorter that performs lexical ordering on items. */ -export function lexical(options: Readonly> = {}): Sorter { +export function lexical( + options: Readonly> = {}, +): Sorter { const attrGetter = options.attrGetter ?? ((item: IdItem) => item.value); const alpha = options.reverse ? -1 : 1; return defineSorter((_denops, { items }, _options) => { diff --git a/builtin/sorter/noop.ts b/builtin/sorter/noop.ts index 8f8cde2..ef92216 100644 --- a/builtin/sorter/noop.ts +++ b/builtin/sorter/noop.ts @@ -8,6 +8,6 @@ import { defineSorter, type Sorter } from "../../sorter.ts"; * * @returns A Sorter that does nothing. */ -export function noop(): Sorter { - return defineSorter(() => {}); +export function noop(): Sorter { + return defineSorter(() => {}); } diff --git a/builtin/sorter/numerical.ts b/builtin/sorter/numerical.ts index 1db80f3..5540bb9 100644 --- a/builtin/sorter/numerical.ts +++ b/builtin/sorter/numerical.ts @@ -1,7 +1,7 @@ -import type { IdItem } from "../../item.ts"; +import type { Detail, IdItem } from "../../item.ts"; import { defineSorter, type Sorter } from "../../sorter.ts"; -type Options = { +export type NumericalOptions = { /** * Function to extract the attribute used for sorting. * If not provided, the item's `value` will be used. @@ -24,7 +24,9 @@ type Options = { * @param options - Options for customizing the sort behavior. * @returns A Sorter that performs numerical ordering on items. */ -export function numerical(options: Readonly> = {}): Sorter { +export function numerical( + options: Readonly> = {}, +): Sorter { const attrGetter = options.attrGetter ?? ((item: IdItem) => item.value); const alpha = options.reverse ? -1 : 1; return defineSorter((_denops, { items }, _options) => { diff --git a/builtin/source/buffer.ts b/builtin/source/buffer.ts index e003184..f731f9c 100644 --- a/builtin/source/buffer.ts +++ b/builtin/source/buffer.ts @@ -3,18 +3,6 @@ import * as fn from "@denops/std/function"; import { defineSource, type Source } from "../../source.ts"; -type Filter = "buflisted" | "bufloaded" | "bufmodified"; - -type Options = { - /** - * The mode to filter the buffer. - * - `buflisted`: Only includes buffers listed in the buffer list. - * - `bufloaded`: Only includes loaded buffers. - * - `bufmodified`: Only includes buffers with unsaved changes. - */ - filter?: Filter; -}; - type Detail = { /** * Buffer number @@ -32,6 +20,18 @@ type Detail = { bufinfo: fn.BufInfo; }; +export type BufferOptions = { + /** + * The mode to filter the buffer. + * - `buflisted`: Only includes buffers listed in the buffer list. + * - `bufloaded`: Only includes loaded buffers. + * - `bufmodified`: Only includes buffers with unsaved changes. + */ + filter?: Filter; +}; + +type Filter = "buflisted" | "bufloaded" | "bufmodified"; + /** * Creates a Source that generates items from the current buffers based on filter criteria. * @@ -41,7 +41,7 @@ type Detail = { * @param options - Options to customize buffer filtering. * @returns A Source that generates items representing filtered buffers. */ -export function buffer(options: Readonly = {}): Source { +export function buffer(options: Readonly = {}): Source { const filter = options.filter; return defineSource(async function* (denops, _params, { signal }) { const bufinfo = await fn.getbufinfo(denops); diff --git a/builtin/source/file.ts b/builtin/source/file.ts index 755a067..ebd85da 100644 --- a/builtin/source/file.ts +++ b/builtin/source/file.ts @@ -4,28 +4,28 @@ import { join } from "@std/path/join"; import { defineSource, type Source } from "../../source.ts"; -type Options = { +type Detail = { /** - * Patterns to include files matching specific paths. + * Absolute path of the file. */ - includes?: RegExp[]; + path: string; /** - * Patterns to exclude files matching specific paths. + * File information including metadata like size, permissions, etc. */ - excludes?: RegExp[]; + stat: Deno.FileInfo; }; -type Detail = { +export type FileOptions = { /** - * Absolute path of the file. + * Patterns to include files matching specific paths. */ - path: string; + includes?: RegExp[]; /** - * File information including metadata like size, permissions, etc. + * Patterns to exclude files matching specific paths. */ - stat: Deno.FileInfo; + excludes?: RegExp[]; }; /** @@ -37,7 +37,7 @@ type Detail = { * @param options - Options to filter files based on patterns. * @returns A Source that generates items representing filtered files. */ -export function file(options: Readonly = {}): Source { +export function file(options: Readonly = {}): Source { const { includes, excludes } = options; return defineSource(async function* (denops, { args }, { signal }) { const path = await fn.expand(denops, args[0] ?? ".") as string; diff --git a/builtin/source/helptag.ts b/builtin/source/helptag.ts index 70c06c2..8a78ecd 100644 --- a/builtin/source/helptag.ts +++ b/builtin/source/helptag.ts @@ -4,7 +4,7 @@ import { join } from "@std/path/join"; import { defineSource, type Source } from "../../source.ts"; -type Helptag = { +type Detail = { /** * The helptag identifier. */ @@ -30,7 +30,7 @@ type Helptag = { * * @returns A Source that yields helptags with associated details. */ -export function helptag(): Source { +export function helptag(): Source { return defineSource(async function* (denops, _params, { signal }) { const runtimepaths = (await opt.runtimepath.get(denops)).split(","); signal?.throwIfAborted(); @@ -66,7 +66,7 @@ export function helptag(): Source { */ async function* discoverHelptags( runtimepath: string, -): AsyncGenerator { +): AsyncGenerator { const match = [/\/tags(?:-\w{2})?$/]; try { for await ( @@ -95,7 +95,7 @@ async function* discoverHelptags( * @param content - The raw content of the helptag file. * @returns A generator yielding helptag objects. */ -function* parseHelptags(content: string): Generator { +function* parseHelptags(content: string): Generator { const lines = content.split("\n"); for (const line of lines) { if (line.startsWith("!_TAG_") || line.trim() === "") { diff --git a/builtin/source/history.ts b/builtin/source/history.ts index 31e87bb..6f6e779 100644 --- a/builtin/source/history.ts +++ b/builtin/source/history.ts @@ -2,15 +2,17 @@ import * as fn from "@denops/std/function"; import { defineSource, type Source } from "../../source.ts"; +type Detail = { + history: History; +}; + /** - * Mode of the history to retrieve. - * - `cmd`: Command history - * - `search`: Search history - * - `expr`: Expression history - * - `input`: Input history - * - `debug`: Debug history + * Options for the history source. + * - `mode`: Specifies which history mode to retrieve. */ -type Mode = "cmd" | "search" | "expr" | "input" | "debug"; +export type HistoryOptions = { + mode?: Mode; +}; /** * Structure of a single history entry. @@ -38,19 +40,18 @@ type History = { }; /** - * Options for the history source. - * - `mode`: Specifies which history mode to retrieve. + * Mode of the history to retrieve. + * - `cmd`: Command history + * - `search`: Search history + * - `expr`: Expression history + * - `input`: Input history + * - `debug`: Debug history */ -type Options = { - mode?: Mode; -}; +type Mode = "cmd" | "search" | "expr" | "input" | "debug"; /** * Detail information attached to each history item. */ -type Detail = { - history: History; -}; /** * Source to retrieve history items from the specified mode. @@ -61,9 +62,9 @@ type Detail = { * @param options - The options to configure the history retrieval, with `mode` specifying the history type. * @returns A Source that yields history entries as items. */ -export function history(options: Options = {}): Source { +export function history(options: HistoryOptions = {}): Source { const { mode = "cmd" } = options; - return defineSource(async function* (denops, _params, { signal }) { + return defineSource(async function* (denops, _params, { signal }) { const histnr = await fn.histnr(denops, mode); signal?.throwIfAborted(); let id = 0; diff --git a/builtin/source/line.ts b/builtin/source/line.ts index 3411eca..08b849f 100644 --- a/builtin/source/line.ts +++ b/builtin/source/line.ts @@ -4,17 +4,6 @@ import { defineSource, type Source } from "../../source.ts"; const CHUNK_SIZE = 1000; -/** - * Options for the line source. - * - `chunkSize`: Specifies the number of lines to read at once from the buffer. - */ -type Options = { - chunkSize?: number; -}; - -/** - * Detail information attached to each line item in the buffer. - */ type Detail = { /** * Buffer number. @@ -37,6 +26,14 @@ type Detail = { context: string; }; +/** + * Options for the line source. + * - `chunkSize`: Specifies the number of lines to read at once from the buffer. + */ +export type LineOptions = { + chunkSize?: number; +}; + /** * Source to retrieve lines from the specified buffer. * @@ -46,7 +43,7 @@ type Detail = { * @param options - Configuration options, such as `chunkSize` to specify the batch size for reading lines. * @returns A Source that yields each line in the buffer as an item. */ -export function line(options: Options = {}): Source { +export function line(options: LineOptions = {}): Source { const { chunkSize = CHUNK_SIZE } = options; return defineSource(async function* (denops, { args }, { signal }) { diff --git a/builtin/source/list.ts b/builtin/source/list.ts index af997a0..c1f1a0b 100644 --- a/builtin/source/list.ts +++ b/builtin/source/list.ts @@ -1,4 +1,4 @@ -import type { IdItem } from "../../item.ts"; +import type { Detail, IdItem } from "../../item.ts"; import { defineSource, type Source } from "../../source.ts"; /** @@ -11,7 +11,7 @@ import { defineSource, type Source } from "../../source.ts"; * @param items - An iterable or async iterable of items to yield. * @returns A source that yields each item in the provided list. */ -export function list( +export function list( items: Iterable> | AsyncIterable>, ): Source { return defineSource(async function* (_denops, _params, _options) { diff --git a/builtin/source/noop.ts b/builtin/source/noop.ts index f3e2e27..4a66c0a 100644 --- a/builtin/source/noop.ts +++ b/builtin/source/noop.ts @@ -1,3 +1,4 @@ +import type { DetailUnit } from "../../item.ts"; import { defineSource, type Source } from "../../source.ts"; /** @@ -9,6 +10,6 @@ import { defineSource, type Source } from "../../source.ts"; * * @returns A source that yields no items. */ -export function noop(): Source { +export function noop(): Source { return defineSource(async function* () {}); } diff --git a/builtin/source/oldfiles.ts b/builtin/source/oldfiles.ts index 4442df1..5c5bdf1 100644 --- a/builtin/source/oldfiles.ts +++ b/builtin/source/oldfiles.ts @@ -16,7 +16,7 @@ type Detail = { * @returns A source that yields recently accessed files. */ export function oldfiles(): Source { - return defineSource(async function* (denops, _params, { signal }) { + return defineSource(async function* (denops, _params, { signal }) { const oldfiles = await vars.v.get(denops, "oldfiles") as string[]; signal?.throwIfAborted(); for (const [id, path] of enumerate(oldfiles)) { diff --git a/config.ts b/config.ts index 76203a3..24bb808 100644 --- a/config.ts +++ b/config.ts @@ -1,16 +1,15 @@ import type { Denops } from "@denops/std"; -import type { - Action, - Coordinator, - Curator, - Matcher, - Previewer, - Renderer, - Sorter, - Source, - Theme, -} from "@vim-fall/core"; +import type { Action } from "./action.ts"; +import type { Detail } from "./item.ts"; +import type { Coordinator } from "./coordinator.ts"; +import type { Curator } from "./curator.ts"; +import type { Matcher } from "./matcher.ts"; +import type { Previewer } from "./previewer.ts"; +import type { Renderer } from "./renderer.ts"; +import type { Sorter } from "./sorter.ts"; +import type { Source } from "./source.ts"; +import type { Theme } from "./theme.ts"; import type { Derivable, DerivableArray, @@ -23,7 +22,7 @@ import type { * @template T - The type of items the actions operate on. * @template A - The type representing the default action name. */ -export type Actions = +export type Actions = & Record> & { [key in A]: Action }; @@ -33,7 +32,7 @@ export type Actions = * @template T - The type of items in the picker. * @template A - The type representing the default action name. */ -export type ItemPickerParams = { +export type ItemPickerParams = { name: string; source: Source; actions: Actions>; @@ -50,10 +49,10 @@ export type ItemPickerParams = { * Parameters required to configure an action picker. */ export type ActionPickerParams = { - matchers: [Matcher>, ...Matcher>[]]; - sorters?: Sorter>[]; - renderers?: Renderer>[]; - previewers?: Previewer>[]; + matchers: [Matcher>, ...Matcher>[]]; + sorters?: Sorter>[]; + renderers?: Renderer>[]; + previewers?: Previewer>[]; coordinator?: Coordinator; theme?: Theme; }; @@ -72,7 +71,7 @@ export type GlobalConfig = { * @template T - The type of items handled by the picker. * @template A - The type representing the default action name. */ -export type DefineItemPickerFromSource = ( +export type DefineItemPickerFromSource = ( name: string, source: Derivable>, params: { @@ -93,7 +92,7 @@ export type DefineItemPickerFromSource = ( * @template T - The type of items handled by the picker. * @template A - The type representing the default action name. */ -export type DefineItemPickerFromCurator = ( +export type DefineItemPickerFromCurator = ( name: string, curator: Derivable>, params: { @@ -113,11 +112,11 @@ export type DefineItemPickerFromCurator = ( export type RefineActionPicker = ( params: { matchers: DerivableArray< - [Matcher>, ...Matcher>[]] + [Matcher>, ...Matcher>[]] >; - sorters?: DerivableArray>[]>; - renderers?: DerivableArray>[]>; - previewers?: DerivableArray>[]>; + sorters?: DerivableArray>[]>; + renderers?: DerivableArray>[]>; + previewers?: DerivableArray>[]>; coordinator?: Derivable; theme?: Derivable; }, diff --git a/curator.ts b/curator.ts index 4472ad8..0bc6e80 100644 --- a/curator.ts +++ b/curator.ts @@ -1,14 +1,18 @@ +export type * from "@vim-fall/core/curator"; + import type { Denops } from "@denops/std"; -import type { IdItem } from "@vim-fall/core/item"; import type { CurateParams, Curator } from "@vim-fall/core/curator"; +import type { Detail, IdItem } from "./item.ts"; +import { type DerivableArray, deriveArray } from "./util/derivable.ts"; + /** * Defines a curator responsible for collecting and filtering items. * * @param curate - A function to curate items based on the provided parameters. * @returns A curator object containing the `curate` function. */ -export function defineCurator( +export function defineCurator( curate: ( denops: Denops, params: CurateParams, @@ -18,4 +22,40 @@ export function defineCurator( return { curate }; } -export type * from "@vim-fall/core/curator"; +/** + * Composes multiple curators into a single curator. + * + * Each curator is collected sequentially in the order it is provided. The + * resulting items are combined into a single asynchronous iterable, with each + * item assigned a unique incremental ID. + * + * @param curators - The curators to compose. + * @returns A single composed curator that collects items from all given curators. + */ +export function composeCurators< + S extends DerivableArray<[Curator, ...Curator[]]>, + R extends UnionCurator, +>(...curators: S): Curator { + return { + curate: async function* (denops, params, options) { + let id = 0; + for (const curator of deriveArray(curators)) { + for await (const item of curator.curate(denops, params, options)) { + yield { ...item, id: id++ } as IdItem; + } + } + }, + }; +} + +/** + * Recursively constructs a union type from an array of curators. + * + * @template S - Array of curators to create a union type from. + */ +type UnionCurator< + S extends DerivableArray[]>, +> = S extends DerivableArray< + [Curator, ...infer R extends DerivableArray[]>] +> ? T | UnionCurator + : never; diff --git a/curator_test.ts b/curator_test.ts index 7c7e51b..828589c 100644 --- a/curator_test.ts +++ b/curator_test.ts @@ -1,9 +1,128 @@ import { assertEquals } from "@std/assert"; +import { DenopsStub } from "@denops/test/stub"; import { assertType, type IsExact } from "@std/testing/types"; -import { type Curator, defineCurator } from "./curator.ts"; +import type { Detail } from "./item.ts"; +import { composeCurators, type Curator, defineCurator } from "./curator.ts"; -Deno.test("defineCurator", () => { - const curator = defineCurator(async function* () {}); - assertEquals(typeof curator.curate, "function"); - assertType>>(true); +Deno.test("defineCurator", async (t) => { + await t.step("without type contraint", async () => { + const curator = defineCurator(async function* () { + yield { id: 1, value: "1", detail: { a: "" } }; + yield { id: 2, value: "2", detail: { a: "" } }; + yield { id: 3, value: "3", detail: { a: "" } }; + }); + assertType>>(true); + const denops = new DenopsStub(); + const params = { + args: [], + query: "", + }; + const items = await Array.fromAsync(curator.curate(denops, params, {})); + assertEquals(items, [ + { id: 1, value: "1", detail: { a: "" } }, + { id: 2, value: "2", detail: { a: "" } }, + { id: 3, value: "3", detail: { a: "" } }, + ]); + }); + + await t.step("with type contraint", async () => { + type C = { a: string }; + // @ts-expect-error: 'detail' does not follow the type constraint + defineCurator(async function* () { + yield { id: 1, value: "1", detail: "invalid detail" }; + }); + const curator = defineCurator(async function* () { + yield { id: 1, value: "1", detail: { a: "" } }; + yield { id: 2, value: "2", detail: { a: "" } }; + yield { id: 3, value: "3", detail: { a: "" } }; + }); + assertType>>(true); + const denops = new DenopsStub(); + const params = { + args: [], + query: "", + }; + const items = await Array.fromAsync(curator.curate(denops, params, {})); + assertEquals(items, [ + { id: 1, value: "1", detail: { a: "" } }, + { id: 2, value: "2", detail: { a: "" } }, + { id: 3, value: "3", detail: { a: "" } }, + ]); + }); +}); + +Deno.test("composeCurators", async (t) => { + await t.step("with bear curators", async (t) => { + await t.step("curators are applied in order", async () => { + const results: string[] = []; + const curator1 = defineCurator(async function* () { + results.push("curator1"); + yield* Array.from({ length: 3 }).map((_, id) => ({ + id, + value: `A-${id}`, + detail: { + a: id, + }, + })); + }); + const curator2 = defineCurator(async function* () { + results.push("curator2"); + yield* Array.from({ length: 3 }).map((_, id) => ({ + id, + value: `B-${id}`, + detail: { + b: id, + }, + })); + }); + const curator3 = defineCurator(async function* () { + results.push("curator3"); + yield* Array.from({ length: 3 }).map((_, id) => ({ + id, + value: `C-${id}`, + detail: { + c: id, + }, + })); + }); + const curator = composeCurators(curator2, curator1, curator3); + const denops = new DenopsStub(); + const params = { + args: [], + query: "", + }; + const items = await Array.fromAsync(curator.curate(denops, params, {})); + assertEquals(results, ["curator2", "curator1", "curator3"]); + assertEquals(items.map((v) => v.value), [ + "B-0", + "B-1", + "B-2", + "A-0", + "A-1", + "A-2", + "C-0", + "C-1", + "C-2", + ]); + }); + + await t.step("without type constraint", () => { + const curator1 = defineCurator(async function* () {}); + const curator2 = defineCurator(async function* () {}); + const curator3 = defineCurator(async function* () {}); + const curator = composeCurators(curator2, curator1, curator3); + assertType>>(true); + }); + + await t.step("with type constraint", () => { + type C1 = { a: string }; + type C2 = { b: string }; + type C3 = { c: string }; + const curator1 = defineCurator(async function* () {}); + const curator2 = defineCurator(async function* () {}); + const curator3 = defineCurator(async function* () {}); + const curator = composeCurators(curator2, curator1, curator3); + assertType>>(true); + }); + }); }); diff --git a/deno.jsonc b/deno.jsonc index b19be5c..1d9df0f 100644 --- a/deno.jsonc +++ b/deno.jsonc @@ -26,24 +26,22 @@ "./builtin/curator/grep": "./builtin/curator/grep.ts", "./builtin/curator/noop": "./builtin/curator/noop.ts", "./builtin/curator/rg": "./builtin/curator/rg.ts", - "./builtin/filter": "./builtin/filter/mod.ts", - "./builtin/filter/cwd": "./builtin/filter/cwd.ts", - "./builtin/filter/exists": "./builtin/filter/exists.ts", - "./builtin/filter/noop": "./builtin/filter/noop.ts", - "./builtin/filter/regexp": "./builtin/filter/regexp.ts", "./builtin/matcher": "./builtin/matcher/mod.ts", "./builtin/matcher/fzf": "./builtin/matcher/fzf.ts", "./builtin/matcher/noop": "./builtin/matcher/noop.ts", "./builtin/matcher/regexp": "./builtin/matcher/regexp.ts", "./builtin/matcher/substring": "./builtin/matcher/substring.ts", - "./builtin/modifier": "./builtin/modifier/mod.ts", - "./builtin/modifier/noop": "./builtin/modifier/noop.ts", - "./builtin/modifier/relative-path": "./builtin/modifier/relative_path.ts", "./builtin/previewer": "./builtin/previewer/mod.ts", "./builtin/previewer/buffer": "./builtin/previewer/buffer.ts", "./builtin/previewer/file": "./builtin/previewer/file.ts", "./builtin/previewer/helptag": "./builtin/previewer/helptag.ts", "./builtin/previewer/noop": "./builtin/previewer/noop.ts", + "./builtin/refiner": "./builtin/refiner/mod.ts", + "./builtin/refiner/cwd": "./builtin/refiner/cwd.ts", + "./builtin/refiner/exists": "./builtin/refiner/exists.ts", + "./builtin/refiner/noop": "./builtin/refiner/noop.ts", + "./builtin/refiner/regexp": "./builtin/refiner/regexp.ts", + "./builtin/refiner/relative-path": "./builtin/refiner/relative_path.ts", "./builtin/renderer": "./builtin/renderer/mod.ts", "./builtin/renderer/helptag": "./builtin/renderer/helptag.ts", "./builtin/renderer/nerdfont": "./builtin/renderer/nerdfont.ts", @@ -73,7 +71,7 @@ "./item": "./item.ts", "./matcher": "./matcher.ts", "./previewer": "./previewer.ts", - "./projector": "./projector.ts", + "./refiner": "./refiner.ts", "./renderer": "./renderer.ts", "./sorter": "./sorter.ts", "./source": "./source.ts", @@ -123,9 +121,7 @@ "@std/path": "jsr:@std/path@^1.0.8", "@std/streams": "jsr:@std/streams@^1.0.8", "@std/testing": "jsr:@std/testing@^1.0.4", - "@vim-fall/core": "jsr:@vim-fall/core@^0.1.1", - "fzf": "npm:fzf@^0.5.2", - "jsr:@vim-fall/std@^0.1.0": "./mod.ts", - "jsr:@vim-fall/std@^0.1.0/builtin": "./builtin/mod.ts" + "@vim-fall/core": "jsr:@vim-fall/core@^0.1.6", + "fzf": "npm:fzf@^0.5.2" } } diff --git a/matcher.ts b/matcher.ts index 556ad59..4e65010 100644 --- a/matcher.ts +++ b/matcher.ts @@ -1,5 +1,7 @@ +export type * from "@vim-fall/core/matcher"; + import type { Denops } from "@denops/std"; -import type { IdItem } from "@vim-fall/core/item"; +import type { Detail, DetailUnit, IdItem } from "@vim-fall/core/item"; import type { Matcher, MatchParams } from "@vim-fall/core/matcher"; import { type DerivableArray, deriveArray } from "./util/derivable.ts"; @@ -10,12 +12,12 @@ import { type DerivableArray, deriveArray } from "./util/derivable.ts"; * @param match - A function that matches items based on given parameters. * @returns A matcher object containing the `match` function. */ -export function defineMatcher( - match: ( +export function defineMatcher( + match: ( denops: Denops, - params: MatchParams, + params: MatchParams, options: { signal?: AbortSignal }, - ) => AsyncIterableIterator>, + ) => AsyncIterableIterator>, ): Matcher { return { match }; } @@ -29,10 +31,9 @@ export function defineMatcher( * @param matchers - The matchers to compose. * @returns A matcher that applies all composed matchers in sequence. */ -export function composeMatchers< - T, - M extends DerivableArray<[Matcher, ...Matcher[]]>, ->(...matchers: M): Matcher { +export function composeMatchers( + ...matchers: DerivableArray<[Matcher, ...Matcher>[]]> +): Matcher { return { match: async function* (denops, { items, query }, options) { for (const matcher of deriveArray(matchers)) { @@ -44,5 +45,3 @@ export function composeMatchers< }, }; } - -export type * from "@vim-fall/core/matcher"; diff --git a/matcher_test.ts b/matcher_test.ts index bba0076..c83bef1 100644 --- a/matcher_test.ts +++ b/matcher_test.ts @@ -1,46 +1,184 @@ import { assertEquals } from "@std/assert"; import { assertType, type IsExact } from "@std/testing/types"; import { DenopsStub } from "@denops/test/stub"; -import { composeMatchers, defineMatcher, type Matcher } from "./matcher.ts"; +import type { DetailUnit } from "./item.ts"; +import { + composeMatchers, + defineMatcher, + type Matcher, + type MatchParams, +} from "./matcher.ts"; -Deno.test("defineMatcher", () => { - const matcher = defineMatcher(async function* () {}); - assertEquals(typeof matcher.match, "function"); - assertType>>(true); -}); +Deno.test("defineMatcher", async (t) => { + await t.step("without type constraint", () => { + type C = { a: string }; + const matcher = defineMatcher(async function* (_denops, params) { + // NOTE: + // `match` method itself has `V` thus we cannot use `AssertTrue` here. + // @ts-expect-error: `params` does not establish the type constraint + const _: MatchParams = params; + yield* []; + }); + assertType>>(true); + }); -Deno.test("composeMatchers", async () => { - const results: string[] = []; - const matcher1 = defineMatcher(async function* (_denops, { items }) { - results.push("matcher1"); - yield* items.filter((item) => item.value.includes("1")); + await t.step("with type constraint", () => { + type C = { a: string }; + const matcher = defineMatcher( + async function* (_denops, params) { + // NOTE: + // `match` method itself has `V` thus we cannot use `AssertTrue` here. + // `params` should establish the type constraint + const _: MatchParams = params; + yield* []; + }, + ); + assertType>>(true); }); - const matcher2 = defineMatcher(async function* (_denops, { items }) { - results.push("matcher2"); - yield* items.filter((item) => item.value.includes("2")); +}); + +Deno.test("composeMatchers", async (t) => { + await t.step("with bear matchers", async (t) => { + await t.step("matchers are applied in order", async () => { + const results: string[] = []; + const matcher1 = defineMatcher( + async function* (_denops, { items }) { + results.push("matcher1"); + yield* items.filter((item) => item.value.includes("1")); + }, + ); + const matcher2 = defineMatcher( + async function* (_denops, { items }) { + results.push("matcher2"); + yield* items.filter((item) => item.value.includes("2")); + }, + ); + const matcher3 = defineMatcher( + async function* (_denops, { items }) { + results.push("matcher3"); + yield* items.filter((item) => item.value.includes("3")); + }, + ); + const matcher = composeMatchers(matcher2, matcher1, matcher3); + const denops = new DenopsStub(); + const params = { + query: "", + items: Array.from({ length: 1000 }).map((_, id) => ({ + id, + value: id.toString(), + detail: {}, + })), + }; + const items = await Array.fromAsync(matcher.match(denops, params, {})); + assertEquals(results, ["matcher2", "matcher1", "matcher3"]); + assertEquals(items.map((item) => item.value), [ + "123", + "132", + "213", + "231", + "312", + "321", + ]); + }); + + await t.step("without type constraint", () => { + const matcher1 = defineMatcher(async function* () {}); + const matcher2 = defineMatcher(async function* () {}); + const matcher3 = defineMatcher(async function* () {}); + const matcher = composeMatchers(matcher1, matcher2, matcher3); + assertType>>(true); + }); + + await t.step("with type constraint", () => { + type C = { a: string }; + const matcher1 = defineMatcher(async function* () {}); + const matcher2 = defineMatcher(async function* () {}); + const matcher3 = defineMatcher(async function* () {}); + const matcher = composeMatchers(matcher1, matcher2, matcher3); + assertType>>(true); + }); + + await t.step("with type constraint (fail)", () => { + type C = { a: string }; + const matcher1 = defineMatcher(async function* () {}); + const matcher2 = defineMatcher(async function* () {}); + const matcher3 = defineMatcher<{ b: string }>(async function* () {}); + // @ts-expect-error: `matcher3` requires `{ b: string }` + composeMatchers(matcher1, matcher2, matcher3); + }); }); - const matcher3 = defineMatcher(async function* (_denops, { items }) { - results.push("matcher3"); - yield* items.filter((item) => item.value.includes("3")); + + await t.step("with derivable matchers", async (t) => { + await t.step("matchers are applied in order", async () => { + const results: string[] = []; + const matcher1 = () => + defineMatcher( + async function* (_denops, { items }) { + results.push("matcher1"); + yield* items.filter((item) => item.value.includes("1")); + }, + ); + const matcher2 = () => + defineMatcher( + async function* (_denops, { items }) { + results.push("matcher2"); + yield* items.filter((item) => item.value.includes("2")); + }, + ); + const matcher3 = () => + defineMatcher( + async function* (_denops, { items }) { + results.push("matcher3"); + yield* items.filter((item) => item.value.includes("3")); + }, + ); + const matcher = composeMatchers(matcher2, matcher1, matcher3); + const denops = new DenopsStub(); + const params = { + query: "", + items: Array.from({ length: 1000 }).map((_, id) => ({ + id, + value: id.toString(), + detail: {}, + })), + }; + const items = await Array.fromAsync(matcher.match(denops, params, {})); + assertEquals(results, ["matcher2", "matcher1", "matcher3"]); + assertEquals(items.map((item) => item.value), [ + "123", + "132", + "213", + "231", + "312", + "321", + ]); + }); + + await t.step("without type constraint", () => { + const matcher1 = () => defineMatcher(async function* () {}); + const matcher2 = () => defineMatcher(async function* () {}); + const matcher3 = () => defineMatcher(async function* () {}); + const matcher = composeMatchers(matcher1, matcher2, matcher3); + assertType>>(true); + }); + + await t.step("with type constraint", () => { + type C = { a: string }; + const matcher1 = () => defineMatcher(async function* () {}); + const matcher2 = () => defineMatcher(async function* () {}); + const matcher3 = () => defineMatcher(async function* () {}); + const matcher = composeMatchers(matcher1, matcher2, matcher3); + assertType>>(true); + }); + + await t.step("with type constraint (fail)", () => { + type C = { a: string }; + const matcher1 = () => defineMatcher(async function* () {}); + const matcher2 = () => defineMatcher(async function* () {}); + const matcher3 = () => + defineMatcher<{ b: string }>(async function* () {}); + // @ts-expect-error: `matcher3` requires `{ b: string }` + composeMatchers(matcher1, matcher2, matcher3); + }); }); - const matcher = composeMatchers(matcher2, matcher1, matcher3); - const denops = new DenopsStub(); - const params = { - query: "", - items: Array.from({ length: 1000 }).map((_, id) => ({ - id, - value: id.toString(), - detail: undefined, - })), - }; - const items = await Array.fromAsync(matcher.match(denops, params, {})); - assertEquals(results, ["matcher2", "matcher1", "matcher3"]); - assertEquals(items.map((item) => item.value), [ - "123", - "132", - "213", - "231", - "312", - "321", - ]); }); diff --git a/mod.ts b/mod.ts index ed33315..4c117bd 100644 --- a/mod.ts +++ b/mod.ts @@ -5,7 +5,7 @@ export * from "./curator.ts"; export * from "./item.ts"; export * from "./matcher.ts"; export * from "./previewer.ts"; -export * from "./projector.ts"; +export * from "./refiner.ts"; export * from "./renderer.ts"; export * from "./sorter.ts"; export * from "./source.ts"; diff --git a/previewer.ts b/previewer.ts index e25c94f..aaa2754 100644 --- a/previewer.ts +++ b/previewer.ts @@ -1,7 +1,9 @@ +export type * from "@vim-fall/core/previewer"; + import type { Denops } from "@denops/std"; -import type { PreviewItem } from "@vim-fall/core/item"; import type { Previewer, PreviewParams } from "@vim-fall/core/previewer"; +import type { Detail, DetailUnit, PreviewItem } from "./item.ts"; import type { Promish } from "./util/_typeutil.ts"; import { type DerivableArray, deriveArray } from "./util/derivable.ts"; @@ -11,7 +13,7 @@ import { type DerivableArray, deriveArray } from "./util/derivable.ts"; * @param preview - A function that generates a preview for an item. * @returns A previewer object containing the `preview` function. */ -export function definePreviewer( +export function definePreviewer( preview: ( denops: Denops, params: PreviewParams, @@ -31,10 +33,9 @@ export function definePreviewer( * @param previewers - The previewers to compose. * @returns A single previewer that applies each previewer in sequence until a preview is generated. */ -export function composePreviewers< - T, - P extends DerivableArray<[Previewer, ...Previewer[]]>, ->(...previewers: P): Previewer { +export function composePreviewers( + ...previewers: DerivableArray<[Previewer, ...Previewer[]]> +): Previewer { return { preview: async (denops, params, options) => { for (const previewer of deriveArray(previewers)) { @@ -46,5 +47,3 @@ export function composePreviewers< }, }; } - -export type * from "@vim-fall/core/previewer"; diff --git a/previewer_test.ts b/previewer_test.ts index a369ab7..6d60750 100644 --- a/previewer_test.ts +++ b/previewer_test.ts @@ -1,43 +1,134 @@ import { assertEquals } from "@std/assert"; -import { assertType, type IsExact } from "@std/testing/types"; +import { type AssertTrue, assertType, type IsExact } from "@std/testing/types"; import { DenopsStub } from "@denops/test/stub"; +import type { DetailUnit } from "./item.ts"; import { composePreviewers, definePreviewer, type Previewer, + type PreviewParams, } from "./previewer.ts"; -Deno.test("definePreviewer", () => { - const previewer = definePreviewer(async () => {}); - assertEquals(typeof previewer.preview, "function"); - assertType>>(true); -}); - -Deno.test("composePreviewers", async () => { - const results: string[] = []; - const previewer1 = definePreviewer(() => { - results.push("previewer1"); - return { content: ["Hello world"] }; +Deno.test("definePreviewer", async (t) => { + await t.step("without type contraint", () => { + const previewer = definePreviewer((_denops, params) => { + type _ = AssertTrue>>; + }); + assertType>>(true); }); - const previewer2 = definePreviewer(() => { - results.push("previewer2"); + + await t.step("with type contraint", () => { + type C = { a: string }; + const previewer = definePreviewer((_denops, params) => { + type _ = AssertTrue>>; + }); + assertType>>(true); }); - const previewer3 = definePreviewer(() => { - results.push("previewer3"); - return { content: ["Goodbye world"] }; +}); + +Deno.test("composePreviewers", async (t) => { + await t.step("with bear previewers", async (t) => { + await t.step( + "previewers are applied in order and terminate on success", + async () => { + const results: string[] = []; + const previewer1 = definePreviewer(() => { + results.push("previewer1"); + return { content: ["Hello world"] }; + }); + const previewer2 = definePreviewer(() => { + results.push("previewer2"); + }); + const previewer3 = definePreviewer(() => { + results.push("previewer3"); + return { content: ["Goodbye world"] }; + }); + const previewer = composePreviewers(previewer2, previewer1, previewer3); + const denops = new DenopsStub(); + const params = { + item: { + id: 0, + value: "123", + detail: {}, + }, + }; + const item = await previewer.preview(denops, params, {}); + assertEquals(results, ["previewer2", "previewer1"]); + assertEquals(item, { + content: ["Hello world"], + }); + }, + ); + + await t.step("without type constraint", () => { + const previewer1 = definePreviewer(() => {}); + const previewer2 = definePreviewer(() => {}); + const previewer3 = definePreviewer(() => {}); + const previewer = composePreviewers(previewer2, previewer1, previewer3); + assertType>>(true); + }); + + await t.step("with type constraint", () => { + type C = { a: string }; + const previewer1 = definePreviewer(() => {}); + const previewer2 = definePreviewer(() => {}); + const previewer3 = definePreviewer(() => {}); + const previewer = composePreviewers(previewer2, previewer1, previewer3); + assertType>>(true); + }); }); - const previewer = composePreviewers(previewer2, previewer1, previewer3); - const denops = new DenopsStub(); - const params = { - item: { - id: 0, - value: "123", - detail: undefined, - }, - }; - const item = await previewer.preview(denops, params, {}); - assertEquals(results, ["previewer2", "previewer1"]); - assertEquals(item, { - content: ["Hello world"], + + await t.step("with derivable previewers", async (t) => { + await t.step( + "previewers are applied in order and terminate on success", + async () => { + const results: string[] = []; + const previewer1 = () => + definePreviewer(() => { + results.push("previewer1"); + return { content: ["Hello world"] }; + }); + const previewer2 = () => + definePreviewer(() => { + results.push("previewer2"); + }); + const previewer3 = () => + definePreviewer(() => { + results.push("previewer3"); + return { content: ["Goodbye world"] }; + }); + const previewer = composePreviewers(previewer2, previewer1, previewer3); + const denops = new DenopsStub(); + const params = { + item: { + id: 0, + value: "123", + detail: {}, + }, + }; + const item = await previewer.preview(denops, params, {}); + assertEquals(results, ["previewer2", "previewer1"]); + assertEquals(item, { + content: ["Hello world"], + }); + }, + ); + + await t.step("without type constraint", () => { + const previewer1 = () => definePreviewer(() => {}); + const previewer2 = () => definePreviewer(() => {}); + const previewer3 = () => definePreviewer(() => {}); + const previewer = composePreviewers(previewer2, previewer1, previewer3); + assertType>>(true); + }); + + await t.step("with type constraint", () => { + type C = { a: string }; + const previewer1 = () => definePreviewer(() => {}); + const previewer2 = () => definePreviewer(() => {}); + const previewer3 = () => definePreviewer(() => {}); + const previewer = composePreviewers(previewer2, previewer1, previewer3); + assertType>>(true); + }); }); }); diff --git a/projector.ts b/projector.ts deleted file mode 100644 index 9ac90de..0000000 --- a/projector.ts +++ /dev/null @@ -1,107 +0,0 @@ -import type { Denops } from "@denops/std"; -import type { IdItem } from "@vim-fall/core/item"; -import type { Projector, ProjectParams } from "@vim-fall/core/projector"; - -import type { FirstType, LastType } from "./util/_typeutil.ts"; -import { defineSource, type Source } from "./source.ts"; -import { type Curator, defineCurator } from "./curator.ts"; -import { - type Derivable, - type DerivableArray, - derive, - deriveArray, -} from "./util/derivable.ts"; - -/** - * Defines a projector responsible for transforming or filtering items. - * - * @param project - A function that processes items based on given parameters. - * @returns A projector object containing the `project` function. - */ -export function defineProjector( - project: ( - denops: Denops, - params: ProjectParams, - options: { signal?: AbortSignal }, - ) => AsyncIterableIterator>, -): Projector { - return { project }; -} - -/** - * Composes multiple projectors into a single projector. - * - * The projectors are applied sequentially in the order they are passed. - * Each projector processes the items from the previous one, allowing for - * a series of transformations or filters. - * - * @param projectors - The projectors to compose. - * @returns A composed projector that applies all given projectors in sequence. - */ -export function composeProjectors< - T extends FirstType

extends Derivable> ? T - : never, - U extends LastType

extends Derivable> ? U - : never, - P extends DerivableArray<[ - Projector, - ...Projector[], - ]>, ->(...projectors: P): Projector { - return { - project: async function* ( - denops: Denops, - params: ProjectParams, - options: { signal?: AbortSignal }, - ) { - let it: AsyncIterable> = params.items; - for (const projector of deriveArray(projectors)) { - it = projector.project(denops, { items: it }, options); - } - yield* it as AsyncIterable>; - }, - }; -} - -/** - * Pipes projectors to a source or curator, applying them sequentially. - * - * Each projector is applied in the order specified, transforming or filtering - * items from the source or curator. - * - * @param source - The source or curator to which projectors are applied. - * @param projectors - The projectors to apply. - * @returns A new source or curator with the projectors applied in sequence. - */ -export function pipeProjectors< - T, - U extends LastType

extends Derivable> ? U - : never, - S extends Derivable | Curator>, - P extends DerivableArray<[ - Projector, - ...Projector[], - ]>, - R extends S extends Derivable> ? Source : Curator, ->( - source: S, - ...projectors: P -): R { - const src = derive(source); - const projector = composeProjectors(...projectors) as Projector; - if ("collect" in src) { - // Define a new source with the composed projectors applied. - return defineSource((denops, params, options) => { - const items = src.collect(denops, params, options); - return projector.project(denops, { items }, options); - }) as R; - } else { - // Define a new curator with the composed projectors applied. - return defineCurator((denops, params, options) => { - const items = src.curate(denops, params, options); - return projector.project(denops, { items }, options); - }) as R; - } -} - -export type * from "@vim-fall/core/projector"; diff --git a/projector_test.ts b/projector_test.ts deleted file mode 100644 index 91fb39a..0000000 --- a/projector_test.ts +++ /dev/null @@ -1,260 +0,0 @@ -import { assertEquals } from "@std/assert"; -import { assertType, type IsExact } from "@std/testing/types"; -import { DenopsStub } from "@denops/test/stub"; -import { range } from "@core/iterutil"; -import { map } from "@core/iterutil/async"; -import { - composeProjectors, - defineProjector, - pipeProjectors, - type Projector, -} from "./projector.ts"; -import { defineSource, type Source } from "./source.ts"; -import { type Curator, defineCurator } from "./curator.ts"; - -Deno.test("defineProjector", () => { - const projector = defineProjector(async function* () {}); - assertEquals(typeof projector.project, "function"); - assertType>>(true); -}); - -Deno.test("composeProjectors", async () => { - const results: string[] = []; - const projector1 = defineProjector( - async function* (_denops, { items }) { - results.push("projector1"); - yield* map(items, (item) => ({ - ...item, - detail: { - a: item.detail, - }, - })); - }, - ); - const projector2 = defineProjector( - async function* (_denops, { items }) { - results.push("projector2"); - yield* map(items, (item) => ({ - ...item, - detail: { - b: item.detail, - }, - })); - }, - ); - const projector3 = defineProjector( - async function* (_denops, { items }) { - results.push("projector3"); - yield* map(items, (item) => ({ - ...item, - detail: { - c: item.detail, - }, - })); - }, - ); - const projector = composeProjectors(projector2, projector1, projector3); - assertType< - IsExact< - typeof projector, - Projector - > - >(true); - const denops = new DenopsStub(); - const params = { - items: map(range(1, 3), (id) => ({ - id, - value: `item-${id}`, - detail: undefined, - })), - }; - const items = await Array.fromAsync(projector.project(denops, params, {})); - assertEquals(results, ["projector3", "projector1", "projector2"]); - assertEquals(items, [ - { - id: 1, - value: "item-1", - detail: { - c: { - a: { - b: undefined, - }, - }, - }, - }, - { - id: 2, - value: "item-2", - detail: { - c: { - a: { - b: undefined, - }, - }, - }, - }, - { - id: 3, - value: "item-3", - detail: { - c: { - a: { - b: undefined, - }, - }, - }, - }, - ]); -}); - -Deno.test("pipeProjectors", async (t) => { - const projector1 = defineProjector( - async function* (_denops, { items }) { - yield* map(items, (item) => ({ - ...item, - detail: { - a: item.detail, - }, - })); - }, - ); - const projector2 = defineProjector( - async function* (_denops, { items }) { - yield* map(items, (item) => ({ - ...item, - detail: { - b: item.detail, - }, - })); - }, - ); - const projector3 = defineProjector( - async function* (_denops, { items }) { - yield* map(items, (item) => ({ - ...item, - detail: { - c: item.detail, - }, - })); - }, - ); - - await t.step("Source", async () => { - const source = defineSource(async function* () { - yield* map(range(1, 3), (id) => ({ - id, - value: `item-${id}`, - detail: undefined, - })); - }); - const pipedSource = pipeProjectors( - source, - projector2, - projector1, - projector3, - ); - assertType>>(true); - const denops = new DenopsStub(); - const params = { - args: [], - }; - const items = await Array.fromAsync( - pipedSource.collect(denops, params, {}), - ); - assertEquals(items, [ - { - id: 1, - value: "item-1", - detail: { - c: { - a: { - b: undefined, - }, - }, - }, - }, - { - id: 2, - value: "item-2", - detail: { - c: { - a: { - b: undefined, - }, - }, - }, - }, - { - id: 3, - value: "item-3", - detail: { - c: { - a: { - b: undefined, - }, - }, - }, - }, - ]); - }); - - await t.step("Curator", async () => { - const curator = defineCurator(async function* () { - yield* map(range(1, 3), (id) => ({ - id, - value: `item-${id}`, - detail: undefined, - })); - }); - const pipedCurator = pipeProjectors( - curator, - projector2, - projector1, - projector3, - ); - assertType>>(true); - const denops = new DenopsStub(); - const params = { - args: [], - query: "", - }; - const items = await Array.fromAsync( - pipedCurator.curate(denops, params, {}), - ); - assertEquals(items, [ - { - id: 1, - value: "item-1", - detail: { - c: { - a: { - b: undefined, - }, - }, - }, - }, - { - id: 2, - value: "item-2", - detail: { - c: { - a: { - b: undefined, - }, - }, - }, - }, - { - id: 3, - value: "item-3", - detail: { - c: { - a: { - b: undefined, - }, - }, - }, - }, - ]); - }); -}); diff --git a/refiner.ts b/refiner.ts new file mode 100644 index 0000000..fe6f063 --- /dev/null +++ b/refiner.ts @@ -0,0 +1,119 @@ +import type { Denops } from "@denops/std"; +import type { IdItem } from "@vim-fall/core/item"; + +import type { FlatType } from "./util/_typeutil.ts"; +import type { Detail, DetailUnit } from "./item.ts"; +import { defineSource, type Source } from "./source.ts"; +import { type Curator, defineCurator } from "./curator.ts"; +import { type Derivable, derive, deriveArray } from "./util/derivable.ts"; + +type Refine = ( + denops: Denops, + params: RefineParams, + options: { signal?: AbortSignal }, +) => AsyncIterableIterator>; + +export type RefineParams = { + readonly items: AsyncIterable>; +}; + +export type Refiner< + T extends Detail = DetailUnit, + U extends Detail = DetailUnit, +> = { + __phantom?: (_: T) => void; + refine: ( + denops: Denops, + params: RefineParams, + options: { signal?: AbortSignal }, + ) => AsyncIterableIterator>; +}; + +export function defineRefiner< + T extends Detail = DetailUnit, + U extends Detail = DetailUnit, +>( + refine: Refine, +): Refiner { + return { refine } as Refiner; +} + +export function refineSource< + Input extends Detail, + // deno-lint-ignore no-explicit-any + Refiners extends Derivable>[], +>( + source: Derivable>, + ...refiners: PipeRefiners +): Source>> { + // deno-lint-ignore no-explicit-any + const refiner = composeRefiners(...refiners as any); + source = derive(source); + return defineSource((denops, params, options) => { + const items = source.collect(denops, params, options); + return refiner.refine(denops, { items }, options); + }); +} + +export function refineCurator< + Input extends Detail, + // deno-lint-ignore no-explicit-any + Refiners extends Derivable>[], +>( + curator: Derivable>, + ...refiners: PipeRefiners +): Curator>> { + // deno-lint-ignore no-explicit-any + const refiner = composeRefiners(...refiners as any); + curator = derive(curator); + return defineCurator((denops, params, options) => { + const items = curator.curate(denops, params, options); + return refiner.refine(denops, { items }, options); + }); +} + +export function composeRefiners< + Input extends Detail, + // deno-lint-ignore no-explicit-any + Refiners extends Derivable>[], +>( + ...refiners: PipeRefiners +): Refiner>> { + return { + refine: async function* ( + denops: Denops, + params: RefineParams, + options: { signal?: AbortSignal }, + ) { + let items: AsyncIterable> = params.items; + for (const refiner of deriveArray(refiners)) { + items = (refiner.refine as Refine)( + denops, + { items }, + options, + ); + } + yield* items; + }, + } as Refiner>>; +} + +type RefinerB = T extends Derivable> ? B : never; + +type PipeRefiners< + Refiners extends Derivable>[], + Input extends Detail, +> = Refiners extends // deno-lint-ignore no-explicit-any +[infer Head, ...infer Tail extends Derivable>[]] ? [ + Derivable>>, + ...PipeRefiners>, + ] + : Refiners; + +type PipeRefinersOutput< + Refiners extends Derivable>[], +> = Refiners extends [infer Head] ? RefinerB + : Refiners extends // deno-lint-ignore no-explicit-any + [infer Head, ...infer Tail extends Derivable>[]] + ? RefinerB & PipeRefinersOutput + : never; diff --git a/refiner_test.ts b/refiner_test.ts new file mode 100644 index 0000000..a585511 --- /dev/null +++ b/refiner_test.ts @@ -0,0 +1,859 @@ +import { assertEquals } from "@std/assert"; +import { type AssertTrue, assertType, type IsExact } from "@std/testing/types"; +import { toAsyncIterable } from "@core/iterutil/async/to-async-iterable"; +import { DenopsStub } from "@denops/test/stub"; + +import type { DetailUnit } from "./item.ts"; +import { defineSource, type Source } from "./source.ts"; +import { type Curator, defineCurator } from "./curator.ts"; +import { + composeRefiners, + defineRefiner, + refineCurator, + type RefineParams, + type Refiner, + refineSource, +} from "./refiner.ts"; + +type RefinerA = T extends Refiner ? A : never; +type RefinerB = T extends Refiner ? B : never; + +Deno.test("defineRefiner", async (t) => { + await t.step("without type contraint", () => { + const refiner = defineRefiner(async function* (_denops, params) { + type _ = AssertTrue>>; + yield* []; + }); + assertType>>(true); + }); + + await t.step("with type contraint", () => { + type C = { a: string }; + const refiner = defineRefiner(async function* (_denops, params) { + type _ = AssertTrue>>; + yield* []; + }); + assertType>>(true); + }); +}); + +Deno.test("composeRefiners", async (t) => { + await t.step("with bear refiners", async (t) => { + await t.step("refiners are applied in order", async () => { + const results: string[] = []; + const refiner1 = defineRefiner< + { a: string }, + { b: string; B: string } + >( + async function* (_denops, { items }) { + results.push("refiner1"); + for await (const item of items) { + yield { + ...item, + detail: { + ...item.detail, + b: "Hello", + B: "World", + }, + }; + } + }, + ); + const refiner2 = defineRefiner< + { b: string }, + { c: string; C: string } + >( + async function* (_denops, { items }) { + results.push("refiner2"); + for await (const item of items) { + yield { + ...item, + detail: { + ...item.detail, + c: "Hello", + C: "World", + }, + }; + } + }, + ); + const refiner3 = defineRefiner< + { c: string }, + { d: string; D: string } + >( + async function* (_denops, { items }) { + results.push("refiner3"); + for await (const item of items) { + yield { + ...item, + detail: { + ...item.detail, + d: "Hello", + D: "World", + }, + }; + } + }, + ); + const refiner = composeRefiners(refiner1, refiner2, refiner3); + const denops = new DenopsStub(); + const params = { + items: toAsyncIterable([{ + id: 0, + value: "123", + detail: { + a: "Hello", + A: "World", + }, + }]), + }; + const items = await Array.fromAsync(refiner.refine(denops, params, {})); + assertEquals(results, ["refiner3", "refiner2", "refiner1"]); + assertEquals(items, [{ + id: 0, + value: "123", + detail: { + a: "Hello", + A: "World", + b: "Hello", + B: "World", + c: "Hello", + C: "World", + d: "Hello", + D: "World", + }, + }]); + }); + + await t.step("without type constraint", () => { + const refiner1 = defineRefiner(async function* () {}); + const refiner2 = defineRefiner(async function* () {}); + const refiner3 = defineRefiner(async function* () {}); + const refiner = composeRefiners(refiner1, refiner2, refiner3); + assertType>>(true); + }); + + await t.step("with type constraint", () => { + type C1 = { a: string }; + type C2 = { b: string }; + type C3 = { c: string }; + type C4 = { d: string }; + const refiner1 = defineRefiner(async function* () {}); + const refiner2 = defineRefiner(async function* () {}); + const refiner3 = defineRefiner(async function* () {}); + composeRefiners( + refiner2, + // @ts-expect-error: refiner1 requires C1 but C3 is provided + refiner1, + refiner3, + ); + const refiner = composeRefiners(refiner1, refiner2, refiner3); + // NOTE: + // It seems `IsExact` could not properly compare the `Refiner` type + // so compare extracted types instead. + assertType, C1>>(true); + assertType< + IsExact, { + b: string; + c: string; + d: string; + }> + >(true); + }); + + await t.step("with type constraint (extra attributes)", () => { + type C1 = { a: string }; + type C2 = { b: string }; + type C3 = { c: string }; + type C4 = { d: string }; + const refiner1 = defineRefiner( + async function* () {}, + ); + const refiner2 = defineRefiner( + async function* () {}, + ); + const refiner3 = defineRefiner( + async function* () {}, + ); + composeRefiners( + refiner2, + // @ts-expect-error: refiner1 requires C1 but C3 is provided + refiner1, + refiner3, + ); + const refiner = composeRefiners(refiner1, refiner2, refiner3); + // NOTE: + // It seems `IsExact` could not properly compare the `Refiner` type + // so compare extracted types instead. + assertType, C1>>(true); + assertType< + IsExact, { + b: string; + c: string; + d: string; + B: string; + C: string; + D: string; + }> + >(true); + }); + }); + + await t.step("with derivable refiners", async (t) => { + await t.step("refiners are applied in order", async () => { + const results: string[] = []; + const refiner1 = () => + defineRefiner< + { a: string }, + { b: string; B: string } + >( + async function* (_denops, { items }) { + results.push("refiner1"); + for await (const item of items) { + yield { + ...item, + detail: { + ...item.detail, + b: "Hello", + B: "World", + }, + }; + } + }, + ); + const refiner2 = () => + defineRefiner< + { b: string }, + { c: string; C: string } + >( + async function* (_denops, { items }) { + results.push("refiner2"); + for await (const item of items) { + yield { + ...item, + detail: { + ...item.detail, + c: "Hello", + C: "World", + }, + }; + } + }, + ); + const refiner3 = () => + defineRefiner< + { c: string }, + { d: string; D: string } + >( + async function* (_denops, { items }) { + results.push("refiner3"); + for await (const item of items) { + yield { + ...item, + detail: { + ...item.detail, + d: "Hello", + D: "World", + }, + }; + } + }, + ); + const refiner = composeRefiners(refiner1, refiner2, refiner3); + const denops = new DenopsStub(); + const params = { + items: toAsyncIterable([{ + id: 0, + value: "123", + detail: { + a: "Hello", + A: "World", + }, + }]), + }; + const items = await Array.fromAsync(refiner.refine(denops, params, {})); + assertEquals(results, ["refiner3", "refiner2", "refiner1"]); + assertEquals(items, [{ + id: 0, + value: "123", + detail: { + a: "Hello", + A: "World", + b: "Hello", + B: "World", + c: "Hello", + C: "World", + d: "Hello", + D: "World", + }, + }]); + }); + + await t.step("without type constraint", () => { + const refiner1 = () => defineRefiner(async function* () {}); + const refiner2 = () => defineRefiner(async function* () {}); + const refiner3 = () => defineRefiner(async function* () {}); + const refiner = composeRefiners(refiner1, refiner2, refiner3); + assertType>>(true); + }); + + await t.step("with type constraint", () => { + type C1 = { a: string }; + type C2 = { b: string }; + type C3 = { c: string }; + type C4 = { d: string }; + const refiner1 = () => defineRefiner(async function* () {}); + const refiner2 = () => defineRefiner(async function* () {}); + const refiner3 = () => defineRefiner(async function* () {}); + composeRefiners( + refiner2, + // @ts-expect-error: refiner1 requires C1 but C3 is provided + refiner1, + refiner3, + ); + const refiner = composeRefiners(refiner1, refiner2, refiner3); + // NOTE: + // It seems `IsExact` could not properly compare the `Refiner` type + // so compare extracted types instead. + assertType, C1>>(true); + assertType< + IsExact, { + b: string; + c: string; + d: string; + }> + >(true); + }); + + await t.step("with type constraint (extra attributes)", () => { + type C1 = { a: string }; + type C2 = { b: string }; + type C3 = { c: string }; + type C4 = { d: string }; + const refiner1 = () => + defineRefiner( + async function* () {}, + ); + const refiner2 = () => + defineRefiner( + async function* () {}, + ); + const refiner3 = () => + defineRefiner( + async function* () {}, + ); + composeRefiners( + refiner2, + // @ts-expect-error: refiner1 requires C1 but C3 is provided + refiner1, + refiner3, + ); + const refiner = composeRefiners(refiner1, refiner2, refiner3); + // NOTE: + // It seems `IsExact` could not properly compare the `Refiner` type + // so compare extracted types instead. + assertType, C1>>(true); + assertType< + IsExact, { + b: string; + c: string; + d: string; + B: string; + C: string; + D: string; + }> + >(true); + }); + + await t.step("with type constraint (complicated)", () => { + const refiner1 = () => + defineRefiner<{ a: string }, { a: string; A: string }>( + async function* () {}, + ); + const refiner2 = () => + defineRefiner( + async function* () {}, + ); + const refiner3 = () => + defineRefiner<{ A: string }, { B: string }>( + async function* () {}, + ); + composeRefiners( + refiner2, + // @ts-expect-error: refiner1 requires C1 but C3 is provided + refiner1, + refiner3, + ); + const refiner = composeRefiners(refiner1, refiner2, refiner3); + // NOTE: + // It seems `IsExact` could not properly compare the `Refiner` type + // so compare extracted types instead. + assertType, { a: string }>>(true); + assertType< + IsExact, { + a: string; + A: string; + B: string; + }> + >(true); + }); + }); +}); + +Deno.test("refineSource", async (t) => { + await t.step("with bear refiners", async (t) => { + await t.step( + "returns a source that is refined by the refiners", + async () => { + const source = defineSource(async function* () { + yield* Array.from({ length: 5 }).map((_, id) => ({ + id, + value: id.toString(), + detail: { a: "Hello", b: "World" }, + })); + }); + // Modifier + const refiner1 = defineRefiner<{ a: string }, { a: string; A: string }>( + async function* (_denops, { items }) { + for await (const item of items) { + yield { + ...item, + detail: { + ...item.detail, + a: item.detail.a.toUpperCase(), + A: item.detail.a, + }, + }; + } + }, + ); + // Filter + const refiner2 = defineRefiner( + async function* (_denops, { items }) { + for await (const item of items) { + if (typeof item.id === "number" && item.id % 2 === 0) continue; + yield item; + } + }, + ); + // Annotator + const refiner3 = defineRefiner<{ A: string }, { B: string }>( + async function* (_denops, { items }) { + for await (const item of items) { + yield { + ...item, + detail: { ...item.detail, B: item.detail.A.repeat(3) }, + }; + } + }, + ); + const refinedSource = refineSource( + source, + refiner1, + refiner2, + refiner3, + ); + const denops = new DenopsStub(); + const params = { + args: [], + }; + const items = await Array.fromAsync( + refinedSource.collect(denops, params, {}), + ); + assertEquals(items, [ + { + detail: { + A: "Hello", + B: "HelloHelloHello", + a: "HELLO", + b: "World", + }, + id: 1, + value: "1", + }, + { + detail: { + A: "Hello", + B: "HelloHelloHello", + a: "HELLO", + b: "World", + }, + id: 3, + value: "3", + }, + ]); + }, + ); + + await t.step("check type constraint", () => { + const source = defineSource<{ a: string; b: string }>( + async function* () {}, + ); + const refiner1 = defineRefiner<{ a: string }, { a: string; A: string }>( + async function* () {}, + ); + const refiner2 = defineRefiner(async function* () {}); + const refiner3 = defineRefiner<{ A: string }, { B: string }>( + async function* () {}, + ); + const refinedSource = refineSource(source, refiner1, refiner2, refiner3); + assertType< + IsExact< + typeof refinedSource, + Source<{ a: string; b: string; A: string; B: string }> + > + >( + true, + ); + }); + }); + + await t.step("with derivable refiners", async (t) => { + await t.step( + "returns a source that is refined by the refiners", + async () => { + const source = () => + defineSource(async function* () { + yield* Array.from({ length: 5 }).map((_, id) => ({ + id, + value: id.toString(), + detail: { a: "Hello", b: "World" }, + })); + }); + // Modifier + const refiner1 = () => + defineRefiner<{ a: string }, { a: string; A: string }>( + async function* (_denops, { items }) { + for await (const item of items) { + yield { + ...item, + detail: { + ...item.detail, + a: item.detail.a.toUpperCase(), + A: item.detail.a, + }, + }; + } + }, + ); + // Filter + const refiner2 = () => + defineRefiner( + async function* (_denops, { items }) { + for await (const item of items) { + if (typeof item.id === "number" && item.id % 2 === 0) continue; + yield item; + } + }, + ); + // Annotator + const refiner3 = () => + defineRefiner<{ A: string }, { B: string }>( + async function* (_denops, { items }) { + for await (const item of items) { + yield { + ...item, + detail: { ...item.detail, B: item.detail.A.repeat(3) }, + }; + } + }, + ); + const refinedSource = refineSource( + source, + refiner1, + refiner2, + refiner3, + ); + const denops = new DenopsStub(); + const params = { + args: [], + }; + const items = await Array.fromAsync( + refinedSource.collect(denops, params, {}), + ); + assertEquals(items, [ + { + detail: { + A: "Hello", + B: "HelloHelloHello", + a: "HELLO", + b: "World", + }, + id: 1, + value: "1", + }, + { + detail: { + A: "Hello", + B: "HelloHelloHello", + a: "HELLO", + b: "World", + }, + id: 3, + value: "3", + }, + ]); + }, + ); + + await t.step("check type constraint", () => { + const source = () => + defineSource<{ a: string; b: string }>( + async function* () {}, + ); + const refiner1 = () => + defineRefiner<{ a: string }, { a: string; A: string }>( + async function* () {}, + ); + const refiner2 = () => defineRefiner(async function* () {}); + const refiner3 = () => + defineRefiner<{ A: string }, { B: string }>( + async function* () {}, + ); + const refinedSource = refineSource(source, refiner1, refiner2, refiner3); + assertType< + IsExact< + typeof refinedSource, + Source<{ a: string; b: string; A: string; B: string }> + > + >( + true, + ); + }); + }); +}); + +Deno.test("refineCurator", async (t) => { + await t.step("with bear refiners", async (t) => { + await t.step( + "returns a curator that is refined by the refiners", + async () => { + const curator = defineCurator(async function* () { + yield* Array.from({ length: 5 }).map((_, id) => ({ + id, + value: id.toString(), + detail: { a: "Hello", b: "World" }, + })); + }); + // Modifier + const refiner1 = defineRefiner<{ a: string }, { a: string; A: string }>( + async function* (_denops, { items }) { + for await (const item of items) { + yield { + ...item, + detail: { + ...item.detail, + a: item.detail.a.toUpperCase(), + A: item.detail.a, + }, + }; + } + }, + ); + // Filter + const refiner2 = defineRefiner( + async function* (_denops, { items }) { + for await (const item of items) { + if (typeof item.id === "number" && item.id % 2 === 0) continue; + yield item; + } + }, + ); + // Annotator + const refiner3 = defineRefiner<{ A: string }, { B: string }>( + async function* (_denops, { items }) { + for await (const item of items) { + yield { + ...item, + detail: { ...item.detail, B: item.detail.A.repeat(3) }, + }; + } + }, + ); + const refinedCurator = refineCurator( + curator, + refiner1, + refiner2, + refiner3, + ); + const denops = new DenopsStub(); + const params = { + args: [], + query: "", + }; + const items = await Array.fromAsync( + refinedCurator.curate(denops, params, {}), + ); + assertEquals(items, [ + { + detail: { + A: "Hello", + B: "HelloHelloHello", + a: "HELLO", + b: "World", + }, + id: 1, + value: "1", + }, + { + detail: { + A: "Hello", + B: "HelloHelloHello", + a: "HELLO", + b: "World", + }, + id: 3, + value: "3", + }, + ]); + }, + ); + + await t.step("check type constraint", () => { + const curator = defineCurator<{ a: string; b: string }>( + async function* () {}, + ); + const refiner1 = defineRefiner<{ a: string }, { a: string; A: string }>( + async function* () {}, + ); + const refiner2 = defineRefiner(async function* () {}); + const refiner3 = defineRefiner<{ A: string }, { B: string }>( + async function* () {}, + ); + const refinedCurator = refineCurator( + curator, + refiner1, + refiner2, + refiner3, + ); + assertType< + IsExact< + typeof refinedCurator, + Curator<{ a: string; b: string; A: string; B: string }> + > + >( + true, + ); + }); + }); + + await t.step("with derivable refiners", async (t) => { + await t.step( + "returns a curator that is refined by the refiners", + async () => { + const curator = () => + defineCurator(async function* () { + yield* Array.from({ length: 5 }).map((_, id) => ({ + id, + value: id.toString(), + detail: { a: "Hello", b: "World" }, + })); + }); + // Modifier + const refiner1 = () => + defineRefiner<{ a: string }, { a: string; A: string }>( + async function* (_denops, { items }) { + for await (const item of items) { + yield { + ...item, + detail: { + ...item.detail, + a: item.detail.a.toUpperCase(), + A: item.detail.a, + }, + }; + } + }, + ); + // Filter + const refiner2 = () => + defineRefiner( + async function* (_denops, { items }) { + for await (const item of items) { + if (typeof item.id === "number" && item.id % 2 === 0) continue; + yield item; + } + }, + ); + // Annotator + const refiner3 = () => + defineRefiner<{ A: string }, { B: string }>( + async function* (_denops, { items }) { + for await (const item of items) { + yield { + ...item, + detail: { ...item.detail, B: item.detail.A.repeat(3) }, + }; + } + }, + ); + const refinedCurator = refineCurator( + curator, + refiner1, + refiner2, + refiner3, + ); + const denops = new DenopsStub(); + const params = { + args: [], + query: "", + }; + const items = await Array.fromAsync( + refinedCurator.curate(denops, params, {}), + ); + assertEquals(items, [ + { + detail: { + A: "Hello", + B: "HelloHelloHello", + a: "HELLO", + b: "World", + }, + id: 1, + value: "1", + }, + { + detail: { + A: "Hello", + B: "HelloHelloHello", + a: "HELLO", + b: "World", + }, + id: 3, + value: "3", + }, + ]); + }, + ); + + await t.step("check type constraint", () => { + const curator = () => + defineCurator<{ a: string; b: string }>( + async function* () {}, + ); + const refiner1 = () => + defineRefiner<{ a: string }, { a: string; A: string }>( + async function* () {}, + ); + const refiner2 = () => defineRefiner(async function* () {}); + const refiner3 = () => + defineRefiner<{ A: string }, { B: string }>( + async function* () {}, + ); + const refinedCurator = refineCurator( + curator, + refiner1, + refiner2, + refiner3, + ); + assertType< + IsExact< + typeof refinedCurator, + Curator<{ a: string; b: string; A: string; B: string }> + > + >( + true, + ); + }); + }); +}); diff --git a/renderer.ts b/renderer.ts index 2b0d22b..94c8b8a 100644 --- a/renderer.ts +++ b/renderer.ts @@ -1,6 +1,9 @@ +export type * from "@vim-fall/core/renderer"; + import type { Denops } from "@denops/std"; import type { Renderer, RenderParams } from "@vim-fall/core/renderer"; +import type { Detail, DetailUnit } from "./item.ts"; import { type DerivableArray, deriveArray } from "./util/derivable.ts"; /** @@ -9,7 +12,7 @@ import { type DerivableArray, deriveArray } from "./util/derivable.ts"; * @param render - A function that renders items based on provided parameters. * @returns A renderer object containing the `render` function. */ -export function defineRenderer( +export function defineRenderer( render: ( denops: Denops, params: RenderParams, @@ -28,11 +31,8 @@ export function defineRenderer( * @param renderers - The renderers to compose. * @returns A single renderer that applies all given renderers in sequence. */ -export function composeRenderers< - T, - R extends DerivableArray<[Renderer, ...Renderer[]]>, ->( - ...renderers: R +export function composeRenderers( + ...renderers: DerivableArray<[Renderer, ...Renderer[]]> ): Renderer { return { render: async (denops, params, options) => { @@ -42,5 +42,3 @@ export function composeRenderers< }, }; } - -export type * from "@vim-fall/core/renderer"; diff --git a/renderer_test.ts b/renderer_test.ts index 7088bc5..ad1ceac 100644 --- a/renderer_test.ts +++ b/renderer_test.ts @@ -1,52 +1,154 @@ import { assertEquals } from "@std/assert"; -import { assertType, type IsExact } from "@std/testing/types"; +import { type AssertTrue, assertType, type IsExact } from "@std/testing/types"; import { DenopsStub } from "@denops/test/stub"; -import { composeRenderers, defineRenderer, type Renderer } from "./renderer.ts"; +import type { DetailUnit } from "./item.ts"; +import { + composeRenderers, + defineRenderer, + type Renderer, + type RenderParams, +} from "./renderer.ts"; -Deno.test("defineRenderer", () => { - const renderer = defineRenderer(async () => {}); - assertEquals(typeof renderer.render, "function"); - assertType>>(true); -}); +Deno.test("defineRenderer", async (t) => { + await t.step("without type contraint", () => { + const renderer = defineRenderer((_denops, params) => { + type _ = AssertTrue>>; + }); + assertType>>(true); + }); -Deno.test("composeRenderers", async () => { - const results: string[] = []; - const renderer1 = defineRenderer((_denops, { items }) => { - results.push("renderer1"); - items.forEach((item) => { - item.label = `${item.label}-1`; + await t.step("with type contraint", () => { + type C = { a: string }; + const renderer = defineRenderer((_denops, params) => { + type _ = AssertTrue>>; }); + assertType>>(true); }); - const renderer2 = defineRenderer((_denops, { items }) => { - results.push("renderer2"); - items.forEach((item) => { - item.label = `${item.label}-2`; +}); + +Deno.test("composeRenderers", async (t) => { + await t.step("with bear renderers", async (t) => { + await t.step("renderers are applied in order", async () => { + const results: string[] = []; + const renderer1 = defineRenderer((_denops, { items }) => { + results.push("renderer1"); + items.forEach((item) => { + item.label = `${item.label}-1`; + }); + }); + const renderer2 = defineRenderer((_denops, { items }) => { + results.push("renderer2"); + items.forEach((item) => { + item.label = `${item.label}-2`; + }); + }); + const renderer3 = defineRenderer((_denops, { items }) => { + results.push("renderer3"); + items.forEach((item) => { + item.label = `${item.label}-3`; + }); + }); + const renderer = composeRenderers(renderer2, renderer1, renderer3); + const denops = new DenopsStub(); + const params = { + items: [{ + id: 0, + value: "Hello", + label: "Hello", + detail: {}, + decorations: [], + }], + }; + await renderer.render(denops, params, {}); + assertEquals(results, ["renderer2", "renderer1", "renderer3"]); + assertEquals(params.items, [{ + id: 0, + value: "Hello", + label: "Hello-2-1-3", + detail: {}, + decorations: [], + }]); + }); + + await t.step("without type constraint", () => { + const renderer1 = defineRenderer(() => {}); + const renderer2 = defineRenderer(() => {}); + const renderer3 = defineRenderer(() => {}); + const renderer = composeRenderers(renderer1, renderer2, renderer3); + assertType>>(true); + }); + + await t.step("with type constraint", () => { + type C = { a: string }; + const renderer1 = defineRenderer(() => {}); + const renderer2 = defineRenderer(() => {}); + const renderer3 = defineRenderer(() => {}); + const renderer = composeRenderers(renderer1, renderer2, renderer3); + assertType>>(true); }); }); - const renderer3 = defineRenderer((_denops, { items }) => { - results.push("renderer3"); - items.forEach((item) => { - item.label = `${item.label}-3`; + + await t.step("with derivable renderers", async (t) => { + await t.step("renderers are applied in order", async () => { + const results: string[] = []; + const renderer1 = () => + defineRenderer((_denops, { items }) => { + results.push("renderer1"); + items.forEach((item) => { + item.label = `${item.label}-1`; + }); + }); + const renderer2 = () => + defineRenderer((_denops, { items }) => { + results.push("renderer2"); + items.forEach((item) => { + item.label = `${item.label}-2`; + }); + }); + const renderer3 = () => + defineRenderer((_denops, { items }) => { + results.push("renderer3"); + items.forEach((item) => { + item.label = `${item.label}-3`; + }); + }); + const renderer = composeRenderers(renderer2, renderer1, renderer3); + const denops = new DenopsStub(); + const params = { + items: [{ + id: 0, + value: "Hello", + label: "Hello", + detail: {}, + decorations: [], + }], + }; + await renderer.render(denops, params, {}); + assertEquals(results, ["renderer2", "renderer1", "renderer3"]); + assertEquals(params.items, [{ + id: 0, + value: "Hello", + label: "Hello-2-1-3", + detail: {}, + decorations: [], + }]); + }); + + await t.step("without type constraint", () => { + const renderer1 = () => defineRenderer(() => {}); + const renderer2 = () => defineRenderer(() => {}); + const renderer3 = () => defineRenderer(() => {}); + const renderer = composeRenderers(renderer1, renderer2, renderer3); + assertType>>(true); + }); + + await t.step("with type constraint", () => { + type C = { a: string }; + const renderer1 = () => defineRenderer(() => {}); + const renderer2 = () => defineRenderer(() => {}); + const renderer3 = () => defineRenderer(() => {}); + const renderer = composeRenderers(renderer1, renderer2, renderer3); + assertType>>(true); }); }); - const renderer = composeRenderers(renderer2, renderer1, renderer3); - const denops = new DenopsStub(); - const params = { - items: [{ - id: 0, - value: "Hello", - label: "Hello", - detail: undefined, - decorations: [], - }], - }; - await renderer.render(denops, params, {}); - assertEquals(results, ["renderer2", "renderer1", "renderer3"]); - assertEquals(params.items, [{ - id: 0, - value: "Hello", - label: "Hello-2-1-3", - detail: undefined, - decorations: [], - }]); }); diff --git a/sorter.ts b/sorter.ts index 980de12..7710de6 100644 --- a/sorter.ts +++ b/sorter.ts @@ -1,6 +1,9 @@ +export type * from "@vim-fall/core/sorter"; + import type { Denops } from "@denops/std"; import type { Sorter, SortParams } from "@vim-fall/core/sorter"; +import type { Detail, DetailUnit } from "./item.ts"; import { type DerivableArray, deriveArray } from "./util/derivable.ts"; /** @@ -9,7 +12,7 @@ import { type DerivableArray, deriveArray } from "./util/derivable.ts"; * @param sort - A function that sorts items based on given parameters. * @returns A sorter object containing the `sort` function. */ -export function defineSorter( +export function defineSorter( sort: ( denops: Denops, params: SortParams, @@ -28,10 +31,9 @@ export function defineSorter( * @param sorters - The sorters to compose. * @returns A single sorter that applies all given sorters in sequence. */ -export function composeSorters< - T, - S extends DerivableArray<[Sorter, ...Sorter[]]>, ->(...sorters: S): Sorter { +export function composeSorters( + ...sorters: DerivableArray<[Sorter, ...Sorter[]]> +): Sorter { return { sort: async (denops, params, options) => { for (const sorter of deriveArray(sorters)) { @@ -40,5 +42,3 @@ export function composeSorters< }, }; } - -export type * from "@vim-fall/core/sorter"; diff --git a/sorter_test.ts b/sorter_test.ts index 02b7531..ddc73a4 100644 --- a/sorter_test.ts +++ b/sorter_test.ts @@ -1,49 +1,87 @@ import { assertEquals } from "@std/assert"; -import { assertType, type IsExact } from "@std/testing/types"; +import { type AssertTrue, assertType, type IsExact } from "@std/testing/types"; import { DenopsStub } from "@denops/test/stub"; -import { composeSorters, defineSorter, type Sorter } from "./sorter.ts"; +import type { DetailUnit } from "./item.ts"; +import { + composeSorters, + defineSorter, + type Sorter, + type SortParams, +} from "./sorter.ts"; -Deno.test("defineSorter", () => { - const sorter = defineSorter(async () => {}); - assertEquals(typeof sorter.sort, "function"); - assertType>>(true); -}); - -Deno.test("composeSorters", async () => { - const results: string[] = []; - const sorter1 = defineSorter((_denops, { items }) => { - results.push("sorter1"); - items.sort((a, b) => a.value.localeCompare(b.value)); +Deno.test("defineSorter", async (t) => { + await t.step("without type contraint", () => { + const sorter = defineSorter((_denops, params) => { + type _ = AssertTrue>>; + }); + assertType>>(true); }); - const sorter2 = defineSorter((_denops, { items }) => { - results.push("sorter2"); - items.sort((a, b) => a.value.localeCompare(b.value)); + + await t.step("with type contraint", () => { + type C = { a: string }; + const sorter = defineSorter((_denops, params) => { + type _ = AssertTrue>>; + }); + assertType>>(true); }); - const sorter3 = defineSorter((_denops, { items }) => { - results.push("sorter3"); - items.sort((a, b) => b.value.localeCompare(a.value)); +}); + +Deno.test("composeSorters", async (t) => { + await t.step("with bear sorters", async (t) => { + await t.step("sorters are applied in order", async () => { + const results: string[] = []; + const sorter1 = defineSorter((_denops, { items }) => { + results.push("sorter1"); + items.sort((a, b) => a.value.localeCompare(b.value)); + }); + const sorter2 = defineSorter((_denops, { items }) => { + results.push("sorter2"); + items.sort((a, b) => a.value.localeCompare(b.value)); + }); + const sorter3 = defineSorter((_denops, { items }) => { + results.push("sorter3"); + items.sort((a, b) => b.value.localeCompare(a.value)); + }); + const sorter = composeSorters(sorter2, sorter1, sorter3); + const denops = new DenopsStub(); + const params = { + items: Array.from({ length: 10 }).map((_, id) => ({ + id, + value: id.toString(), + detail: {}, + })), + }; + await sorter.sort(denops, params, {}); + assertEquals(results, ["sorter2", "sorter1", "sorter3"]); + assertEquals(params.items.map((v) => v.value), [ + "9", + "8", + "7", + "6", + "5", + "4", + "3", + "2", + "1", + "0", + ]); + }); + + await t.step("without type constraint", () => { + const sorter1 = defineSorter(() => {}); + const sorter2 = defineSorter(() => {}); + const sorter3 = defineSorter(() => {}); + const sorter = composeSorters(sorter1, sorter2, sorter3); + assertType>>(true); + }); + + await t.step("with type constraint", () => { + type C = { a: string }; + const sorter1 = defineSorter(() => {}); + const sorter2 = defineSorter(() => {}); + const sorter3 = defineSorter(() => {}); + const sorter = composeSorters(sorter1, sorter2, sorter3); + assertType>>(true); + }); }); - const sorter = composeSorters(sorter2, sorter1, sorter3); - const denops = new DenopsStub(); - const params = { - items: Array.from({ length: 10 }).map((_, id) => ({ - id, - value: id.toString(), - detail: undefined, - })), - }; - await sorter.sort(denops, params, {}); - assertEquals(results, ["sorter2", "sorter1", "sorter3"]); - assertEquals(params.items.map((v) => v.value), [ - "9", - "8", - "7", - "6", - "5", - "4", - "3", - "2", - "1", - "0", - ]); }); diff --git a/source.ts b/source.ts index ff22741..c092f1c 100644 --- a/source.ts +++ b/source.ts @@ -1,7 +1,9 @@ +export type * from "@vim-fall/core/source"; + import type { Denops } from "@denops/std"; import type { CollectParams, Source } from "@vim-fall/core/source"; -import type { IdItem } from "./item.ts"; +import type { Detail, IdItem } from "./item.ts"; import { type DerivableArray, deriveArray } from "./util/derivable.ts"; /** @@ -10,7 +12,7 @@ import { type DerivableArray, deriveArray } from "./util/derivable.ts"; * @param collect - A function that collects items asynchronously. * @returns A source object containing the `collect` function. */ -export function defineSource( +export function defineSource( collect: ( denops: Denops, params: CollectParams, @@ -31,7 +33,7 @@ export function defineSource( * @returns A single composed source that collects items from all given sources. */ export function composeSources< - S extends DerivableArray<[Source, ...Source[]]>, + S extends DerivableArray<[Source, ...Source[]]>, R extends UnionSource, >(...sources: S): Source { return { @@ -52,10 +54,8 @@ export function composeSources< * @template S - Array of sources to create a union type from. */ type UnionSource< - S extends DerivableArray[]>, + S extends DerivableArray[]>, > = S extends DerivableArray< - [Source, ...infer R extends DerivableArray[]>] + [Source, ...infer R extends DerivableArray[]>] > ? T | UnionSource : never; - -export type * from "@vim-fall/core/source"; diff --git a/source_test.ts b/source_test.ts index c3bcd21..2b99be6 100644 --- a/source_test.ts +++ b/source_test.ts @@ -1,68 +1,127 @@ import { assertEquals } from "@std/assert"; import { assertType, type IsExact } from "@std/testing/types"; import { DenopsStub } from "@denops/test/stub"; +import type { Detail } from "./item.ts"; import { composeSources, defineSource, type Source } from "./source.ts"; -Deno.test("defineSource", () => { - const source = defineSource(async function* () {}); - assertEquals(typeof source.collect, "function"); - assertType>>(true); -}); - -Deno.test("composeSources", async () => { - const results: string[] = []; - const source1 = defineSource(async function* () { - results.push("source1"); - yield* Array.from({ length: 3 }).map((_, id) => ({ - id, - value: `A-${id}`, - detail: { - a: id, - }, - })); +Deno.test("defineSource", async (t) => { + await t.step("without type contraint", async () => { + const source = defineSource(async function* () { + yield { id: 1, value: "1", detail: { a: "" } }; + yield { id: 2, value: "2", detail: { a: "" } }; + yield { id: 3, value: "3", detail: { a: "" } }; + }); + assertType>>(true); + const denops = new DenopsStub(); + const params = { + args: [], + query: "", + }; + const items = await Array.fromAsync(source.collect(denops, params, {})); + assertEquals(items, [ + { id: 1, value: "1", detail: { a: "" } }, + { id: 2, value: "2", detail: { a: "" } }, + { id: 3, value: "3", detail: { a: "" } }, + ]); }); - const source2 = defineSource(async function* () { - results.push("source2"); - yield* Array.from({ length: 3 }).map((_, id) => ({ - id, - value: `B-${id}`, - detail: { - b: id, - }, - })); + + await t.step("with type contraint", async () => { + type C = { a: string }; + // @ts-expect-error: 'detail' does not follow the type constraint + defineSource(async function* () { + yield { id: 1, value: "1", detail: "invalid detail" }; + }); + const source = defineSource(async function* () { + yield { id: 1, value: "1", detail: { a: "" } }; + yield { id: 2, value: "2", detail: { a: "" } }; + yield { id: 3, value: "3", detail: { a: "" } }; + }); + assertType>>(true); + const denops = new DenopsStub(); + const params = { + args: [], + query: "", + }; + const items = await Array.fromAsync(source.collect(denops, params, {})); + assertEquals(items, [ + { id: 1, value: "1", detail: { a: "" } }, + { id: 2, value: "2", detail: { a: "" } }, + { id: 3, value: "3", detail: { a: "" } }, + ]); }); - const source3 = defineSource(async function* () { - results.push("source3"); - yield* Array.from({ length: 3 }).map((_, id) => ({ - id, - value: `C-${id}`, - detail: { - c: id, - }, - })); +}); + +Deno.test("composeSources", async (t) => { + await t.step("with bear sources", async (t) => { + await t.step("sources are applied in order", async () => { + const results: string[] = []; + const source1 = defineSource(async function* () { + results.push("source1"); + yield* Array.from({ length: 3 }).map((_, id) => ({ + id, + value: `A-${id}`, + detail: { + a: id, + }, + })); + }); + const source2 = defineSource(async function* () { + results.push("source2"); + yield* Array.from({ length: 3 }).map((_, id) => ({ + id, + value: `B-${id}`, + detail: { + b: id, + }, + })); + }); + const source3 = defineSource(async function* () { + results.push("source3"); + yield* Array.from({ length: 3 }).map((_, id) => ({ + id, + value: `C-${id}`, + detail: { + c: id, + }, + })); + }); + const source = composeSources(source2, source1, source3); + const denops = new DenopsStub(); + const params = { + args: [], + }; + const items = await Array.fromAsync(source.collect(denops, params, {})); + assertEquals(results, ["source2", "source1", "source3"]); + assertEquals(items.map((v) => v.value), [ + "B-0", + "B-1", + "B-2", + "A-0", + "A-1", + "A-2", + "C-0", + "C-1", + "C-2", + ]); + }); + + await t.step("without type constraint", () => { + const source1 = defineSource(async function* () {}); + const source2 = defineSource(async function* () {}); + const source3 = defineSource(async function* () {}); + const source = composeSources(source2, source1, source3); + assertType>>(true); + }); + + await t.step("with type constraint", () => { + type C1 = { a: string }; + type C2 = { b: string }; + type C3 = { c: string }; + const source1 = defineSource(async function* () {}); + const source2 = defineSource(async function* () {}); + const source3 = defineSource(async function* () {}); + const source = composeSources(source2, source1, source3); + assertType>>(true); + }); }); - const source = composeSources(source2, source1, source3); - assertType< - IsExact< - typeof source, - Source<{ a: number } | { b: number } | { c: number }> - > - >(true); - const denops = new DenopsStub(); - const params = { - args: [], - }; - const items = await Array.fromAsync(source.collect(denops, params, {})); - assertEquals(results, ["source2", "source1", "source3"]); - assertEquals(items.map((v) => v.value), [ - "B-0", - "B-1", - "B-2", - "A-0", - "A-1", - "A-2", - "C-0", - "C-1", - "C-2", - ]); }); diff --git a/util/derivable.ts b/util/derivable.ts index 7f78538..5083a50 100644 --- a/util/derivable.ts +++ b/util/derivable.ts @@ -55,7 +55,7 @@ export function deriveMap< * @returns A new array with each element resolved. */ export function deriveArray< - A extends NonFunction[], + A extends NonFunction[], R extends { [K in keyof A]: A[K] extends Derivable ? T : A[K] }, >(array: A): R { return array.map((v) => derive(v)) as R;