From e1d10dbdc4e3bc5dd1cfbc8900c313af19d481b2 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 5 Apr 2026 04:07:16 +0000 Subject: [PATCH 1/2] Initial plan From 9ab78d44f7149660371bfec4d69077ebc31cbd6d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 5 Apr 2026 04:10:40 +0000 Subject: [PATCH 2/2] perf(utils): optimize linkPath to reduce stat calls and batch entries Agent-Logs-Url: https://github.com/Shougo/dpp.vim/sessions/aa2fcc21-0873-4fd5-b750-904c7631f4b3 Co-authored-by: Shougo <41495+Shougo@users.noreply.github.com> --- denops/dpp/tests/linkpath_test.ts | 118 ++++++++++++++++++++++++++++++ denops/dpp/utils.ts | 31 +++++++- 2 files changed, 146 insertions(+), 3 deletions(-) create mode 100644 denops/dpp/tests/linkpath_test.ts diff --git a/denops/dpp/tests/linkpath_test.ts b/denops/dpp/tests/linkpath_test.ts new file mode 100644 index 0000000..8037790 --- /dev/null +++ b/denops/dpp/tests/linkpath_test.ts @@ -0,0 +1,118 @@ +import { assertEquals } from "@std/assert/equals"; +import { join } from "@std/path/join"; +import { linkPath } from "../utils.ts"; + +Deno.test( + "linkPath: symlinks a file on non-Windows", + { ignore: Deno.build.os === "windows" }, + async () => { + const tmpDir = await Deno.makeTempDir(); + try { + const srcFile = join(tmpDir, "src.txt"); + const destFile = join(tmpDir, "dest.txt"); + await Deno.writeTextFile(srcFile, "hello"); + + await linkPath(false, srcFile, destFile); + + const stat = await Deno.stat(destFile); + assertEquals(stat.isFile, true); + } finally { + await Deno.remove(tmpDir, { recursive: true }); + } + }, +); + +Deno.test( + "linkPath: skips existing dest file", + { ignore: Deno.build.os === "windows" }, + async () => { + const tmpDir = await Deno.makeTempDir(); + try { + const srcFile = join(tmpDir, "src.txt"); + const destFile = join(tmpDir, "dest.txt"); + await Deno.writeTextFile(srcFile, "hello"); + await Deno.writeTextFile(destFile, "existing"); + + // Should not throw even though dest already exists. + await linkPath(false, srcFile, destFile); + + const content = await Deno.readTextFile(destFile); + assertEquals(content, "existing"); + } finally { + await Deno.remove(tmpDir, { recursive: true }); + } + }, +); + +Deno.test( + "linkPath: recursively symlinks directory contents", + { ignore: Deno.build.os === "windows" }, + async () => { + const tmpDir = await Deno.makeTempDir(); + try { + const srcDir = join(tmpDir, "src"); + const destDir = join(tmpDir, "dest"); + await Deno.mkdir(srcDir); + await Deno.writeTextFile(join(srcDir, "a.txt"), "a"); + await Deno.writeTextFile(join(srcDir, "b.txt"), "b"); + + await linkPath(false, srcDir, destDir); + + const aStat = await Deno.stat(join(destDir, "a.txt")); + const bStat = await Deno.stat(join(destDir, "b.txt")); + assertEquals(aStat.isFile, true); + assertEquals(bStat.isFile, true); + } finally { + await Deno.remove(tmpDir, { recursive: true }); + } + }, +); + +Deno.test( + "linkPath: recursively symlinks nested subdirectories", + { ignore: Deno.build.os === "windows" }, + async () => { + const tmpDir = await Deno.makeTempDir(); + try { + const srcDir = join(tmpDir, "src"); + const subDir = join(srcDir, "sub"); + const destDir = join(tmpDir, "dest"); + await Deno.mkdir(subDir, { recursive: true }); + await Deno.writeTextFile(join(subDir, "c.txt"), "c"); + + await linkPath(false, srcDir, destDir); + + const cStat = await Deno.stat(join(destDir, "sub", "c.txt")); + assertEquals(cStat.isFile, true); + } finally { + await Deno.remove(tmpDir, { recursive: true }); + } + }, +); + +Deno.test( + "linkPath: no-ops when src does not exist", + { ignore: Deno.build.os === "windows" }, + async () => { + const tmpDir = await Deno.makeTempDir(); + try { + const nonExistent = join(tmpDir, "nonexistent.txt"); + const destFile = join(tmpDir, "dest.txt"); + + // Should not throw even when src is missing. + await linkPath(false, nonExistent, destFile); + + // dest must not have been created. + let created = false; + try { + await Deno.stat(destFile); + created = true; + } catch (_) { + // expected + } + assertEquals(created, false); + } finally { + await Deno.remove(tmpDir, { recursive: true }); + } + }, +); diff --git a/denops/dpp/utils.ts b/denops/dpp/utils.ts index 1e207ee..56a997a 100644 --- a/denops/dpp/utils.ts +++ b/denops/dpp/utils.ts @@ -83,8 +83,23 @@ export async function safeStat(path: string): Promise { return null; } +const LINK_BATCH_SIZE = 32; + export async function linkPath(hasWindows: boolean, src: string, dest: string) { - if (await isDirectory(src)) { + await linkPathInternal(hasWindows, src, dest); +} + +async function linkPathInternal( + hasWindows: boolean, + src: string, + dest: string, +) { + const srcStat = await safeStat(src); + if (!srcStat) { + return; + } + + if (srcStat.isDirectory) { if (!await safeStat(dest)) { // Not exists directory await Deno.mkdir(dest, { recursive: true }); @@ -95,9 +110,19 @@ export async function linkPath(hasWindows: boolean, src: string, dest: string) { return; } - // Recursive + // Collect all entries first, then process in parallel batches. + const entries: string[] = []; for await (const entry of Deno.readDir(src)) { - await linkPath(hasWindows, join(src, entry.name), join(dest, entry.name)); + entries.push(entry.name); + } + + for (let i = 0; i < entries.length; i += LINK_BATCH_SIZE) { + const batch = entries.slice(i, i + LINK_BATCH_SIZE); + await Promise.allSettled( + batch.map((name) => + linkPathInternal(hasWindows, join(src, name), join(dest, name)) + ), + ); } } else { if (await safeStat(dest)) {