From a80cc4303af877d3e06ee6ae70522f6a073cf736 Mon Sep 17 00:00:00 2001 From: Alisue Date: Sun, 10 Nov 2024 04:12:18 +0900 Subject: [PATCH 01/13] deps: upgrade @vim-fall/core to 0.1.3 --- deno.jsonc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/deno.jsonc b/deno.jsonc index b19be5c..0e0022a 100644 --- a/deno.jsonc +++ b/deno.jsonc @@ -123,7 +123,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", + "@vim-fall/core": "jsr:@vim-fall/core@^0.1.3", "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" From 17da474c818c89a18fba70b191c9cebb1d21434b Mon Sep 17 00:00:00 2001 From: Alisue Date: Sun, 10 Nov 2024 04:12:33 +0900 Subject: [PATCH 02/13] feat: add removed `Projector` types --- projector.ts | 39 ++++++++++++++++++++++++++++++++++++--- 1 file changed, 36 insertions(+), 3 deletions(-) diff --git a/projector.ts b/projector.ts index 9ac90de..0ac79eb 100644 --- a/projector.ts +++ b/projector.ts @@ -1,6 +1,5 @@ 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"; @@ -12,6 +11,42 @@ import { deriveArray, } from "./util/derivable.ts"; +/** + * Parameters for projecting items. + */ +export type ProjectParams = { + /** + * Async iterable of items to project. + */ + readonly items: AsyncIterable>; +}; + +/** + * Projector that processes items from the source or curator. + * + * Generally, the projector has the following two roles: + * + * 1. Filter items from the source or curator based on criteria other than user input (Filter) + * 2. Transform items from the source or curator to adapt the item type for further processing (Modifier) + * + * In built-in extensions, projectors are categorized into the two types above. + */ +export type Projector = { + /** + * Projects items for further processing. + * + * @param denops - The Denops instance. + * @param params - Parameters specifying the items to project. + * @param options - Additional options, including an abort signal. + * @returns An async iterator over the projected `IdItem` elements of type `U`. + */ + project( + denops: Denops, + params: ProjectParams, + options: { signal?: AbortSignal }, + ): AsyncIterableIterator>; +}; + /** * Defines a projector responsible for transforming or filtering items. * @@ -103,5 +138,3 @@ export function pipeProjectors< }) as R; } } - -export type * from "@vim-fall/core/projector"; From f3ecb13996a869f7fce2deb9ea399776c0a71c3b Mon Sep 17 00:00:00 2001 From: Alisue Date: Sun, 10 Nov 2024 04:12:52 +0900 Subject: [PATCH 03/13] feat: fix `Matcher` types --- builtin/action/submatch.ts | 6 +++--- builtin/matcher/fzf.ts | 4 ++-- matcher.ts | 6 +++--- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/builtin/action/submatch.ts b/builtin/action/submatch.ts index 0239de3..28e2794 100644 --- a/builtin/action/submatch.ts +++ b/builtin/action/submatch.ts @@ -159,9 +159,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/matcher/fzf.ts b/builtin/matcher/fzf.ts index 54bb93a..d94ad0c 100644 --- a/builtin/matcher/fzf.ts +++ b/builtin/matcher/fzf.ts @@ -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/matcher.ts b/matcher.ts index 556ad59..1686837 100644 --- a/matcher.ts +++ b/matcher.ts @@ -11,11 +11,11 @@ import { type DerivableArray, deriveArray } from "./util/derivable.ts"; * @returns A matcher object containing the `match` function. */ export function defineMatcher( - match: ( + match: ( denops: Denops, - params: MatchParams, + params: MatchParams, options: { signal?: AbortSignal }, - ) => AsyncIterableIterator>, + ) => AsyncIterableIterator>, ): Matcher { return { match }; } From 0539561b83fd66934b75505603c97ad6483ed7bf Mon Sep 17 00:00:00 2001 From: Alisue Date: Sun, 10 Nov 2024 06:41:02 +0900 Subject: [PATCH 04/13] docs: remove usage section from README.md It seems there are type issue so remove it for now. --- README.md | 154 ------------------------------------------------------ 1 file changed, 154 deletions(-) diff --git a/README.md b/README.md index b5ce520..1c62f67 100644 --- a/README.md +++ b/README.md @@ -11,160 +11,6 @@ Vim/Neovim Fuzzy Finder plugin powered by It is also used to develop extensions of Fall. -## 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 - }); -}; -``` - ## License The code in this repository follows the MIT license, as detailed in From 5915d3eb07236409331cb974ee123d08734183ee Mon Sep 17 00:00:00 2001 From: Alisue Date: Sun, 10 Nov 2024 06:41:41 +0900 Subject: [PATCH 05/13] feat!: remove `Projector` and add `Modifier` and `Filter` --- builtin/filter/cwd.ts | 12 +- builtin/filter/exists.ts | 12 +- builtin/filter/noop.ts | 14 +- builtin/filter/regexp.ts | 12 +- builtin/modifier/mod.ts | 1 - builtin/modifier/noop.ts | 13 -- builtin/modifier/relative_path.ts | 8 +- deno.jsonc | 6 +- filter.ts | 39 +++++ filter_test.ts | 125 ++++++++++++++ matcher.ts | 7 +- matcher_test.ts | 115 ++++++++----- mod.ts | 3 +- modifier.ts | 51 ++++++ modifier_test.ts | 258 +++++++++++++++++++++++++++++ projector.ts | 140 ---------------- projector_test.ts | 260 ------------------------------ 17 files changed, 588 insertions(+), 488 deletions(-) delete mode 100644 builtin/modifier/noop.ts create mode 100644 filter.ts create mode 100644 filter_test.ts create mode 100644 modifier.ts create mode 100644 modifier_test.ts delete mode 100644 projector.ts delete mode 100644 projector_test.ts diff --git a/builtin/filter/cwd.ts b/builtin/filter/cwd.ts index 5c0238a..1655423 100644 --- a/builtin/filter/cwd.ts +++ b/builtin/filter/cwd.ts @@ -1,6 +1,6 @@ import * as fn from "@denops/std/function"; -import { defineProjector, type Projector } from "../../projector.ts"; +import { defineFilter, type Filter } from "../../filter.ts"; /** * Represents detailed information for each item, specifically the file path. @@ -10,14 +10,14 @@ type Detail = { }; /** - * Creates a Projector that filters items based on the current working directory. + * Creates a Filter that filters items based on the current working directory. * - * This Projector yields only those items whose `path` is within the current working directory. + * This Filter yields only those items whose `path` is within the current working directory. * - * @returns A Projector that filters items according to the current working directory. + * @returns A Filter that filters items according to the current working directory. */ -export function cwd(): Projector { - return defineProjector(async function* (denops, { items }, { signal }) { +export function cwd(): Filter { + return defineFilter(async function* (denops, { items }, { signal }) { // Retrieve the current working directory const cwd = await fn.getcwd(denops); signal?.throwIfAborted(); diff --git a/builtin/filter/exists.ts b/builtin/filter/exists.ts index 3e80015..0978f51 100644 --- a/builtin/filter/exists.ts +++ b/builtin/filter/exists.ts @@ -1,6 +1,6 @@ import { exists as exists_ } from "@std/fs/exists"; -import { defineProjector, type Projector } from "../../projector.ts"; +import { defineFilter, type Filter } from "../../filter.ts"; /** * Represents detailed information for each item, specifically the file path. @@ -10,15 +10,15 @@ type Detail = { }; /** - * Creates a Projector that filters items based on file existence. + * Creates a Filter that filters items based on file existence. * - * This Projector checks each item's `path` and yields only those items + * This Filter 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. + * @returns A Filter that filters items according to file existence. */ -export function exists(): Projector { - return defineProjector(async function* (_denops, { items }, { signal }) { +export function exists(): Filter { + return defineFilter(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)) { diff --git a/builtin/filter/noop.ts b/builtin/filter/noop.ts index ec8b5f5..4aa3578 100644 --- a/builtin/filter/noop.ts +++ b/builtin/filter/noop.ts @@ -1,13 +1,13 @@ -import { defineProjector, type Projector } from "../../projector.ts"; +import { defineFilter, type Filter } from "../../filter.ts"; /** - * A no-operation (noop) Projector. + * A no-operation (noop) Filter. * - * 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. + * This Filter does nothing and yields no items. It can be used as a placeholder + * or a default value where a Filter is required but no action is needed. * - * @returns A Projector that yields nothing. + * @returns A Filter that yields nothing. */ -export function noop(): Projector { - return defineProjector(async function* () {}); +export function noop(): Filter { + return defineFilter(async function* () {}); } diff --git a/builtin/filter/regexp.ts b/builtin/filter/regexp.ts index 75f97df..60ac6ef 100644 --- a/builtin/filter/regexp.ts +++ b/builtin/filter/regexp.ts @@ -1,4 +1,4 @@ -import { defineProjector, type Projector } from "../../projector.ts"; +import { defineFilter, type Filter } from "../../filter.ts"; /** * Options for filtering items by regular expressions. @@ -27,19 +27,19 @@ type Detail = { }; /** - * Creates a Projector that filters items based on regular expression patterns. + * Creates a Filter that filters items based on regular expression patterns. * - * The `regexp` Projector filters items using `includes` and/or `excludes` patterns. + * The `regexp` Filter 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. + * @returns A Filter that yields items matching the specified patterns. */ export function regexp( { includes, excludes }: Readonly, -): Projector { - return defineProjector(async function* (_denops, { items }, { signal }) { +): Filter { + return defineFilter(async function* (_denops, { items }, { signal }) { signal?.throwIfAborted(); // Process each item and yield only those matching the filter conditions diff --git a/builtin/modifier/mod.ts b/builtin/modifier/mod.ts index 4e8eb41..52c6faa 100644 --- a/builtin/modifier/mod.ts +++ b/builtin/modifier/mod.ts @@ -1,3 +1,2 @@ // 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 index 0a48e55..ad93d55 100644 --- a/builtin/modifier/relative_path.ts +++ b/builtin/modifier/relative_path.ts @@ -2,7 +2,7 @@ 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"; +import { defineModifier, type Modifier } from "../../modifier.ts"; /** * Represents item details with a file path. @@ -30,8 +30,8 @@ type DetailAfter = { export function relativePath< T extends Detail, U extends T & DetailAfter, ->(): Projector { - return defineProjector(async function* (denops, { items }, { signal }) { +>(): Modifier { + return defineModifier(async function* (denops, { items }, { signal }) { // Get the current working directory const cwd = await fn.getcwd(denops); signal?.throwIfAborted(); @@ -50,7 +50,7 @@ export function relativePath< path: relpath, abspath: item.detail.path, }, - } as IdItem; + } as IdItem; } }); } diff --git a/deno.jsonc b/deno.jsonc index 0e0022a..b10d0e0 100644 --- a/deno.jsonc +++ b/deno.jsonc @@ -37,7 +37,6 @@ "./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", @@ -70,10 +69,11 @@ "./config": "./config.ts", "./coordinator": "./coordinator.ts", "./curator": "./curator.ts", + "./filter": "./filter.ts", "./item": "./item.ts", "./matcher": "./matcher.ts", + "./modifier": "./modifier.ts", "./previewer": "./previewer.ts", - "./projector": "./projector.ts", "./renderer": "./renderer.ts", "./sorter": "./sorter.ts", "./source": "./source.ts", @@ -123,7 +123,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.3", + "@vim-fall/core": "jsr:@vim-fall/core@^0.1.4", "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" diff --git a/filter.ts b/filter.ts new file mode 100644 index 0000000..085a147 --- /dev/null +++ b/filter.ts @@ -0,0 +1,39 @@ +import type { Denops } from "@denops/std"; +import type { IdItem } from "@vim-fall/core/item"; + +import { defineSource, type Source } from "./source.ts"; +import { type Curator, defineCurator } from "./curator.ts"; + +export type FilterParams = { + readonly items: AsyncIterable>; +}; + +export type Filter = ( | Curator>(source: S) => S) & { + __phantom?: T; +}; + +export function defineFilter( + modify: ( + denops: Denops, + params: FilterParams, + options: { signal?: AbortSignal }, + ) => AsyncIterableIterator>, +): Filter { + return ((source) => { + if ("collect" in source) { + return defineSource( + (denops, params, options) => { + const items = source.collect(denops, params, options); + return modify(denops, { items }, options); + }, + ); + } else { + return defineCurator( + (denops, params, options) => { + const items = source.curate(denops, params, options); + return modify(denops, { items }, options); + }, + ); + } + }) as Filter; +} diff --git a/filter_test.ts b/filter_test.ts new file mode 100644 index 0000000..ec1f7a5 --- /dev/null +++ b/filter_test.ts @@ -0,0 +1,125 @@ +import { assertEquals } from "@std/assert"; +import { assertType, type IsExact } from "@std/testing/types"; + +import { defineFilter, type Filter, type FilterParams } from "./filter.ts"; +import type { Source } from "./source.ts"; +import type { Curator } from "./curator.ts"; + +Deno.test("Filter", async (t) => { + await t.step("with Source", async (t) => { + const filter = (() => {}) as Filter<{ a: string }>; + + await t.step("passed type is equal to the type restriction", () => { + const modified = filter({} as Source<{ a: string }>); + assertType< + IsExact< + typeof modified, + Source<{ a: string }> + > + >(true); + }); + + await t.step("passed type establishes the type restriction", () => { + const modified = filter({} as Source<{ a: string; b: string }>); + assertType< + IsExact< + typeof modified, + Source<{ a: string; b: string }> + > + >(true); + }); + + await t.step( + "passed type does not establish the type restriction", + () => { + // @ts-expect-error: 'a' is missing + filter({} as Source<{ b: string }>); + }, + ); + + await t.step( + "check if the type constraint correctly triggers the type checking", + () => { + const filter1 = (() => {}) as Filter<{ a: string }>; + const filter2 = (() => {}) as Filter<{ b: string }>; + const filter3 = (() => {}) as Filter<{ c: string }>; + function strictFunction(_: Filter) {} + strictFunction(filter1); + // @ts-expect-error: 'a' is missing + strictFunction(filter2); + // @ts-expect-error: 'a' is missing + strictFunction(filter3); + }, + ); + }); + + await t.step("with Curator", async (t) => { + const filter = (() => {}) as Filter<{ a: string }>; + + await t.step("passed type is equal to the type restriction", () => { + const modified = filter({} as Curator<{ a: string }>); + assertType< + IsExact< + typeof modified, + Curator<{ a: string }> + > + >(true); + }); + + await t.step("passed type establishes the type restriction", () => { + const modified = filter({} as Curator<{ a: string; b: string }>); + assertType< + IsExact< + typeof modified, + Curator<{ a: string; b: string }> + > + >(true); + }); + + await t.step( + "passed type does not establish the type restriction", + () => { + // @ts-expect-error: 'a' is missing + filter({} as Curator<{ b: string }>); + }, + ); + + await t.step( + "check if the type constraint correctly triggers the type checking", + () => { + const filter1 = (() => {}) as Filter<{ a: string }>; + const filter2 = (() => {}) as Filter<{ b: string }>; + const filter3 = (() => {}) as Filter<{ c: string }>; + function strictFunction(_: Filter) {} + strictFunction(filter1); + // @ts-expect-error: 'a' is missing + strictFunction(filter2); + // @ts-expect-error: 'a' is missing + strictFunction(filter3); + }, + ); + }); +}); + +Deno.test("defineFilter", async (t) => { + await t.step("without type constraint", () => { + const filter = defineFilter(async function* (_denops, params) { + // @ts-expect-error: `params` is not type restrained + const _: FilterParams<{ a: string }> = params; + yield* []; + }); + assertEquals(typeof filter, "function"); + assertType>>(true); + }); + + await t.step("without type constraint T", () => { + const filter = defineFilter<{ a: string }>( + async function* (_denops, params) { + const _: FilterParams<{ a: string }> = params; + yield* []; + }, + ); + assertEquals(typeof filter, "function"); + assertType>>(true); + }); +}); diff --git a/matcher.ts b/matcher.ts index 1686837..5c05323 100644 --- a/matcher.ts +++ b/matcher.ts @@ -29,10 +29,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)) { diff --git a/matcher_test.ts b/matcher_test.ts index bba0076..cb0106a 100644 --- a/matcher_test.ts +++ b/matcher_test.ts @@ -1,46 +1,87 @@ 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 { + 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", () => { + const matcher = defineMatcher(async function* (_denops, params) { + // @ts-expect-error: `params` is not type restrained + const _: MatchParams<{ a: string }> = params; + yield* []; + }); + assertEquals(typeof matcher.match, "function"); + 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", () => { + const matcher = defineMatcher<{ a: string }>( + async function* (_denops, params) { + const _: MatchParams<{ a: string }> = params; + yield* []; + }, + ); + assertEquals(typeof matcher.match, "function"); + 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("compose matchers 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); + assertType>>(true); + 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", + ]); }); - const matcher3 = defineMatcher(async function* (_denops, { items }) { - results.push("matcher3"); - yield* items.filter((item) => item.value.includes("3")); + + await t.step("properly triggers type constraint", () => { + const matcher1 = defineMatcher<{ a: string }>(async function* () {}); + const matcher2 = defineMatcher<{ b: string }>(async function* () {}); + const matcher3 = defineMatcher<{ c: string }>(async function* () {}); + // @ts-expect-error: `matcher2` is not assignable to `Matcher<{ a: string }>` + composeMatchers(matcher1, matcher2); + // @ts-expect-error: `matcher3` is not assignable to `Matcher<{ a: string }>` + composeMatchers(matcher1, 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..c52db9a 100644 --- a/mod.ts +++ b/mod.ts @@ -2,10 +2,11 @@ export * from "./action.ts"; export * from "./config.ts"; export * from "./coordinator.ts"; export * from "./curator.ts"; +export * from "./filter.ts"; export * from "./item.ts"; export * from "./matcher.ts"; +export * from "./modifier.ts"; export * from "./previewer.ts"; -export * from "./projector.ts"; export * from "./renderer.ts"; export * from "./sorter.ts"; export * from "./source.ts"; diff --git a/modifier.ts b/modifier.ts new file mode 100644 index 0000000..12e79dd --- /dev/null +++ b/modifier.ts @@ -0,0 +1,51 @@ +import type { Denops } from "@denops/std"; +import type { IdItem } from "@vim-fall/core/item"; + +import type { FlatType } from "./util/_typeutil.ts"; +import { defineSource, type Source } from "./source.ts"; +import { type Curator, defineCurator } from "./curator.ts"; + +declare const UNSPECIFIED: unique symbol; +type UNSPECIFIED = typeof UNSPECIFIED; + +export type ModifyParams = { + readonly items: AsyncIterable>; +}; + +export type Modifier = + & (< + S extends Source | Curator, + V extends S extends (Source | Curator) ? V : never, + W extends U extends UNSPECIFIED ? V : U, + R extends S extends Source ? Source> + : Curator>, + >(source: S) => R) + & { + __phantom?: T; + }; + +export function defineModifier( + modify: ( + denops: Denops, + params: ModifyParams, + options: { signal?: AbortSignal }, + ) => AsyncIterableIterator>, +): Modifier { + return ((source) => { + if ("collect" in source) { + return defineSource( + (denops, params, options) => { + const items = source.collect(denops, params, options); + return modify(denops, { items }, options); + }, + ); + } else { + return defineCurator( + (denops, params, options) => { + const items = source.curate(denops, params, options); + return modify(denops, { items }, options); + }, + ); + } + }) as Modifier; +} diff --git a/modifier_test.ts b/modifier_test.ts new file mode 100644 index 0000000..1666f22 --- /dev/null +++ b/modifier_test.ts @@ -0,0 +1,258 @@ +import { assertEquals } from "@std/assert"; +import { assertType, type IsExact } from "@std/testing/types"; + +import { + defineModifier, + type Modifier, + type ModifyParams, +} from "./modifier.ts"; +import type { Source } from "./source.ts"; +import type { Curator } from "./curator.ts"; + +Deno.test("Modifier", async (t) => { + await t.step("with Source", async (t) => { + await t.step("without type constraint U", async (t) => { + const modifier = (() => {}) as Modifier<{ a: string }>; + + await t.step("passed type is equal to the type restriction", () => { + const modified = modifier({} as Source<{ a: string }>); + assertType< + IsExact< + typeof modified, + Source<{ a: string }> + > + >(true); + }); + + await t.step("passed type establishes the type restriction", () => { + const modified = modifier({} as Source<{ a: string; b: string }>); + assertType< + IsExact< + typeof modified, + Source<{ a: string; b: string }> + > + >(true); + }); + + await t.step( + "passed type does not establish the type restriction", + () => { + // @ts-expect-error: 'a' is missing + modifier({} as Source<{ b: string }>); + }, + ); + + await t.step( + "check if the type constraint correctly triggers the type checking", + () => { + const modifier1 = (() => {}) as Modifier<{ a: string }>; + const modifier2 = (() => {}) as Modifier<{ b: string }>; + const modifier3 = (() => {}) as Modifier<{ c: string }>; + function strictFunction(_: Modifier) {} + strictFunction(modifier1); + // @ts-expect-error: 'a' is missing + strictFunction(modifier2); + // @ts-expect-error: 'a' is missing + strictFunction(modifier3); + }, + ); + }); + + await t.step("with type constraint U", async (t) => { + const modifier = (() => {}) as Modifier<{ a: string }, { z: string }>; + + await t.step("passed type is equal to the type restriction", () => { + const modified = modifier({} as Source<{ a: string }>); + assertType< + IsExact< + typeof modified, + Source<{ a: string; z: string }> + > + >(true); + }); + + await t.step("passed type establishes the type restriction", () => { + const modified = modifier({} as Source<{ a: string; b: string }>); + assertType< + IsExact< + typeof modified, + Source<{ a: string; b: string; z: string }> + > + >(true); + }); + + await t.step( + "passed type does not establish the type restriction", + () => { + // @ts-expect-error: 'a' is missing + modifier({} as Source<{ b: string }>); + }, + ); + + await t.step( + "check if the type constraint correctly triggers the type checking", + () => { + const modifier1 = (() => {}) as Modifier< + { a: string }, + { z: string } + >; + const modifier2 = (() => {}) as Modifier< + { b: string }, + { z: string } + >; + const modifier3 = (() => {}) as Modifier< + { c: string }, + { z: string } + >; + function strictFunction(_: Modifier) {} + strictFunction(modifier1); + // @ts-expect-error: 'a' is missing + strictFunction(modifier2); + // @ts-expect-error: 'a' is missing + strictFunction(modifier3); + }, + ); + }); + }); + + await t.step("with Curator", async (t) => { + await t.step("without type constraint U", async (t) => { + const modifier = (() => {}) as Modifier<{ a: string }>; + + await t.step("passed type is equal to the type restriction", () => { + const modified = modifier({} as Curator<{ a: string }>); + assertType< + IsExact< + typeof modified, + Curator<{ a: string }> + > + >(true); + }); + + await t.step("passed type establishes the type restriction", () => { + const modified = modifier({} as Curator<{ a: string; b: string }>); + assertType< + IsExact< + typeof modified, + Curator<{ a: string; b: string }> + > + >(true); + }); + + await t.step( + "passed type does not establish the type restriction", + () => { + // @ts-expect-error: 'a' is missing + modifier({} as Curator<{ b: string }>); + }, + ); + + await t.step( + "check if the type constraint correctly triggers the type checking", + () => { + const modifier1 = (() => {}) as Modifier<{ a: string }>; + const modifier2 = (() => {}) as Modifier<{ b: string }>; + const modifier3 = (() => {}) as Modifier<{ c: string }>; + function strictFunction(_: Modifier) {} + strictFunction(modifier1); + // @ts-expect-error: 'a' is missing + strictFunction(modifier2); + // @ts-expect-error: 'a' is missing + strictFunction(modifier3); + }, + ); + }); + + await t.step("with type constraint U", async (t) => { + const modifier = (() => {}) as Modifier<{ a: string }, { z: string }>; + + await t.step("passed type is equal to the type restriction", () => { + const modified = modifier({} as Curator<{ a: string }>); + assertType< + IsExact< + typeof modified, + Curator<{ a: string; z: string }> + > + >(true); + }); + + await t.step("passed type establishes the type restriction", () => { + const modified = modifier({} as Curator<{ a: string; b: string }>); + assertType< + IsExact< + typeof modified, + Curator<{ a: string; b: string; z: string }> + > + >(true); + }); + + await t.step( + "passed type does not establish the type restriction", + () => { + // @ts-expect-error: 'a' is missing + modifier({} as Curator<{ b: string }>); + }, + ); + + await t.step( + "check if the type constraint correctly triggers the type checking", + () => { + const modifier1 = (() => {}) as Modifier< + { a: string }, + { z: string } + >; + const modifier2 = (() => {}) as Modifier< + { b: string }, + { z: string } + >; + const modifier3 = (() => {}) as Modifier< + { c: string }, + { z: string } + >; + function strictFunction(_: Modifier) {} + strictFunction(modifier1); + // @ts-expect-error: 'a' is missing + strictFunction(modifier2); + // @ts-expect-error: 'a' is missing + strictFunction(modifier3); + }, + ); + }); + }); +}); + +Deno.test("defineModifier", async (t) => { + await t.step("without type constraint", () => { + const modifier = defineModifier(async function* (_denops, params) { + // @ts-expect-error: `params` is not type restrained + const _: ModifyParams<{ a: string }> = params; + yield* []; + }); + assertEquals(typeof modifier, "function"); + assertType>>(true); + }); + + await t.step("without type constraint T", () => { + const modifier = defineModifier<{ a: string }>( + async function* (_denops, params) { + const _: ModifyParams<{ a: string }> = params; + yield* []; + }, + ); + assertEquals(typeof modifier, "function"); + assertType>>(true); + }); + + await t.step("without type constraint T and U", () => { + const modifier = defineModifier<{ a: string }, { z: string }>( + async function* (_denops, params) { + const _: ModifyParams<{ a: string }> = params; + yield* []; + }, + ); + assertEquals(typeof modifier, "function"); + assertType< + IsExact> + >(true); + }); +}); diff --git a/projector.ts b/projector.ts deleted file mode 100644 index 0ac79eb..0000000 --- a/projector.ts +++ /dev/null @@ -1,140 +0,0 @@ -import type { Denops } from "@denops/std"; -import type { IdItem } from "@vim-fall/core/item"; - -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"; - -/** - * Parameters for projecting items. - */ -export type ProjectParams = { - /** - * Async iterable of items to project. - */ - readonly items: AsyncIterable>; -}; - -/** - * Projector that processes items from the source or curator. - * - * Generally, the projector has the following two roles: - * - * 1. Filter items from the source or curator based on criteria other than user input (Filter) - * 2. Transform items from the source or curator to adapt the item type for further processing (Modifier) - * - * In built-in extensions, projectors are categorized into the two types above. - */ -export type Projector = { - /** - * Projects items for further processing. - * - * @param denops - The Denops instance. - * @param params - Parameters specifying the items to project. - * @param options - Additional options, including an abort signal. - * @returns An async iterator over the projected `IdItem` elements of type `U`. - */ - project( - denops: Denops, - params: ProjectParams, - options: { signal?: AbortSignal }, - ): AsyncIterableIterator>; -}; - -/** - * 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; - } -} 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, - }, - }, - }, - }, - ]); - }); -}); From 7d4318773fcc82221f90525cafd2b690231e08cb Mon Sep 17 00:00:00 2001 From: Alisue Date: Mon, 11 Nov 2024 00:22:01 +0900 Subject: [PATCH 06/13] chore: remove unnecessary dependencies --- deno.jsonc | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/deno.jsonc b/deno.jsonc index b10d0e0..7562a4a 100644 --- a/deno.jsonc +++ b/deno.jsonc @@ -124,8 +124,6 @@ "@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.4", - "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" + "fzf": "npm:fzf@^0.5.2" } } From faae5a68d8c27df58f9f2d5d9c84404778f1fe5c Mon Sep 17 00:00:00 2001 From: Alisue Date: Mon, 11 Nov 2024 00:22:21 +0900 Subject: [PATCH 07/13] deps: update @vim-fall/core --- deno.jsonc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/deno.jsonc b/deno.jsonc index 7562a4a..7fc0c4c 100644 --- a/deno.jsonc +++ b/deno.jsonc @@ -123,7 +123,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.4", + "@vim-fall/core": "jsr:@vim-fall/core@^0.1.5-pre.1", "fzf": "npm:fzf@^0.5.2" } } From 58a40cef00f6c6693c8eb53c976827013d4f6fed Mon Sep 17 00:00:00 2001 From: Alisue Date: Mon, 11 Nov 2024 00:24:00 +0900 Subject: [PATCH 08/13] feat!: remove and unite filter, modifier into refiner --- builtin/filter/cwd.ts | 33 - builtin/filter/exists.ts | 31 - builtin/filter/noop.ts | 13 - builtin/modifier/mod.ts | 2 - builtin/modifier/relative_path.ts | 56 -- builtin/refiner/cwd.ts | 30 + builtin/refiner/exists.ts | 28 + builtin/{filter => refiner}/mod.ts | 1 + builtin/refiner/noop.ts | 13 + builtin/{filter => refiner}/regexp.ts | 27 +- builtin/refiner/relative_path.ts | 48 ++ deno.jsonc | 16 +- filter.ts | 39 -- filter_test.ts | 125 ---- mod.ts | 3 +- modifier.ts | 51 -- modifier_test.ts | 258 -------- refiner.ts | 119 ++++ refiner_test.ts | 859 ++++++++++++++++++++++++++ util/derivable.ts | 2 +- 20 files changed, 1117 insertions(+), 637 deletions(-) delete mode 100644 builtin/filter/cwd.ts delete mode 100644 builtin/filter/exists.ts delete mode 100644 builtin/filter/noop.ts delete mode 100644 builtin/modifier/mod.ts delete mode 100644 builtin/modifier/relative_path.ts create mode 100644 builtin/refiner/cwd.ts create mode 100644 builtin/refiner/exists.ts rename builtin/{filter => refiner}/mod.ts (80%) create mode 100644 builtin/refiner/noop.ts rename builtin/{filter => refiner}/regexp.ts (65%) create mode 100644 builtin/refiner/relative_path.ts delete mode 100644 filter.ts delete mode 100644 filter_test.ts delete mode 100644 modifier.ts delete mode 100644 modifier_test.ts create mode 100644 refiner.ts create mode 100644 refiner_test.ts diff --git a/builtin/filter/cwd.ts b/builtin/filter/cwd.ts deleted file mode 100644 index 1655423..0000000 --- a/builtin/filter/cwd.ts +++ /dev/null @@ -1,33 +0,0 @@ -import * as fn from "@denops/std/function"; - -import { defineFilter, type Filter } from "../../filter.ts"; - -/** - * Represents detailed information for each item, specifically the file path. - */ -type Detail = { - path: string; -}; - -/** - * Creates a Filter that filters items based on the current working directory. - * - * This Filter yields only those items whose `path` is within the current working directory. - * - * @returns A Filter that filters items according to the current working directory. - */ -export function cwd(): Filter { - return defineFilter(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 0978f51..0000000 --- a/builtin/filter/exists.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { exists as exists_ } from "@std/fs/exists"; - -import { defineFilter, type Filter } from "../../filter.ts"; - -/** - * Represents detailed information for each item, specifically the file path. - */ -type Detail = { - path: string; -}; - -/** - * Creates a Filter that filters items based on file existence. - * - * This Filter checks each item's `path` and yields only those items - * where the path exists in the filesystem. - * - * @returns A Filter that filters items according to file existence. - */ -export function exists(): Filter { - return defineFilter(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 4aa3578..0000000 --- a/builtin/filter/noop.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { defineFilter, type Filter } from "../../filter.ts"; - -/** - * A no-operation (noop) Filter. - * - * This Filter does nothing and yields no items. It can be used as a placeholder - * or a default value where a Filter is required but no action is needed. - * - * @returns A Filter that yields nothing. - */ -export function noop(): Filter { - return defineFilter(async function* () {}); -} diff --git a/builtin/modifier/mod.ts b/builtin/modifier/mod.ts deleted file mode 100644 index 52c6faa..0000000 --- a/builtin/modifier/mod.ts +++ /dev/null @@ -1,2 +0,0 @@ -// This file is generated by gen-mod.ts -export * from "./relative_path.ts"; diff --git a/builtin/modifier/relative_path.ts b/builtin/modifier/relative_path.ts deleted file mode 100644 index ad93d55..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 { defineModifier, type Modifier } from "../../modifier.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, ->(): Modifier { - return defineModifier(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/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 65% rename from builtin/filter/regexp.ts rename to builtin/refiner/regexp.ts index 60ac6ef..ab3ec61 100644 --- a/builtin/filter/regexp.ts +++ b/builtin/refiner/regexp.ts @@ -1,4 +1,4 @@ -import { defineFilter, type Filter } from "../../filter.ts"; +import { defineRefiner, type Refiner } from "../../refiner.ts"; /** * Options for filtering items by regular expressions. @@ -8,7 +8,7 @@ import { defineFilter, type Filter } from "../../filter.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 Filter that filters items based on regular expression patterns. + * Creates a Refiner that filters items based on regular expression patterns. * - * The `regexp` Filter 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 Filter 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, -): Filter { - return defineFilter(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/deno.jsonc b/deno.jsonc index 7fc0c4c..f303c67 100644 --- a/deno.jsonc +++ b/deno.jsonc @@ -26,23 +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/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", @@ -69,11 +68,10 @@ "./config": "./config.ts", "./coordinator": "./coordinator.ts", "./curator": "./curator.ts", - "./filter": "./filter.ts", "./item": "./item.ts", "./matcher": "./matcher.ts", - "./modifier": "./modifier.ts", "./previewer": "./previewer.ts", + "./refiner": "./refiner.ts", "./renderer": "./renderer.ts", "./sorter": "./sorter.ts", "./source": "./source.ts", diff --git a/filter.ts b/filter.ts deleted file mode 100644 index 085a147..0000000 --- a/filter.ts +++ /dev/null @@ -1,39 +0,0 @@ -import type { Denops } from "@denops/std"; -import type { IdItem } from "@vim-fall/core/item"; - -import { defineSource, type Source } from "./source.ts"; -import { type Curator, defineCurator } from "./curator.ts"; - -export type FilterParams = { - readonly items: AsyncIterable>; -}; - -export type Filter = ( | Curator>(source: S) => S) & { - __phantom?: T; -}; - -export function defineFilter( - modify: ( - denops: Denops, - params: FilterParams, - options: { signal?: AbortSignal }, - ) => AsyncIterableIterator>, -): Filter { - return ((source) => { - if ("collect" in source) { - return defineSource( - (denops, params, options) => { - const items = source.collect(denops, params, options); - return modify(denops, { items }, options); - }, - ); - } else { - return defineCurator( - (denops, params, options) => { - const items = source.curate(denops, params, options); - return modify(denops, { items }, options); - }, - ); - } - }) as Filter; -} diff --git a/filter_test.ts b/filter_test.ts deleted file mode 100644 index ec1f7a5..0000000 --- a/filter_test.ts +++ /dev/null @@ -1,125 +0,0 @@ -import { assertEquals } from "@std/assert"; -import { assertType, type IsExact } from "@std/testing/types"; - -import { defineFilter, type Filter, type FilterParams } from "./filter.ts"; -import type { Source } from "./source.ts"; -import type { Curator } from "./curator.ts"; - -Deno.test("Filter", async (t) => { - await t.step("with Source", async (t) => { - const filter = (() => {}) as Filter<{ a: string }>; - - await t.step("passed type is equal to the type restriction", () => { - const modified = filter({} as Source<{ a: string }>); - assertType< - IsExact< - typeof modified, - Source<{ a: string }> - > - >(true); - }); - - await t.step("passed type establishes the type restriction", () => { - const modified = filter({} as Source<{ a: string; b: string }>); - assertType< - IsExact< - typeof modified, - Source<{ a: string; b: string }> - > - >(true); - }); - - await t.step( - "passed type does not establish the type restriction", - () => { - // @ts-expect-error: 'a' is missing - filter({} as Source<{ b: string }>); - }, - ); - - await t.step( - "check if the type constraint correctly triggers the type checking", - () => { - const filter1 = (() => {}) as Filter<{ a: string }>; - const filter2 = (() => {}) as Filter<{ b: string }>; - const filter3 = (() => {}) as Filter<{ c: string }>; - function strictFunction(_: Filter) {} - strictFunction(filter1); - // @ts-expect-error: 'a' is missing - strictFunction(filter2); - // @ts-expect-error: 'a' is missing - strictFunction(filter3); - }, - ); - }); - - await t.step("with Curator", async (t) => { - const filter = (() => {}) as Filter<{ a: string }>; - - await t.step("passed type is equal to the type restriction", () => { - const modified = filter({} as Curator<{ a: string }>); - assertType< - IsExact< - typeof modified, - Curator<{ a: string }> - > - >(true); - }); - - await t.step("passed type establishes the type restriction", () => { - const modified = filter({} as Curator<{ a: string; b: string }>); - assertType< - IsExact< - typeof modified, - Curator<{ a: string; b: string }> - > - >(true); - }); - - await t.step( - "passed type does not establish the type restriction", - () => { - // @ts-expect-error: 'a' is missing - filter({} as Curator<{ b: string }>); - }, - ); - - await t.step( - "check if the type constraint correctly triggers the type checking", - () => { - const filter1 = (() => {}) as Filter<{ a: string }>; - const filter2 = (() => {}) as Filter<{ b: string }>; - const filter3 = (() => {}) as Filter<{ c: string }>; - function strictFunction(_: Filter) {} - strictFunction(filter1); - // @ts-expect-error: 'a' is missing - strictFunction(filter2); - // @ts-expect-error: 'a' is missing - strictFunction(filter3); - }, - ); - }); -}); - -Deno.test("defineFilter", async (t) => { - await t.step("without type constraint", () => { - const filter = defineFilter(async function* (_denops, params) { - // @ts-expect-error: `params` is not type restrained - const _: FilterParams<{ a: string }> = params; - yield* []; - }); - assertEquals(typeof filter, "function"); - assertType>>(true); - }); - - await t.step("without type constraint T", () => { - const filter = defineFilter<{ a: string }>( - async function* (_denops, params) { - const _: FilterParams<{ a: string }> = params; - yield* []; - }, - ); - assertEquals(typeof filter, "function"); - assertType>>(true); - }); -}); diff --git a/mod.ts b/mod.ts index c52db9a..4c117bd 100644 --- a/mod.ts +++ b/mod.ts @@ -2,11 +2,10 @@ export * from "./action.ts"; export * from "./config.ts"; export * from "./coordinator.ts"; export * from "./curator.ts"; -export * from "./filter.ts"; export * from "./item.ts"; export * from "./matcher.ts"; -export * from "./modifier.ts"; export * from "./previewer.ts"; +export * from "./refiner.ts"; export * from "./renderer.ts"; export * from "./sorter.ts"; export * from "./source.ts"; diff --git a/modifier.ts b/modifier.ts deleted file mode 100644 index 12e79dd..0000000 --- a/modifier.ts +++ /dev/null @@ -1,51 +0,0 @@ -import type { Denops } from "@denops/std"; -import type { IdItem } from "@vim-fall/core/item"; - -import type { FlatType } from "./util/_typeutil.ts"; -import { defineSource, type Source } from "./source.ts"; -import { type Curator, defineCurator } from "./curator.ts"; - -declare const UNSPECIFIED: unique symbol; -type UNSPECIFIED = typeof UNSPECIFIED; - -export type ModifyParams = { - readonly items: AsyncIterable>; -}; - -export type Modifier = - & (< - S extends Source | Curator, - V extends S extends (Source | Curator) ? V : never, - W extends U extends UNSPECIFIED ? V : U, - R extends S extends Source ? Source> - : Curator>, - >(source: S) => R) - & { - __phantom?: T; - }; - -export function defineModifier( - modify: ( - denops: Denops, - params: ModifyParams, - options: { signal?: AbortSignal }, - ) => AsyncIterableIterator>, -): Modifier { - return ((source) => { - if ("collect" in source) { - return defineSource( - (denops, params, options) => { - const items = source.collect(denops, params, options); - return modify(denops, { items }, options); - }, - ); - } else { - return defineCurator( - (denops, params, options) => { - const items = source.curate(denops, params, options); - return modify(denops, { items }, options); - }, - ); - } - }) as Modifier; -} diff --git a/modifier_test.ts b/modifier_test.ts deleted file mode 100644 index 1666f22..0000000 --- a/modifier_test.ts +++ /dev/null @@ -1,258 +0,0 @@ -import { assertEquals } from "@std/assert"; -import { assertType, type IsExact } from "@std/testing/types"; - -import { - defineModifier, - type Modifier, - type ModifyParams, -} from "./modifier.ts"; -import type { Source } from "./source.ts"; -import type { Curator } from "./curator.ts"; - -Deno.test("Modifier", async (t) => { - await t.step("with Source", async (t) => { - await t.step("without type constraint U", async (t) => { - const modifier = (() => {}) as Modifier<{ a: string }>; - - await t.step("passed type is equal to the type restriction", () => { - const modified = modifier({} as Source<{ a: string }>); - assertType< - IsExact< - typeof modified, - Source<{ a: string }> - > - >(true); - }); - - await t.step("passed type establishes the type restriction", () => { - const modified = modifier({} as Source<{ a: string; b: string }>); - assertType< - IsExact< - typeof modified, - Source<{ a: string; b: string }> - > - >(true); - }); - - await t.step( - "passed type does not establish the type restriction", - () => { - // @ts-expect-error: 'a' is missing - modifier({} as Source<{ b: string }>); - }, - ); - - await t.step( - "check if the type constraint correctly triggers the type checking", - () => { - const modifier1 = (() => {}) as Modifier<{ a: string }>; - const modifier2 = (() => {}) as Modifier<{ b: string }>; - const modifier3 = (() => {}) as Modifier<{ c: string }>; - function strictFunction(_: Modifier) {} - strictFunction(modifier1); - // @ts-expect-error: 'a' is missing - strictFunction(modifier2); - // @ts-expect-error: 'a' is missing - strictFunction(modifier3); - }, - ); - }); - - await t.step("with type constraint U", async (t) => { - const modifier = (() => {}) as Modifier<{ a: string }, { z: string }>; - - await t.step("passed type is equal to the type restriction", () => { - const modified = modifier({} as Source<{ a: string }>); - assertType< - IsExact< - typeof modified, - Source<{ a: string; z: string }> - > - >(true); - }); - - await t.step("passed type establishes the type restriction", () => { - const modified = modifier({} as Source<{ a: string; b: string }>); - assertType< - IsExact< - typeof modified, - Source<{ a: string; b: string; z: string }> - > - >(true); - }); - - await t.step( - "passed type does not establish the type restriction", - () => { - // @ts-expect-error: 'a' is missing - modifier({} as Source<{ b: string }>); - }, - ); - - await t.step( - "check if the type constraint correctly triggers the type checking", - () => { - const modifier1 = (() => {}) as Modifier< - { a: string }, - { z: string } - >; - const modifier2 = (() => {}) as Modifier< - { b: string }, - { z: string } - >; - const modifier3 = (() => {}) as Modifier< - { c: string }, - { z: string } - >; - function strictFunction(_: Modifier) {} - strictFunction(modifier1); - // @ts-expect-error: 'a' is missing - strictFunction(modifier2); - // @ts-expect-error: 'a' is missing - strictFunction(modifier3); - }, - ); - }); - }); - - await t.step("with Curator", async (t) => { - await t.step("without type constraint U", async (t) => { - const modifier = (() => {}) as Modifier<{ a: string }>; - - await t.step("passed type is equal to the type restriction", () => { - const modified = modifier({} as Curator<{ a: string }>); - assertType< - IsExact< - typeof modified, - Curator<{ a: string }> - > - >(true); - }); - - await t.step("passed type establishes the type restriction", () => { - const modified = modifier({} as Curator<{ a: string; b: string }>); - assertType< - IsExact< - typeof modified, - Curator<{ a: string; b: string }> - > - >(true); - }); - - await t.step( - "passed type does not establish the type restriction", - () => { - // @ts-expect-error: 'a' is missing - modifier({} as Curator<{ b: string }>); - }, - ); - - await t.step( - "check if the type constraint correctly triggers the type checking", - () => { - const modifier1 = (() => {}) as Modifier<{ a: string }>; - const modifier2 = (() => {}) as Modifier<{ b: string }>; - const modifier3 = (() => {}) as Modifier<{ c: string }>; - function strictFunction(_: Modifier) {} - strictFunction(modifier1); - // @ts-expect-error: 'a' is missing - strictFunction(modifier2); - // @ts-expect-error: 'a' is missing - strictFunction(modifier3); - }, - ); - }); - - await t.step("with type constraint U", async (t) => { - const modifier = (() => {}) as Modifier<{ a: string }, { z: string }>; - - await t.step("passed type is equal to the type restriction", () => { - const modified = modifier({} as Curator<{ a: string }>); - assertType< - IsExact< - typeof modified, - Curator<{ a: string; z: string }> - > - >(true); - }); - - await t.step("passed type establishes the type restriction", () => { - const modified = modifier({} as Curator<{ a: string; b: string }>); - assertType< - IsExact< - typeof modified, - Curator<{ a: string; b: string; z: string }> - > - >(true); - }); - - await t.step( - "passed type does not establish the type restriction", - () => { - // @ts-expect-error: 'a' is missing - modifier({} as Curator<{ b: string }>); - }, - ); - - await t.step( - "check if the type constraint correctly triggers the type checking", - () => { - const modifier1 = (() => {}) as Modifier< - { a: string }, - { z: string } - >; - const modifier2 = (() => {}) as Modifier< - { b: string }, - { z: string } - >; - const modifier3 = (() => {}) as Modifier< - { c: string }, - { z: string } - >; - function strictFunction(_: Modifier) {} - strictFunction(modifier1); - // @ts-expect-error: 'a' is missing - strictFunction(modifier2); - // @ts-expect-error: 'a' is missing - strictFunction(modifier3); - }, - ); - }); - }); -}); - -Deno.test("defineModifier", async (t) => { - await t.step("without type constraint", () => { - const modifier = defineModifier(async function* (_denops, params) { - // @ts-expect-error: `params` is not type restrained - const _: ModifyParams<{ a: string }> = params; - yield* []; - }); - assertEquals(typeof modifier, "function"); - assertType>>(true); - }); - - await t.step("without type constraint T", () => { - const modifier = defineModifier<{ a: string }>( - async function* (_denops, params) { - const _: ModifyParams<{ a: string }> = params; - yield* []; - }, - ); - assertEquals(typeof modifier, "function"); - assertType>>(true); - }); - - await t.step("without type constraint T and U", () => { - const modifier = defineModifier<{ a: string }, { z: string }>( - async function* (_denops, params) { - const _: ModifyParams<{ a: string }> = params; - yield* []; - }, - ); - assertEquals(typeof modifier, "function"); - assertType< - IsExact> - >(true); - }); -}); 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/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; From b82ac92466159a0d9299b0c4a26d71cf27cfe2f0 Mon Sep 17 00:00:00 2001 From: Alisue Date: Mon, 11 Nov 2024 00:27:47 +0900 Subject: [PATCH 09/13] feat: follow `IdItem` changes --- action.ts | 18 ++- action_test.ts | 115 ++++++++++++++---- builtin/action/buffer.ts | 21 ---- builtin/action/cmd.ts | 10 +- builtin/action/echo.ts | 4 +- builtin/action/noop.ts | 4 +- builtin/action/open.ts | 76 ++++++------ builtin/action/quickfix.ts | 38 +++--- builtin/action/submatch.ts | 48 ++++---- builtin/action/systemopen.ts | 18 +-- builtin/action/yank.ts | 4 +- builtin/coordinator/compact.ts | 4 +- builtin/coordinator/modern.ts | 4 +- builtin/coordinator/separate.ts | 4 +- builtin/curator/git_grep.ts | 9 +- builtin/curator/grep.ts | 6 +- builtin/curator/noop.ts | 5 +- builtin/curator/rg.ts | 9 +- builtin/matcher/fzf.ts | 4 +- builtin/matcher/noop.ts | 4 +- builtin/matcher/regexp.ts | 2 +- builtin/matcher/substring.ts | 6 +- builtin/mod.ts | 3 +- builtin/previewer/buffer.ts | 7 +- builtin/previewer/file.ts | 5 +- builtin/previewer/helptag.ts | 5 +- builtin/previewer/noop.ts | 2 +- builtin/renderer/helptag.ts | 11 +- builtin/renderer/nerdfont.ts | 7 +- builtin/renderer/noop.ts | 4 +- builtin/renderer/smart_path.ts | 4 +- builtin/sorter/lexical.ts | 8 +- builtin/sorter/noop.ts | 4 +- builtin/sorter/numerical.ts | 8 +- builtin/source/buffer.ts | 26 ++-- builtin/source/file.ts | 22 ++-- builtin/source/helptag.ts | 8 +- builtin/source/history.ts | 35 +++--- builtin/source/line.ts | 21 ++-- builtin/source/list.ts | 4 +- builtin/source/noop.ts | 3 +- builtin/source/oldfiles.ts | 2 +- config.ts | 45 ++++--- curator.ts | 46 ++++++- curator_test.ts | 129 +++++++++++++++++++- matcher.ts | 10 +- matcher_test.ts | 209 +++++++++++++++++++++++--------- previewer.ts | 15 ++- previewer_test.ts | 151 ++++++++++++++++++----- renderer.ts | 14 +-- renderer_test.ts | 184 +++++++++++++++++++++------- sorter.ts | 14 +-- sorter_test.ts | 122 ++++++++++++------- source.ts | 14 +-- source_test.ts | 177 ++++++++++++++++++--------- 55 files changed, 1158 insertions(+), 574 deletions(-) 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 28e2794..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/matcher/fzf.ts b/builtin/matcher/fzf.ts index d94ad0c..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; 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/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/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/matcher.ts b/matcher.ts index 5c05323..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,7 +12,7 @@ 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( +export function defineMatcher( match: ( denops: Denops, params: MatchParams, @@ -29,7 +31,7 @@ export function defineMatcher( * @param matchers - The matchers to compose. * @returns A matcher that applies all composed matchers in sequence. */ -export function composeMatchers( +export function composeMatchers( ...matchers: DerivableArray<[Matcher, ...Matcher>[]]> ): Matcher { return { @@ -43,5 +45,3 @@ export function composeMatchers( }, }; } - -export type * from "@vim-fall/core/matcher"; diff --git a/matcher_test.ts b/matcher_test.ts index cb0106a..c83bef1 100644 --- a/matcher_test.ts +++ b/matcher_test.ts @@ -1,6 +1,7 @@ import { assertEquals } from "@std/assert"; import { assertType, type IsExact } from "@std/testing/types"; import { DenopsStub } from "@denops/test/stub"; +import type { DetailUnit } from "./item.ts"; import { composeMatchers, defineMatcher, @@ -10,78 +11,174 @@ import { Deno.test("defineMatcher", async (t) => { await t.step("without type constraint", () => { + type C = { a: string }; const matcher = defineMatcher(async function* (_denops, params) { - // @ts-expect-error: `params` is not type restrained - const _: MatchParams<{ a: string }> = 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* []; }); - assertEquals(typeof matcher.match, "function"); - assertType>>(true); + assertType>>(true); }); await t.step("with type constraint", () => { - const matcher = defineMatcher<{ a: string }>( + type C = { a: string }; + const matcher = defineMatcher( async function* (_denops, params) { - const _: MatchParams<{ a: string }> = params; + // NOTE: + // `match` method itself has `V` thus we cannot use `AssertTrue` here. + // `params` should establish the type constraint + const _: MatchParams = params; yield* []; }, ); - assertEquals(typeof matcher.match, "function"); assertType>>(true); }); }); Deno.test("composeMatchers", async (t) => { - await t.step("compose matchers 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); - assertType>>(true); - 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("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); + }); }); - await t.step("properly triggers type constraint", () => { - const matcher1 = defineMatcher<{ a: string }>(async function* () {}); - const matcher2 = defineMatcher<{ b: string }>(async function* () {}); - const matcher3 = defineMatcher<{ c: string }>(async function* () {}); - // @ts-expect-error: `matcher2` is not assignable to `Matcher<{ a: string }>` - composeMatchers(matcher1, matcher2); - // @ts-expect-error: `matcher3` is not assignable to `Matcher<{ a: string }>` - composeMatchers(matcher1, matcher3); + 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); + }); }); }); 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/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", - ]); }); From 39a9f47dc8913511c7393a4117d4ce1efce8d380 Mon Sep 17 00:00:00 2001 From: Alisue Date: Mon, 11 Nov 2024 00:51:48 +0900 Subject: [PATCH 10/13] docs: update README --- README.md | 65 ++++++++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 59 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 1c62f67..7613e48 100644 --- a/README.md +++ b/README.md @@ -5,14 +5,67 @@ [![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). +[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. -It is also used to develop extensions of Fall. +## Usage + +### 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! From f39c32f3aab9c531e03338935a50bd3e39186a45 Mon Sep 17 00:00:00 2001 From: Alisue Date: Mon, 11 Nov 2024 00:54:31 +0900 Subject: [PATCH 11/13] chore: remove Deno 1.x from CI --- .github/workflows/jsr.yml | 2 +- .github/workflows/test.yml | 10 ++++------ 2 files changed, 5 insertions(+), 7 deletions(-) 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 From e3fa93c7b93bbf056f347a53d20f9e49167da31f Mon Sep 17 00:00:00 2001 From: Alisue Date: Mon, 11 Nov 2024 01:06:02 +0900 Subject: [PATCH 12/13] docs: add deno badge to show deno version to support --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 7613e48..9b44fc3 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,7 @@ # 🍂 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) From 96cea101093d1c1a8124c138be89ebc4ad492a40 Mon Sep 17 00:00:00 2001 From: Alisue Date: Mon, 11 Nov 2024 00:51:38 +0900 Subject: [PATCH 13/13] deps: update @vim-fall/core to ^0.1.6 --- deno.jsonc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/deno.jsonc b/deno.jsonc index f303c67..1d9df0f 100644 --- a/deno.jsonc +++ b/deno.jsonc @@ -121,7 +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.5-pre.1", + "@vim-fall/core": "jsr:@vim-fall/core@^0.1.6", "fzf": "npm:fzf@^0.5.2" } }