async-await loopを使ったScrapbox拡張入力補完テスト
さくっとasync-await loopで拡張Scrapbox入力補完を作れるか試してみる
実装
event loopはevent loopテスト (takker99/emoji-completionで試すevent loop設計)を大体そのまま使う
loop図解
takker99/emoji-completionで試すevent loop設計.icon
UIはPreactでつくる
2022-06-12
12:05:31 一旦中断
まだevent loopをhooksに書き換えている途中
code:App.tsx
/// <reference no-default-lib="true"/>
/// <reference lib="esnext"/>
/// <reference lib="dom" />
/** @jsx h */
/** @jsxFrag Fragment */
import { h, Fragment } from "./preact.tsx";
import {
caret,
getCharDOM,
getText,
getInternalLink,
textInput,
cursor,
} from "../scrapbox-userscript-std/dom.ts";
import { promisify, sleep } from "../async-lib/mod.ts";
const listenStart, handleStart = promisify<void>({
maxQueued: 0,
});
const listenCompletioning, handleCompletioning = promisify<{ type: "cursormove" | "textinput" } | { type: "confirm"; callback: ()=> Promise<void>; }>({
maxQueued: 1,
});
const waitForOrder, orderSearch = promisify<string>({
maxQueued: 1,
});
textInput()!.addEventListener("input", (e) => {
if (e.isComposing) return;
handleStart();
});
const forceStartCompletion = () => handleStart();
const handleInput = () =>
handleCompletioning({ type: "textinput" });
const canStartCompletion = (): Promise<string | void> => {
const { line, char } = caret().position;
const charDOM = getCharDOM(line, char);
if (!charDOM) return Promise.resolve();
const link = getInternalLink(charDOM);
if (!link) return Promise.resolve();
return Promise.resolve(getText(link) ?? "");
};
let completioning = false;
(async () => {
while (true) {
render(
{ type: "text", text: "ready for search" },
);
await listenStart();
try {
const initialParams = await canStartCompletion();
if (!initialParams) continue;
orderSearch(initialParams);
completioning = true;
render(
{ type: "text", text: "completion mode" },
);
textInput()!.addEventListener(
"input",
handleInput,
);
observer.observe(cursor()!, { attributes: true });
try {
while (true) {
const data = await listenCompletioning();
if (
data.type === "cursormove" ||
data.type === "textinput"
) {
const param = await canStartCompletion();
if (!param) break;
orderSearch(param);
continue;
}
if (data.type === "confirm") {
await data.callback();
}
break;
}
} catch (e) {
throw e;
} finally {
textInput()!.removeEventListener(
"input",
handleInput,
);
observer.disconnect();
}
} catch (e) {
console.error(e);
} finally {
completioning = false;
}
}
})();
(async () => {
while (true) {
const params = await waitForOrder();
if (!completioning) continue;
const { render, dispose } = useStatusBar();
render(
{ type: "spinner" },
{ type: "text", text: searching for "${params}" },
);
await sleep(2000);
dispose();
}
})();
code: useEventLoop.ts
import { useEffect, useState } from "./preact.tsx";
import { useAsync } from "./useAsync.ts";
export const useEventLoop = () => {
const forceStart, setForceStart = useState(() => {});
let completioning = false;
useAsync(async () => {
const listenStart, emitStart = promisify<void>({
maxQueued: 0,
});
setForceStart(() => () => emitStart());
const textarea = textInput()!;
textarea.addEventListener("input", (e) => {
if (e.isComposing) return;
emitStart();
});
const listenCompletioning, handleCompletioning = promisify<{ type: "cursormove" | "textinput" } | { type: "confirm"; callback: ()=> Promise<void>; }>({
maxQueued: 1,
});
const waitForOrder, orderSearch = promisify<string>({
maxQueued: 1,
});
const handleInput = () =>
handleCompletioning({ type: "textinput" });
while (true) {
await listenStart();
const initialParams = await canStartCompletion();
if (!initialParams) continue;
orderSearch(initialParams);
try {
setCompletioning(true);
// テキスト入力の監視開始
textarea.addEventListener("input", handleInput);
// カーソル移動の監視開始
const cursor = takeCursor();
const handleMove = () => handleCompletioning({ type: "cursormove" });
cursor.addChangeListener(handleMove);
try {
while (true) {
const data = await listenCompletioning();
if (
data.type === "cursormove" ||
data.type === "textinput"
) {
const param = await canStartCompletion();
if (!param) break;
orderSearch(param);
continue;
}
if (data.type === "confirm") {
await data.callback();
}
break;
}
} catch (e) {
throw e;
} finally {
textarea.removeEventListener("input", handleInput);
cursor.removehangeListener(handleMove);
}
} catch (e) {
throw e;
} finally {
setCompletioning(false);
}
}
}, []);
};
code:useAsync.ts
import { useEffect, useState } from "./preact.tsx";
export type State = "pending" | "resolved" | "rejected";
export const useAsync = <T>(fn: () => Promise<T>, deps: unknown[]): {
state: "pending";
} | {
state: "resolved";
value: T;
} | {
state: "rejected";
value: unknown;
} => {
const error, setError = useState<unknown>();
const result, setResult = useState<T>();
const state, setState = useState<State>("pending");
useEffect(() => fn()
.then((value) => {
setState("resolved");
setResult(value);
})
.catch((value) => {
setState("rejected");
setError(value);
}),
fn
);
switch (state) {
case "pending":
return { state };
case "resolved":
return { state, value: result };
case "rejected":
return { state, value: error};
}
};
code:useCaretPosition.ts
import { useEffect, useState } from "./preact.tsx";
import { Position, takeCursor } from "../scrapbox-userscript-std/dom.ts";
export const useCaretPosition = (): Position => {
const position, setPosition = useState<Position>({ line: 0, char: 0 });
useEffect(() => {
const cursor = takeCursor();
const update = () => setPosition(cursor.getPosition());
cursor.addChangeListener(update);
return () => cursor.removeChangeListener(update);
}, []);
return position;
};
code:preact.tsx
export { Fragment, h, render } from "https://esm.sh/preact@10.6.4";
export type { ComponentChildren } from "https://esm.sh/preact@10.6.4";
export * from "https://esm.sh/preact@10.6.4/hooks";
#2022-06-12 11:48:57