diff --git a/denops/ddc/base/source.ts b/denops/ddc/base/source.ts index cce54fa..25dd596 100644 --- a/denops/ddc/base/source.ts +++ b/denops/ddc/base/source.ts @@ -10,7 +10,7 @@ import type { Previewer, SourceOptions, } from "../types.ts"; -import { convertKeywordPattern } from "../utils.ts"; +import { convertKeywordPattern, getKeywordRegExp } from "../utils.ts"; import type { Denops } from "@denops/std"; @@ -106,7 +106,7 @@ export abstract class BaseSource< ); const completePos = args.context.input.search( - new RegExp("(?:" + keywordPattern + ")$"), + getKeywordRegExp("(?:" + keywordPattern + ")$"), ); return completePos; } diff --git a/denops/ddc/ddc.ts b/denops/ddc/ddc.ts index f468bbf..e3086e6 100644 --- a/denops/ddc/ddc.ts +++ b/denops/ddc/ddc.ts @@ -31,7 +31,7 @@ import type { Denops } from "@denops/std"; import * as autocmd from "@denops/std/autocmd"; import * as op from "@denops/std/option"; import * as fn from "@denops/std/function"; -import { batch } from "@denops/std/batch"; +import { batch, collect } from "@denops/std/batch"; import { assertEquals } from "@std/assert/equals"; @@ -622,9 +622,12 @@ export class Ddc { return; } - const input = denops.call("ddc#util#get_input", context.event); - const mode = fn.mode(denops); - if (context.input !== await input || context.mode !== await mode) { + const [currentInput, currentMode] = await collect(denops, (denops) => [ + // ddc#util#get_input always returns a string; cast for type inference. + denops.call("ddc#util#get_input", context.event) as Promise, + fn.mode(denops), + ]); + if (context.input !== currentInput || context.mode !== currentMode) { // Input is changed. Skip invalid completion. await this.hide(denops, context, options); return; diff --git a/denops/ddc/ext.ts b/denops/ddc/ext.ts index 9e34f7c..9ee6f73 100644 --- a/denops/ddc/ext.ts +++ b/denops/ddc/ext.ts @@ -237,14 +237,11 @@ export async function filterItems( userFilters: UserFilter[], items: Item[], ): Promise { + const resolvedList = await Promise.all( + userFilters.map((uf) => getFilter(denops, loader, options, uf)), + ); const resolved: ResolvedFilter[] = []; - for (const userFilter of userFilters) { - const [filter, filterOptions, filterParams] = await getFilter( - denops, - loader, - options, - userFilter, - ); + for (const [filter, filterOptions, filterParams] of resolvedList) { if (!filter) { return []; } diff --git a/denops/ddc/utils.ts b/denops/ddc/utils.ts index be266dc..1c49ddd 100644 --- a/denops/ddc/utils.ts +++ b/denops/ddc/utils.ts @@ -15,6 +15,12 @@ import { fromFileUrl } from "@std/path/from-file-url"; import { join } from "@std/path/join"; import { dirname } from "@std/path/dirname"; +// Cache size limit: in practice only a handful of distinct keywordPattern / +// iskeyword combinations appear, so 64 entries is more than enough. +const KEYWORD_CACHE_MAX = 64; +const convertKeywordPatternCache = new Map(); +const keywordRegExpCache = new Map(); + export async function convertKeywordPattern( denops: Denops, keywordPattern: string, @@ -23,13 +29,37 @@ export async function convertKeywordPattern( const iskeyword = bufnr === undefined ? await op.iskeyword.getLocal(denops) : await op.iskeyword.getBuffer(denops, bufnr); + // Neither iskeyword nor keywordPattern contain NUL bytes, so this + // composite key is unambiguous. + const cacheKey = keywordPattern + "\0" + iskeyword; + const cached = convertKeywordPatternCache.get(cacheKey); + if (cached !== undefined) { + return cached; + } const keyword = vimoption2ts(iskeyword); const replaced = keywordPattern .replaceAll("\\k", "[" + keyword + "]") .replaceAll("[:keyword:]", keyword); + if (convertKeywordPatternCache.size >= KEYWORD_CACHE_MAX) { + convertKeywordPatternCache.clear(); + } + convertKeywordPatternCache.set(cacheKey, replaced); return replaced; } +export function getKeywordRegExp(expandedPattern: string): RegExp { + const cached = keywordRegExpCache.get(expandedPattern); + if (cached !== undefined) { + return cached; + } + const re = new RegExp(expandedPattern); + if (keywordRegExpCache.size >= KEYWORD_CACHE_MAX) { + keywordRegExpCache.clear(); + } + keywordRegExpCache.set(expandedPattern, re); + return re; +} + // See https://github.com/vim-denops/denops.vim/issues/358 for details export function isDenoCacheIssueError(e: unknown): boolean { const expects = [