external-completion
/emoji/warning.iconThis code has been archived
/icons/hr.icon
/emoji/warning.icon開発版につき、破壊的変更を行う可能性あり
これ本当に、自分のproject限定にしたほうが良い気がしてきたtakker.icon
自分のprojectならどんどん補完していい
他者のprojectを補完し始めると危険
他者の言葉を借りるだけで満足するようになってしまう
あいまい検索
多少間違えていたり、間に別な文字が入っていたりしてもなんとかなる
space区切りでAND検索できる
入力するだけで検索結果がどんどん出てくる
僅かなつながりから検索出来る
/icons/hr.icon
操作説明
[/]の中にカーソルを置くと補完が開始される。
https://gyazo.com/15e780ed467709ff3578db0436bd291a
[]の内側の文字であいまい検索した候補が表示される
https://gyazo.com/94653e9535f37217a56c8b83ce96f3f7
Tabキーで入力候補の選択開始
↓orTabキーで前候補を選択
↑orShift+Tabキーで次候補を選択
Enteror クリックで候補を確定
入力候補が一つになると自動で置き換える
https://gyazo.com/d975d55eca332cde26f33907931fce73
入力途中でEnterを押すと、最初の入力候補が入力される
なんかうまく動いていない?
2020-09-06 04:02:53 直った
https://gyazo.com/807f52876b5c1f1f4690ad5456bd5627
導入方法
code:import.js
import {ExternalCompletion}
from '/api/code/takker/external-completion/script.js';
const externalCompletion = new ExternalCompletion({
Optionの説明
projects: ここに列挙したprojectsのページを入力候補とする。
必須変数
projects_load: 任意のタイミングで読み込むprojects
PageMenuを押したときに読み込まれる
default: []
includeYourProject: このUserScriptを使用するprojectのpageも入力候補に加えるかどうか
default: false
falseの場合、projectsに入っている該当projectも除外する
maxSuggestionNum: 一度に表示する入力候補の最大数
default: 30
この値に入力候補が入り切らなかった場合は、スクロール表示に切り替わる
default: calc(50vh - 100px)
code:import.js
// includeYourProject: false,
// maxSuggestionNum: 30,
// maxHeight: calc(50vh - 100px)
});
externalCompletion.start();
更新情報
2020-10-15 18:21:44
CPUのコア数分リストを分割して渡しておくことで、検索ごとに分割処理しなくて済むようになった
2020-10-13 08:52:51
backgroundでページを読み込むようにした
2020-10-12 01:12:00
ページ読み込み操作からthenを減らした
可読性の向上
2020-09-30 19:36:45
2020-09-17 09:10:50
2020-09-17 05:17:45
2020-09-14 17:41:47
ページリストのshuffleが正常に働いていなかったので直した。
全てのページ読み込みが終わってからshuffleするように直した
2020-09-12 11:09:31
2020-09-09 00:13:47
_isInCodeBlockを修正
2020-09-08 23:56:28
2020-09-06 04:03:04
2020-09-05 22:20:57
/の処理を変更
これで以前よりもうまく補完できるようになったはずtakker.icon
2020-09-05 22:09:37
デバッグ用文字列がemojisになっていたのをlinksに直した
ページタイトルが入力候補に出てこなかったのを修正
2020-09-03 08:47:05 includeYourProjectをfalseにすると、projectsからも自分のprojectを除外するようにした
これにより、単一のproject読み込みリストを自分が所属している全てのprojectで使い回すことが出来るようになった
project読み込みリストに自分が所属しているprojectを片っ端から登録しておく
要再読み込み
2020-09-03 08:22:43 includeYourProjectのdefault値をfalseにした
てかなんでtrueだったんだ……?takker.icon
自分のプロジェクトへのリンクを外部リンク形式にするなんていう需要は少数派だろう
ないとは言わない
完全には対応できなかった
2020-08-28 12:30:28 /emoji/recycle.iconrefactoring
2020-08-27 20:57:42
2020-08-27 06:58:18
projectからのページ読み込みを非同期化して、読み込み途中でも入力補完できるようにした
2020-08-26 23:54:04
文字番号が必要なくなった
既知の問題
なかなか検索結果が出てこない時がある
検索に時間がかかりすぎている
一定時間経ったら、検索を中断するようにしたい
/icons/done.icon一部プロジェクトで何故か機能しない
補完がなんか変
/icons/done.icon補完が開始するまで少し待つ必要がある
/icons/done.icon補完windowの表示でややもたつくかも
何かキーを入力しないと出てこなかったり
/icons/done.icon[/全角文字]はdefaultの入力補完windowが表示される場合もある
↑通常のリンク扱いになるため
出ても特に問題はない
項目が一つだけになったときの自動入力がなんかおかしい
別な項目が挿入される事がある?
2020-09-17 09:34:51 再現不能
/icons/done.icon変にクリックするとwindowが残る?
/icons/done.iconScrapbox標準のpopup menuが出ていると、置換に失敗する?
候補選択のキー入力でぶつかる
2020/8/26 02:09 修正済み
/icons/done.iconリストの下の方のprojectが補完候補に出てこない
対策
全てのprojectから一つづつとってきて並べる
似たようなページはprojectのリンク先から出てくる
入力候補を絞る前のsourceをシャッフルする
これが一番簡単?
projectを絞りたければ、そのproject nameを入力すればいい
簡単に実装できそう
2020/8/26 14:57 実装してみた
いい感じ?
単純にページ数が多い
ただその中にはちゃんと別のproject pageもちらほら出てくるようになった
追加機能とか
/icons/done.icon読み込むproject listをproject間で共通させたい
これはUserScriptで適当に組めばいけると思う
以下、本体のscript
/icons/hr.icon
以下をimportする
code:script.js
import {ICompletion} from '/api/code/takker/ICompletion/script.js';
メイン関数
code:script.js
export class ExternalCompletion extends ICompletion {
constructor({projects, projects_lazy = [], includeYourProject = false, maxSuggestionNum = 30, maxHeight = 'calc(50vh - 100px)'} = {}) {
super({
id: 'external-completion',
projects: projects,
projects_lazy: projects_lazy,
includeYourProject: includeYourProject,
maxSuggestionNum: maxSuggestionNum,
maxHeight: maxHeight,
trigger: '/',
makeRaw: string => string.substr(1, string.length - 2),
searchWorkerCode: '/api/code/takker/external-completion/searchWorker.js'
});
}
awaitで待たないようにしたので、ページの読み込み途中でも補完を使用できる
ページ読み込み抜けが出ている?
Promise.addで全部読み込みできるまで補完を待つことにする
.filter(page => page.image !== null)を消すのを忘れていた
WebWorkerからはmain threadのobjectを使用できないので、scrapbox.Projectなどにアクセスできない code:script.js
async _importDataList() {
// 処理を分けると複雑になるので、自分のprojectであってもAPIからリンクを取得する
this.searchWorker.postMessage({loading: true, projects: this.projects});
// 遅延読み込み用PageMenuを追加する
scrapbox.PageMenu.addMenu({
title: load external links from ${this.projects_lazy.length} projects,
image: '/asset/img/logo.png',
onClick: () =>
this.searchWorker.postMessage({loading: true, projects: this.projects_lazy})
});
}
入力候補を更新する関数
code:script.js
//補完windowに表示する項目を作成する
_createGuiList(matchedList) {
const cursor = document.getElementById('text-input');
return matchedList
.map(link => {
const div = document.createElement('div');
div.textContent = link;
return new Object({
onComfirm: () => {
this._log(clicked [${link}]);
this._comfirm(cursor, [${link}]);},
});
});
}
code:script.js
_postMessage(word) {
this.searchWorker.postMessage({word: word, list: this.links, maxSuggestionNum: this.maxSuggestionNum});
}
}
入力補完候補をfetchする
fetchも並列処理用workerに任せてもいいと思う
仕事の分担が明確になる
searchWorker: 仕事の割り振りと、結果の統合
compute: 割り振られた仕事を実行する
listを本体で持つ必要ないな。
webworkerに持たせよう。
code:searchWorker.js
// list: 補完候補が入ったリスト
const workerNum = navigator.hardwareConcurrency;
const workers = range(workerNum).map(_ => new Worker(
'/api/code/takker/external-completion/compute.js'));
並列で検索を行う
code:searchWorker.js
// 曖昧検索をする
function searchWord(message) {
const word = message.data.word;
_log(start searching for ${word}...);
const jobs = workers
.map(worker => {
// 処理を投げる
const job = new Promise((resolve, reject) =>
worker.addEventListener('message', message => {
resolve(message.data);
}));
worker.postMessage({word: word, limit: message.data.maxSuggestionNum});
return job;
});
// 処理が帰って来たら結果をPOSTする
Promise.all(jobs).then(results => {
// まず、listの順番を復元する
const temp = results.sort((a, b) => a.index - b.index)
.map(result => result.linksData);
// 曖昧度順に並べ直す
const max = temp.map(linksData => linksData.length).reduce((r,c) => Math.max(r,c));
const matchedLinksData = range(max)
.flatMap(i => temp.flatMap(linksData => linksDatai ?? [])); _log(Found ${matchedLinksData.length} links: %o, matchedLinksData);
self.postMessage(matchedLinksData.slice(0, message.data.maxSuggestionNum)
.map(data => data.link));
});
}
code:searchWorker.js
async function addLinks(projects) {
_log(start loading projects...);
const externalLinks = await Promise.all(projects
.flatMap(project => fetchExternalLinks(project)))
.then(lists => lists.flat()
シャッフルしたあとに、page titleの長さ順に並び替えてもよさそう
近い順かつタイトルが短い順に並べられる
計算workerの方でもlinkedを使うので、dataにlinkedを含めて渡す
code:searchWorker.js
.sort((a,b) => b.linked - a.linked)
.map(data => {return {link: /${data.project}/${data.title}, linked: data.linked};}));
_log('Finish loading all links: %o', externalLinks);
// workerの数だけ分割して渡す
const skip = Math.floor(externalLinks.length/workerNum) + 1;
for (const i of range(workerNum)) {
const dividedList = externalLinks.slice(i * skip,(i + 1) * skip);
workersi.postMessage({loading: true, list: dividedList, index: i, // listの順番を保存する
});
}
// 処理が終わったことを伝える
self.postMessage(undefined);
}
async function fetchExternalLinks(project) {
let followingId = null;
let temp = [];
const relations = []; //link先の情報 linkedの計算に使う
_log(Start loading links from ${project}...);
do {
_log(Loading links from ${project}: followingId = ${followingId});
const json = await (!followingId ?
fetch(/api/pages/${project}/search/titles) :
fetch(/api/pages/${project}/search/titles?followingId=${followingId}))
.then(res => {
followingId = res.headers.get('X-Following-Id');
return res.json();
});
relations.push(...json);
} while(followingId);
// linkedを計算する前に重複を除いておく
_log(Start calculating linked of /${project}...: %o,temp);
// linkedを計算する
const links = temp.map(title => {
return {
project: project,
title: title,
linked: relations
.filter(relation => relation.links.includes(title))
.length,
};});
_log(Finish calculating linked of /${project}.);
_log(Loaded ${links.length} links from /${project});
return links;
}
WebWorkerの本体処理
parameterに応じて、読み込みを行うか検索を行うかを切り替える
code:searchWorker.js
self.addEventListener('message', async message => {
if (message.data.loading) {
addLinks(message.data.projects);
return;
}
searchWord(message);
});
// debug用
function _log(msg, ...objects) {
console.log([search-worker@external-completion] ${msg}, ...objects);
}
CPUごとの計算script
検索候補はかき混ぜて文字列長順に並び替えておく
2020-11-26 07:54:08 元のWebWorkerですでにshuffleしてあるのだから、別にいらない処理だったような……
code:compute.js
self.importScripts('/api/code/takker/fizzSearch/script.js');
// 検索候補
let list = [];
let index = 0;
self.addEventListener('message', message => {
if(message.data.loading) {
index = message.data.index;
//_log(index, Adding new ${message.data.list.length} suggestion items...);
// Most linked順に並び替える
.sort((a,b) => b.linked - a.linked);
//_log(index, 'Added.');
return;
}
_log(index, start searching for ${message.data.word}...);
const matchedLinksData = fizzSearch({word: message.data.word.split('/').join(' '), list: list, limit: message.data.limit, func: data => data.link});
_log(index, 'finished: %o', matchedLinksData);
// index: externalLinksの並び順
self.postMessage({linksData: matchedLinksData, index: index});
});
// debug用
function _log(number, msg, ...objects) {
console.log([search-worker ${number}@external-completion] ${msg}, ...objects);
}