external-completion-3/completionObserver-2
カーソル行を監視し、条件に応じて補完を開始するmodule
.cursorではなく.linesを監視する
実際にDOMが変更されたときのみ発火するので、監視頻度が減る
2021-04-08
補完を終了する条件を変えた
.cursorから文字を判定できなくても、判定を止めない
補完対象の更新がなかったとみなす
実際に文字列が変化したときだけ終了判定を行う
singletonをやめた
interfaceを変えた
before
oncompletionstart()を毎回全てのtriggerで実行する
oncompletionstart()でtrueが帰ってきたもののみoncompletionupdate()を実行する
after
test()でそのtriggerを実行できるかどうか判断する
実行できる場合 & 初回のときはoncompletionstart()を実行する
beforeのoncompletionstart()を2つの機能に分ける
unregister()を処理した後idがずれてしまっていた
libraryのupdate
dependencies
code:script.js
import {scrapboxDOM} from '../scrapbox-dom-accessor/script.js';
import {position} from '../scrapbox-cursor-position-2/script.js';
import {insertText} from '../scrapbox-insert-text-2/script.js';
import {press} from '../scrapbox-keyboard-emulation/script.js';
export class CompletionObserver {
補完開始条件の登録
code:script.js
register({test, oncompletionstart, oncompletionupdate, oncompletionend} = {}) {
if (!test) throw Error('Function "test" is not specified.');
const id = this._idCounter++;
this._triggers.push({id, test, oncompletionstart, oncompletionupdate, oncompletionend});
return id;
}
unregister(triggerId) {
this._triggers = this._triggers.filter(({id}) => id !== triggerId);
}
補完を開始する
手動で補完を開始する場合もこのmethodを呼び出す
code:script.js
async start() {
const pos = position();
if (!pos.char) return; // 更新がなかったとみなす
for (const trigger of this._triggers) {
if (!trigger.test(pos)) continue;
this._trigger = trigger;
return;
}
this.end();
}
end() { this._trigger = {} };
constructor() {
this._triggers = [];
this._idCounter = 0;
this._prevLineId = '';
this._presentTrigger = {}; // 現在起動している補完
条件に一致次第、自動で補完を開始するように設定する
code:script.js
// cursor行を監視し、条件に一致次第補完を開始する
const observer = new MutationObserver(() => {
const id = scrapboxDOM.cursorLine?.id?.slice(1);
// focusが外れただけの場合は何もしない
if (!id) return;
// 前回と違う行に飛んだ場合は補完終了
if (this._prevLineId !== id) {
this._prevLineId = id;
this.end();
return;
}
// this._preventChangeがtrueで無い限り実行する
if (this._preventChange) {
this._preventChange = false;
return;
}
this.start();
});
observer.observe(scrapboxDOM.lines, {childList: true, subtree: true});
}
get completing() {
return this._trigger.id !== undefined;
}
// 現在起動している補完
get _trigger() { return this._presentTrigger; }
set _trigger(trigger) {
const pos = position();
if (this._presentTrigger.id !== trigger.id) {
this._presentTrigger.oncompletionend?.(pos, (...params) => this._replace(...params));
this._presentTrigger = trigger;
this._presentTrigger.oncompletionstart?.(pos, (...params) => this._replace(...params));
} else {
this._presentTrigger.oncompletionupdate?.(pos, (...params) => this._replace(...params));
}
}
async _replace(text, index, length) {
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});
}
this._preventChange = true;
console.log('replace text: ', {text, index, length});
await insertText(text);
this.end();
}
}
test code
code:js
import('/api/code/programming-notes/external-completion-3%2FcompletionObserver-2/test1.js');
code:test1.js
import {CompletionObserver} from './script.js';
import {scrapboxDOM} from '../scrapbox-dom-accessor/script.js';
import {suggestContainer} from '../scrapbox-suggest-container-3/script.js';
const suggestBox = suggestContainer();
scrapboxDOM.editor.append(suggestBox);
const completionObserver = new CompletionObserver();
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 = [
{
test: ({char}) => char?.closest?.('.page-link'),
onchange: ({char}) => {
const link = char?.closest?.('.page-link');
if (!link) return;
update(link.innerText);
},
},
{
test: ({char}) => char?.closest?.('.formula'),
onchange: ({char}) => {
const formula = char?.closest?.('.formula');
if (!formula) return;
update(formula.innerText);
},
}
];
// リンク内補完
completionObserver.register({
oncompletionstart: config0.onchange, oncompletionupdate: config0.onchange, oncompletionend,
});
// 数式補完
completionObserver.register({
oncompletionstart: config1.onchange, oncompletionupdate: config1.onchange, oncompletionend,
});
// <C-Space>で強制補完開始
scrapboxDOM.editor.addEventListener('keydown', e => {
if(!e.ctrlKey || e.key !== ' ') return;
e.preventDefault();
e.stopPropagation();
completionObserver.start();
});