external-completion
↑マルチスレッドプログラミングを採用したので、こっちの方がスムーズに動きます
操作説明
[/]の中にカーソルを置くと補完が開始される。
https://gyazo.com/15e780ed467709ff3578db0436bd291a
[]の内側の文字であいまい検索した候補が表示される
https://gyazo.com/4b082798f3e40bc0db10f5c73ff8323b
↓orTabキーで前候補を選択
↑orShift+Tabキーで次候補を選択
Enteror クリックで候補を確定
入力候補が一つになると自動で置き換える選択していれば置き換える
focusなしで自動置換すると、userが文字削除操作をしているときにconflictしてしまうのでやめた
入力途中でEnterを押すと、最初の入力候補が入力される
https://gyazo.com/807f52876b5c1f1f4690ad5456bd5627
導入方法
code:import.js
import {startSuggestingExternalProjectLinks}
from '/api/code/customize/external-completion/script.js';
// 入力候補に入れたいprojectを書く
startSuggestingExternalProjectLinks([
'shokai',
'hub',
'customize',
'scrapboxlab']);
補完したい外部プロジェクトの名前のリストをstartSuggestingExternalProjectLinksの変数に渡す 初期状態では、入力候補は最大30件まで表示される。
これを変更したい場合は、startSuggestingExternalProjectLinksの第2引数に最大表示件数を渡す。
入力補完windowが長すぎる場合は、<ul>のstyleにmax-height: "calc(50vh - 100px)"を追加するといい感じになる 注意
使いすぎると思考停止に陥る恐れがあります
外部プロジェクトを補完する前に自分の言葉で十分書けているか確認したほうがいいでしょう。
もしくは自分のプロジェクトのみを補完候補に入れるのも手です
参考にしたもの
これをベースに作成した
キー入力部分は根っこから変えた
元コードではキー入力を監視し、それをstackに積むことでuserの入力を取得している
しかしそれだと全角文字を取得できないという欠点があった
「日本語」に変換される前の「nihongo」しか取得できない
そこで、scrapboxのeditorをDOM操作して入力文字を取得する方針に変えた 座標計算で結構つまずいた
offsetLeftの基準座標が/icons/GoogleChrome.iconと/icons/firefox.iconで違う
focusの順番を変えるとcursorの位置が一文字分ずれる
入力候補選択で参考にした
当初は全てコードで書くつもりだったが、うまく動かなかったので↑の通りに<a>タグを使うことにした
CoffeeScriptで書かれたクラスをほぼそのままjavascriptに書き換えただけ
空白区切りで入力した文字列の順番に関係なくマッチできる
既知の問題
補完が開始するまで少し待つ必要がある
補完windowの表示でややもたつくかも
何かキーを入力しないと出てこなかったり
[/全角文字]はdefaultの入力補完windowが表示される場合もある
項目が一つだけになったときの自動入力がなんかおかしい
別な項目が挿入される事がある?
/icons/Scrapbox.icon標準のpopup menuが出ていると、置換に失敗する?
候補選択のキー入力でぶつかる
2020/8/26 02:09 修正済み
/icons/hr.icon
以下本体のコード
code:script.js
import {suggestWindow} from '/api/code/customize/suggestWindow/script.js';
import fizzSearch from '/api/code/customize/fizzSearch/import.js';
import {externalLinkObserver} from '/api/code/customize/externalLinkObserver/script.js';
メイン関数
code:script.js
export async function startSuggestingExternalProjectLinks(projectNames, maxSuggestionNum = 30) {
const suggestion=new suggestWindow();
const linkObserver = new externalLinkObserver(suggestion.editor);
const titles = await importExternalPageName(projectNames);
// windowの更新
// keyupとmouseupで作動する
const eventHandler = e => {
if (!e.key) return;
const cursor = document.getElementById('text-input');
// code blockの頭とcode blockの中身では動作しないようにする
if(suggestion.editor.getElementsByClassName('cursor-line').length != 0) {
if (suggestion.editor.getElementsByClassName('cursor-line')0.textContent.trim() == 'code:' || suggestion.editor.getElementsByClassName('cursor-line code-block').length == 1) {
suggestion.close(cursor);
return;
}
}
// 監視対象である、userが入力中の外部プロジェクトリンクに変更があれば、入力候補を更新する
if (linkObserver.reload(cursor)) {
const suggestTitles = updateSuggestList(titles, linkObserver);
if(!suggestTitles) {
suggestion.close();
return;
}
suggestion.updateItems(
createGuiList(suggestTitles.slice(0, maxSuggestionNum)
, suggestion, linkObserver, cursor));
}
// 入力対象が外部プロジェクトリンクでなければ何もしない
if(!linkObserver.linkString) {
suggestion.close();
return;
}
// 候補が一つしかないときはそれを入力する
if(suggestion.getItems().length == 1 && suggestion.hasFocus()) {
comfirmSuggestion(suggestion, linkObserver, cursor
, suggestion.getItems()0.textContent); }
switch(e.key) {
case 'Escape':
case 'Home':
case 'End':
case 'PageUp':
case 'PageDown':
suggestion.close();
return;
}
// for debug
//if(!suggestion.hasFocus()){
// const cursorIndex = getCharPositionUnderCursor(suggestion.editor,cursor);
// console.log(targetLink: ${linkObserver.linkString});
// console.log(left: ${cursorIndex - linkObserver.leftCharIndex -1}, cursor: ${cursorIndex}, right: ${linkObserver.rightCharIndex - cursorIndex});
//}
suggestion.reDraw(cursor);
suggestion.open();
};
// default動作を握りつぶしたいキー入力は、keydownの段階で操作する
const eventHandler2 = e => {
if (!e.key) return;
// 入力補完windowが表示されていないときは何もしない
if(!suggestion.isOpen()) return;
// 一番先頭の候補から上に移動したときはwindowを閉じることにする
if((e.key == 'ArrowUp'
|| (e.key == 'Tab' && e.shiftKey))
&& (suggestion.isOpen() && suggestion.hasFirstItemFocus())) {
e.stopPropagation();
e.preventDefault();
suggestion.close();
return;
}
// focusをwindowに移す
if((e.key == 'ArrowDown' || e.key == 'Tab')
&& (suggestion.isOpen() && !suggestion.hasFocus())) {
e.stopPropagation();
e.preventDefault();
suggestion.selectFirstItem();
return;
}
// 一番先頭の候補で確定する
if(e.key == 'Enter'
&& suggestion.isOpen()
&& !suggestion.hasFocus()) {
e.stopPropagation();
e.preventDefault();
const cursor = document.getElementById('text-input');
comfirmSuggestion(suggestion, linkObserver, cursor
, suggestion.getItems()0.textContent); return;
}
};
suggestion.editor.addEventListener('keydown', eventHandler2);
suggestion.editor.addEventListener('keyup', eventHandler);
suggestion.editor.addEventListener('mouseup', eventHandler);
}
projectの全ページ数に応じてskipの値を変えられるようにしたい
あとUserScriptをloadしたあとも非同期にprojectのページを読み込めるようにできるとなおよい
code:script.js
async function importExternalPageName(projectNames) {
let promises = [];
for (const projectName of projectNames) {
for (let index = 0; index < 10; index++) {
promises.push(fetch(/api/pages/${projectName}/?limit=1000&skip=${index*1000})
.then(res => res.json())
.then(json => json.pages.map(page => /${projectName}/${page.title})));
}
}
const temp = await Promise.all(promises);
//console.log(Got ${result.length} titles:)
//result.slice(0,30)
// .forEach( (title, index) => console.log(\tNo.${index}: ${title}));
//console.log('And more...');
return result;
}
入力候補を更新する関数
code:script.js
function updateSuggestList(sourceList, linkObserver){
if(!linkObserver.linkString) return undefined;
const tergetString = linkObserver.linkString.slice(1,linkObserver.linkString.length - 1);
const suggestTitles = fizzSearch(tergetString, sourceList);
if(suggestTitles.length == 0) return undefined;
// 補完候補と既に入力されたリンクが同じであれば何もしない
if(suggestTitles.length == 1 && suggestTitles0 == tergetString) { return undefined;
}
return suggestTitles;
}
カーソルがある[]の内部をinsertTextで置換する
code:script.js
function comfirmSuggestion(suggestion, linkObserver, cursor, insertText) {
cursor.focus();
// '['に当たるまでカーソルを動かす
const cursorLine = () => suggestion.editor.getElementsByClassName('cursor-line')0 .getElementsByClassName(c-${getCharPositionUnderCursor(suggestion.editor,cursor)})0.textContent; while(cursorLine() != '[') {
//console.log(char: ${cursorLine()});
cursor.dispatchEvent(new KeyboardEvent('keydown',
{bubbles: true, cancelable: true, keyCode: 37}));
}
replaceText(cursor,linkObserver.linkString, [${insertText}]);
suggestion.close();
}
入力補完windowに表示する項目を作成する
code:script.js
function createGuiList(suggestTitles, suggestion, linkObserver, cursor) {
return suggestTitles
.map(title => {
const a = document.createElement('a');
a.setAttribute('tabindex', '0');
a.setAttribute('role', 'menuitem');
const div = document.createElement('div');
div.textContent = title;
a.appendChild(div);
a.onclick = () => comfirmSuggestion(suggestion, linkObserver, cursor, title);
a.onkeypress = e => {
if (e.key !== "Enter") return;
e.stopPropagation();
e.preventDefault();
comfirmSuggestion(suggestion, linkObserver, cursor, title);
};
return a;
});
}
cursor直下の文字が行先頭から数えて何番目に位置しているかを返す関数
code:script.js
function getCharPositionUnderCursor(editor,cursor){
const cursorLine = editor.getElementsByClassName('cursor-line')0; const text = cursorLine.textContent;
if (text === '' ) return undefined;
// 要素の左端の相対座標を取得する
const getLeftPosition = target => target.getBoundingClientRect().left - cursorLine.getBoundingClientRect().left;
// テキストから、それぞれの文字の座標を入れた配列を作る
const charPostions = text.split('').map((char,index) =>
[getLeftPosition(cursorLine.getElementsByClassName(c-${index})0), index]); const charDistances = charPostions
.map(pos => [Math.abs(parseInt(cursor.style.left) - pos0), pos1]) const targetIndex = charDistances01; return targetIndex;
}
oldTextの文字数だけDeleteを実行した後に、insertTextを挿入する関数
code:script.js
function replaceText(cursor,oldText, newText) {
const isFirefox = () => {
const userAgent = window.navigator.userAgent.toLowerCase();
if (userAgent.indexOf('firefox') != -1) {
return true;
}
return false;
};
setTimeout(() => {
// Deleteを実行
for(const _ of range(oldText.length)) {
cursor.dispatchEvent(new KeyboardEvent('keydown',
{bubbles: true, cancelable: true, keyCode: 46}));
}
if (isFirefox()) {
const start = cursor.selectionStart; // in this case maybe 0
cursor.setRangeText(newText);
cursor.selectionStart = cursor.selectionEnd = start + newText.length;
const uiEvent = document.createEvent('UIEvent');
uiEvent.initEvent('input', true, false);
cursor.dispatchEvent(uiEvent);
} else {
document.execCommand('insertText', false, newText);
}
}, 50);
}
UserScript.icon