diff --git a/denops/ddc/app.ts b/denops/ddc/app.ts index 5090bd9..ab4cfef 100644 --- a/denops/ddc/app.ts +++ b/denops/ddc/app.ts @@ -367,6 +367,12 @@ export const main: Entrypoint = (denops: Denops) => { await ddc.checkManualCompletion(denops, context, options, event); + if (skip) return; + + // Revoke any pending callbacks from the previous completion cycle before + // starting a new one. + cbContext.revoke(); + await onEvent( denops, loader, @@ -375,10 +381,6 @@ export const main: Entrypoint = (denops: Denops) => { options, ); - if (skip) return; - - cbContext.revoke(); - if (await ddc.checkSkipCompletion(denops, context, options)) { return; } diff --git a/denops/ddc/context.ts b/denops/ddc/context.ts index c656952..b105afa 100644 --- a/denops/ddc/context.ts +++ b/denops/ddc/context.ts @@ -398,6 +398,8 @@ function isNegligible(older: World, newer: World): boolean { export class ContextBuilderImpl implements ContextBuilder { #lastWorld: World = initialWorld(); #custom: Custom = new Custom(); + // Set to true whenever options are mutated; cleared after first validation. + #needsValidation = true; async createContext( denops: Denops, @@ -431,14 +433,17 @@ export class ContextBuilderImpl implements ContextBuilder { const userOptions = await this.#getUserOptions(denops, world, options); - await this.#validate(denops, "options", userOptions, defaultDdcOptions()); - for (const key in userOptions.sourceOptions) { - await this.#validate( - denops, - "sourceOptions", - userOptions.sourceOptions[key], - defaultSourceOptions(), - ); + if (this.#needsValidation) { + this.#needsValidation = false; + await this.#validate(denops, "options", userOptions, defaultDdcOptions()); + for (const key in userOptions.sourceOptions) { + await this.#validate( + denops, + "sourceOptions", + userOptions.sourceOptions[key], + defaultSourceOptions(), + ); + } } if (context.mode === "c") { @@ -520,31 +525,40 @@ export class ContextBuilderImpl implements ContextBuilder { setGlobal(options: Partial) { this.#custom.setGlobal(options); + this.#needsValidation = true; } setFiletype(ft: string, options: Partial) { this.#custom.setFiletype(ft, options); + this.#needsValidation = true; } setBuffer(bufnr: number, options: Partial) { this.#custom.setBuffer(bufnr, options); + this.#needsValidation = true; } setContextGlobal(callback: Callback) { this.#custom.setContextGlobal(callback); + this.#needsValidation = true; } setContextFiletype(callback: Callback, ft: string) { this.#custom.setContextFiletype(callback, ft); + this.#needsValidation = true; } setContextBuffer(callback: Callback, bufnr: number) { this.#custom.setContextBuffer(callback, bufnr); + this.#needsValidation = true; } patchGlobal(options: Partial) { this.#custom.patchGlobal(options); + this.#needsValidation = true; } patchFiletype(ft: string, options: Partial) { this.#custom.patchFiletype(ft, options); + this.#needsValidation = true; } patchBuffer(bufnr: number, options: Partial) { this.#custom.patchBuffer(bufnr, options); + this.#needsValidation = true; } } diff --git a/denops/ddc/ddc.ts b/denops/ddc/ddc.ts index 1affbf5..f468bbf 100644 --- a/denops/ddc/ddc.ts +++ b/denops/ddc/ddc.ts @@ -35,6 +35,9 @@ import { batch } from "@denops/std/batch"; import { assertEquals } from "@std/assert/equals"; +const _encoder = new TextEncoder(); +const _decoder = new TextDecoder(); + type DdcResult = { items: Item[]; completePos: number; @@ -241,7 +244,7 @@ export class Ddc { completeStr.length > o.maxAutoCompleteLength); // Check cache timeout. - const currentTime = new Date().getSeconds(); + const currentTime = Math.floor(Date.now() / 1000); if ( o.cacheTimeout > 0 && this.#prevResults[s.name] && currentTime > this.#prevResults[s.name].time + o.cacheTimeout @@ -342,7 +345,7 @@ export class Ddc { completeStr, lineNr: context.lineNr, isIncomplete, - time: currentTime, + time: Math.floor(Date.now() / 1000), }; } @@ -731,12 +734,12 @@ function formatMenu(prefix: string, menu: string | undefined): string { } function byteposToCharpos(input: string, pos: number): number { - const bytes = (new TextEncoder()).encode(input); - return (new TextDecoder()).decode(bytes.slice(0, pos)).length; + const bytes = _encoder.encode(input); + return _decoder.decode(bytes.slice(0, pos)).length; } function charposToBytepos(input: string, pos: number): number { - return (new TextEncoder()).encode(input.slice(0, pos)).length; + return _encoder.encode(input.slice(0, pos)).length; } Deno.test("byteposToCharpos", () => { diff --git a/denops/ddc/ext.ts b/denops/ddc/ext.ts index ecf355f..9e34f7c 100644 --- a/denops/ddc/ext.ts +++ b/denops/ddc/ext.ts @@ -204,21 +204,18 @@ export async function filterItems( completeStr: string, cdd: Item[], ): Promise { - // Run a list of filters sequentially on the given items array. - async function callFilters( - userFilters: UserFilter[], + type ResolvedFilter = [ + BaseFilter, + FilterOptions, + BaseParams, + ]; + + // Run a list of pre-resolved filters sequentially on the given items array. + async function callResolvedFilters( + resolved: ResolvedFilter[], items: Item[], ): Promise { - for (const userFilter of userFilters) { - const [filter, filterOptions, filterParams] = await getFilter( - denops, - loader, - options, - userFilter, - ); - if (!filter) { - return []; - } + for (const [filter, filterOptions, filterParams] of resolved) { items = await callFilterFilter( filter, denops, @@ -232,10 +229,30 @@ export async function filterItems( items, ); } - return items; } + // Run a list of filters sequentially on the given items array. + async function callFilters( + userFilters: UserFilter[], + items: Item[], + ): Promise { + const resolved: ResolvedFilter[] = []; + for (const userFilter of userFilters) { + const [filter, filterOptions, filterParams] = await getFilter( + denops, + loader, + options, + userFilter, + ); + if (!filter) { + return []; + } + resolved.push([filter, filterOptions, filterParams]); + } + return callResolvedFilters(resolved, items); + } + // Run matchers concurrently when all of them declare parallelSafe = true and // matcherConcurrency > 1. Falls back to sequential execution when any // safety condition is not met or any chunk throws an exception. @@ -250,17 +267,29 @@ export async function filterItems( return callFilters(matcherFilters, items); } - // Check whether every matcher has opted in to parallel execution + // Resolve filters once and reuse across all branches/chunks. const resolvedFilters = await Promise.all( matcherFilters.map((uf) => getFilter(denops, loader, options, uf)), ); + + // Check whether every matcher has opted in to parallel execution const allParallelSafe = resolvedFilters.every( ([filter, filterOptions]) => filter !== undefined && filterOptions.parallelSafe, ); + // If any filter could not be resolved, treat the same as callFilters does: + // return an empty array. + const validResolved = resolvedFilters.filter( + (r): r is [BaseFilter, FilterOptions, BaseParams] => + r[0] !== undefined, + ); + if (validResolved.length !== resolvedFilters.length) { + return []; + } + if (!allParallelSafe) { - return callFilters(matcherFilters, items); + return callResolvedFilters(validResolved, items); } // Split items into (at most concurrency) equal-sized chunks @@ -273,7 +302,7 @@ export async function filterItems( let parallelResults: Item[][] | null = null; try { parallelResults = await Promise.all( - chunks.map((chunk) => callFilters(matcherFilters, chunk)), + chunks.map((chunk) => callResolvedFilters(validResolved, chunk)), ); } catch (_e) { parallelResults = null; @@ -290,7 +319,7 @@ export async function filterItems( } // Fallback: sequential execution - return callFilters(matcherFilters, items); + return callResolvedFilters(validResolved, items); } if (sourceOptions.maxKeywordLength > 0) {