From 4985613b5e7da12d9b4e361a10da848e1da398e8 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 27 Apr 2026 09:54:25 +0000 Subject: [PATCH 1/3] Initial plan From 275a3ef0b087a756fcd65000e5ad8614f3ffabc1 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 27 Apr 2026 10:01:27 +0000 Subject: [PATCH 2/3] perf: cache keywordPattern and RegExp, parallelize filter resolution, batch denops RPCs Agent-Logs-Url: https://github.com/Shougo/ddc.vim/sessions/e6374dc2-3019-42a7-92b9-dfaac21bbfe0 Co-authored-by: Shougo <41495+Shougo@users.noreply.github.com> --- denops/ddc/base/source.ts | 4 ++-- denops/ddc/ddc.ts | 10 ++++++---- denops/ddc/ext.ts | 11 ++++------- denops/ddc/utils.ts | 19 +++++++++++++++++++ 4 files changed, 31 insertions(+), 13 deletions(-) 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..82405bd 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,11 @@ 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) => [ + 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..8029f49 100644 --- a/denops/ddc/utils.ts +++ b/denops/ddc/utils.ts @@ -15,6 +15,9 @@ import { fromFileUrl } from "@std/path/from-file-url"; import { join } from "@std/path/join"; import { dirname } from "@std/path/dirname"; +const convertKeywordPatternCache = new Map(); +const keywordRegExpCache = new Map(); + export async function convertKeywordPattern( denops: Denops, keywordPattern: string, @@ -23,13 +26,29 @@ export async function convertKeywordPattern( const iskeyword = bufnr === undefined ? await op.iskeyword.getLocal(denops) : await op.iskeyword.getBuffer(denops, bufnr); + 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); + 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); + 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 = [ From 816f014cc6de0aa8b21ae3f653c7d70bec955c2a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 27 Apr 2026 10:03:32 +0000 Subject: [PATCH 3/3] perf: address review comments - add cache size limit and clarify type cast Agent-Logs-Url: https://github.com/Shougo/ddc.vim/sessions/e6374dc2-3019-42a7-92b9-dfaac21bbfe0 Co-authored-by: Shougo <41495+Shougo@users.noreply.github.com> --- denops/ddc/ddc.ts | 1 + denops/ddc/utils.ts | 11 +++++++++++ 2 files changed, 12 insertions(+) diff --git a/denops/ddc/ddc.ts b/denops/ddc/ddc.ts index 82405bd..e3086e6 100644 --- a/denops/ddc/ddc.ts +++ b/denops/ddc/ddc.ts @@ -623,6 +623,7 @@ export class Ddc { } 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), ]); diff --git a/denops/ddc/utils.ts b/denops/ddc/utils.ts index 8029f49..1c49ddd 100644 --- a/denops/ddc/utils.ts +++ b/denops/ddc/utils.ts @@ -15,6 +15,9 @@ 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(); @@ -26,6 +29,8 @@ 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) { @@ -35,6 +40,9 @@ export async function convertKeywordPattern( const replaced = keywordPattern .replaceAll("\\k", "[" + keyword + "]") .replaceAll("[:keyword:]", keyword); + if (convertKeywordPatternCache.size >= KEYWORD_CACHE_MAX) { + convertKeywordPatternCache.clear(); + } convertKeywordPatternCache.set(cacheKey, replaced); return replaced; } @@ -45,6 +53,9 @@ export function getKeywordRegExp(expandedPattern: string): RegExp { return cached; } const re = new RegExp(expandedPattern); + if (keywordRegExpCache.size >= KEYWORD_CACHE_MAX) { + keywordRegExpCache.clear(); + } keywordRegExpCache.set(expandedPattern, re); return re; }