project補完テスト
scrapbox projectを補完するテスト
ASearchの挙動テストその2のUIを使って試してみる
2024-07-22
21:40:11 Levenshtein距離の計算を↓のように工夫した
これで問題なさそう
21:28:27 ようやくバグを潰せた
動作はいい感じ
最大Levenshtein距離は文字列全体の長さではなく、空白で区切ったときの単語の最大長にあわせるほうがよさそう
例えばfoo bar bazは文字列長11なので3文字まで許容される
するとfooがどこにも含まれていない文字列もmatchしてしまう
https://gyazo.com/8d566511b5eb742952174ebef458a977
https://scrapbox-bundler.vercel.app?url=https://scrapbox.io/api/code/takker/project補完テスト/main.ts&bundle&minify&run&reload
code:main.ts
import { mount } from "./App.tsx";
mount();
$ deno check --remote -r=https://scrapbox.io https://scrapbox.io/api/code/takker/project補完テスト/App.tsx
code:App.tsx
/// <reference no-default-lib="true" />
/// <reference lib="esnext" />
/// <reference lib="dom" />
/** @jsx h */
/** @jsxFrag Fragment */
import { Fragment, h, render } from "../preact/mod.tsx";
import {
useCallback,
useMemo,
useEffect,
useState,
} from "../preact/hooks.ts";
import { Asearch, MatchResult } from "../deno-asearch/mod.ts";
import { getMaxDistance } from "./distance.ts";
import { makeSource } from "./makeSource.ts";
export const mount = () => {
const app = document.createElement("div");
const shadowRoot = app.attachShadow({ mode: "open" });
document.body.append(app);
remove = () => app.remove();
render(<App />, shadowRoot);
};
let remove: () => void;
const App = () => {
const pattern_, setPattern = useState("");
const data, setData = useState<{ name: string, displayName: string }[]>([]);
useEffect(() => {
(async () => {
for await (const candidates of makeSource()) {
setData((prev) => ...prev, ...candidates);
}
})();
}, []);
const candidates = useMemo(
() => {
const pattern = pattern_.trim().replace(/^\//, "").replace(/\/$/, "");
if (pattern.length === 0) return [];
const match = Asearch(${pattern} ).match;
const maxDistance = getMaxDistance[
Math.max(...pattern.split(/\s+/).map((word) => word.trim().length))
];
return data.flatMap(
({ name, displayName }) => {
const result1 = match(name, maxDistance);
const result2 = match(displayName, maxDistance);
if (!result1.found) {
if (!result2.found) return [];
return candidate: displayName, distance: result2.distance };
}
if (!result2.found) return candidate: name, distance: result1.distance };
return result1.distance > result2.distance
? candidate: displayName, distance: result2.distance }
: candidate: name, distance: result1.distance };
}
)
// 1. 編集距離 2. 文字列超 3. 辞書順序 が小さい順に並び替える
.sort((a, b) => {
const diff = a.distance - b.distance;
if (diff !== 0) return diff;
const lenDiff = a.candidate.length - b.candidate.length;
if (lenDiff !== 0) return lenDiff;
return a.candidate.localeCompare(b.candidate);
});
},
pattern_, data,
);
const handlePattern = useCallback(
(e: h.JSX.TargetedEvent<HTMLInputElement>) =>
setPattern(e.currentTarget.value),
[],
);
return (
<>
<style>
{`
:host {
position: fixed;
top: 60px;
left: 50%;
transform: translate(-50%, 0);
padding: 5px;
border: 1px solid lime;
border-radius: 5px;
font-size: 14px;
background-color: var(--page-bg);
color: var(--page-text-color);
}
input {
min-width: 40%;
}
button {
position: absolute;
top: 0px;
right: 0px;
}
`}
</style>
<button onClick={remove}>x</button>
<p>
<label>
pattern: <input type="text" value={pattern_} onInput={handlePattern} />
</label>
</p>
<p>
{candidates.length > 0 ? Matched ${candidates.length} words : "No matched"}
<br />
<ul>
{candidates.map(({ candidate }) => (<li key={candidate}>{candidate}</li>))}
</ul>
</p>
</>
);
};
code:distance.ts
export const getMaxDistance = [
0, // 空文字のとき
0, 0,
1, 1,
2, 2, 2, 2,
3, 3, 3, 3, 3, 3,
4, 4, 4, 4, 4, 4, 4, 4, 4, 4,
5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5,
6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6,
];
データを作る
watch list
code:makeSource.ts
import { listProjects } from "../scrapbox-userscript-std/rest.ts";
export async function* makeSource(): AsyncGenerator<{ name: string, displayName: string }[]> {
const watchList = JSON.parse(localStorage.getItem("projectsLastAccessed") ?? "{}");
const chunk = 50;
const ids = ...Object.keys(watchList);
const yielded = new Set<string>();
for (let i = 0; i <= Math.floor(ids.length / chunk); i++) {
const res = await listProjects(ids.slice(i * chunk, (i + 1) * chunk));
if (!res.ok) continue;
yield res.value.projects.flatMap((project) => {
if (yielded.has(project.name)) return [];
yielded.add(project.name);
return project;
});
}
}
#2024-07-22 19:15:01