external-completion-3/completionObserver
from 拡張Scrapbox入力補完の開始・終了条件を決める
カーソル行を監視し、条件に応じて補完を開始するmodule
外部から注入する必要がある設定
補完を開始するか判定する関数
開始するときは、{target, index}を返す
target: 監視対象の文字列
index: 文字列の先頭の列番号
補完開始及び補完対象が更新されたときに呼び出されるcallback関数
oncompletion: ({target, index}) => void
2021-03-16 15:04:14 judgeとoncompletionを別々にする必要はなかったのでは?
judgeで補完を開始できると判断できたら即座にomcompletionを実行する
密接に関わり合っている
judgeとomcompletionは合体させよう
2021-03-18 10:22:20 合体させた
状態遷移を入れる
oncompletionstart
oncompletionupdate
oncompletionend
外部から任意のタイミングで終われるようにする
補完を終了したときに呼び出されるcallback関数
oncompletionend: () => void
いらない
このUserScriptがやること
Scrapbox editorの現在行を監視する
補完条件に一致したら、oncompletionを呼び出す
複数の補完条件とoncompletionのペアを登録できる
一致したものが呼び出される
手動で補完開始・終了ができるようにする
開始:startCompletion
登録されたペアのうち、条件に一致するものが呼び出される
条件に一致するものがなければ何もしない
終了:endCompletion
テキスト入力と合体させる?
scrapboxの本文の状態を走査するという点でまとめられるか?
10:22:15 合体させた
既知の問題
/icons/done.iconscrapbox-cursor-positionが折返しのある行で正しく位置を特定できていない
なんか遅い
external-completion-3/completionObserverでDOMが変更されるたびに文字列比較をしているからか?
対策
.linesのsubtree監視に切り替える
DOMが変更された際に、実際の文字列に変更があったかどうかを確かめる
なければ何もしない
.cursorを監視するよりも監視頻度が少なくなると考えられる
code:script.d.ts
type CursorInfo = {
left?: Char; // cursorの左側
right?: Char; // cursorの右側
line: Line; // cursorのいる行
};
declare class CompletionObserver {
register(props: {
judge: (cursor: CursorInfo) => {
target: string;
index: number;
} | undefined;
oncompletion: (props: {target: string; index: number;}, cursor: CursorInfo) => string | undefined;
}): number;
unregister(triggerId: number): void;
constructor();
start(): void;
}
code:script.js
import {scrapboxDOM} from '../scrapbox-dom-accessor/script.js';
import {cursor} from '../scrapbox-cursor-position/script.js';
import {insertText} from '/api/code/takker/scrapbox-insert-text/script.js';
import {press} from '../scrapbox-keyboard-emulation/script.js';
class CompletionObserver {
補完開始条件の登録
code:script.js
register({oncompletionstart, oncompletionupdate, oncompletionend}) {
const id = this._idCounter++;
this._triggers.push({id, oncompletionstart, oncompletionupdate, oncompletionend});
return id;
}
unregister(triggerId) {
this._triggers = this._triggers.filter((_, i) => i !== triggerId);
}
補完を開始する
手動で補完を開始する場合もこのmethodを呼び出す
code:script.js
async start() {
const c = cursor();
for (const {id, oncompletionstart, oncompletionupdate} of this._triggers) {
const isTriggered = await (this.completing === id ? oncompletionupdate : oncompletionstart)
(c, (...params) => this._replace(...params));
if (!isTriggered) continue;
this._completing = id;
return;
}
this.end();
}
end() {
if (this._completing !== undefined) this._triggersthis._completing.oncompletionend();
this._completing = undefined;
}
constructor() {
this._triggers = [];
this._idCounter = 0;
this._prevLineId = '';
this._prevLineText = '';
this._completing = undefined;
条件に一致次第自動で補完を開始するように設定する
cursor行の文字列が変更された場合のみ補完を開始する
確認するのはidとtext
id
一つ前に確認した行と同じ行かどうか
これを確認しないと、<Up>/<Down>で別の行から補完対象文字列の中に入ったとき、入力していないにも関わらず補完が始まってしまう
リンクサジェストの開始/終了条件はこのことを考慮していない
text
対象行が実際に変化したかどうか
code:script.js
// cursor行を監視し、条件に一致次第補完を開始する
const observer = new MutationObserver(() => {
// cursor行に変更がなければ何もしない
const id = scrapboxDOM.cursorLine?.id?.slice(1);
const text = scrapbox.Page.lines
.find(line => line.id === id)
?.text;
if (this._prevLineId !== id) {
this._prevLineId = id;
this._prevLineText = text;
this.end()
return;
}
if (text === this._prevLineText) {
this.end();
return;
}
this._prevLineText = text;
this.start();
});
observer.observe(scrapboxDOM.cursor, {attributes: true});
}
get completing() {
return this._completing !== undefined;
}
async _replace(text, index, length, cursor) {
scrapboxDOM.textInput.focus();
press('Home');
press('Home');
for (let i = 0; i < index; i++) {
press('ArrowRight');
}
for (let i = 0; i < length; i++) {
press('ArrowRight', {shiftKey: true});
}
補完確定処理による誤検出を防ぐ
補完確定処理によって行の文字列が変更されたときは、その変更をMutationObserverで誤検出しないようにする
code:script.js
// 入力後の文字列
const prevText = cursor.line.text;
this._prevLineText = prevText.slice(0, index) + text + prevText.slice(index + length);
await insertText(text);
this.end();
}
}
export const completionObserver = new CompletionObserver();
test code
code:js
import('/api/code/programming-notes/external-completion-3%2FcompletionObserver/test1.js');
code:test1.js
import {completionObserver} from './script.js';
import {scrapboxDOM} from '../scrapbox-dom-accessor/script.js';
import '../scrapbox-suggest-container-3/script.js';
const suggestBox = document.createElement('suggest-container');
scrapboxDOM.editor.append(suggestBox);
const update = (title) => {
suggestBox.replace({title}, {title: completing: ${completionObserver.completing}});
suggestBox.position({
top: parseInt(scrapboxDOM.cursor.style.top) + 14,
left: parseInt(scrapboxDOM.cursor.style.left),
});
}
const oncompletionend = () => suggestBox.clear();
const config = [
(cursor, replace) => {
const link = (cursor.left ?? cursor.right)?.link;
if (!link) return false;
update(link.text);
return true;
},
(cursor, replace) => {
const formula = (cursor.left ?? cursor.right)?.DOM?.closest?.('.formula');
if (!formula) return false;
update(formula.innerText);
return true;
},
];
// リンク内補完
completionObserver.register({
oncompletionstart: config0,
oncompletionupdate: config0,
oncompletionend,
});
// 数式補完
completionObserver.register({
oncompletionstart: config1,
oncompletionupdate: config1,
oncompletionend,
});
// <C-Space>で強制補完開始
scrapboxDOM.editor.addEventListener('keydown', e => {
if(!e.ctrlKey || e.key !== ' ') return;
e.preventDefault();
e.stopPropagation();
completionObserver.start();
});
#2021-03-18
#2021-03-16
#2021-03-15 17:19:02
#2021-03-08 06:14:24