generated at
external-completion
warningThis code has been archived
new version→external-completion-2
hr
warning開発版につき、破壊的変更を行う可能性あり
ドッグフーディングして、不具合を大体解消できたら、/customizeにも掲載する。

これ本当に、自分のproject限定にしたほうが良い気がしてきたtakker
自分のprojectならどんどん補完していい
自分の言葉で書いた思考だから
他者のprojectを補完し始めると危険
他者の言葉を借りるだけで満足するようになってしまう
外部プロジェクトのページ検索システムとしては超絶優秀
事前に決めた外部プロジェクトを横断検索出来る
あいまい検索
多少間違えていたり、間に別な文字が入っていたりしてもなんとかなる
space区切りでAND検索できる
入力するだけで検索結果がどんどん出てくる
空リンクも検索可能
僅かなつながりから検索出来る


hr
外部プロジェクトのリンクを入力補完出来るようにするUserScriptです

操作説明
[/] の中にカーソルを置くと補完が開始される。



[] の内側の文字であいまい検索した候補が表示される
Tab キーで入力候補の選択開始
or Tab キーで前候補を選択
or Shift+Tab キーで次候補を選択
Enter or クリックで候補を確定

入力候補が一つになると自動で置き換える


入力途中で Enter を押すと、最初の入力候補が入力される
なんかうまく動いていない?
2020-09-06 04:02:53 直った



導入方法
impot.jsをコピーし、自分のページのscript.jsに貼り付ける
import.js
import {ExternalCompletion} from '/api/code/takker/external-completion/script.js'; const externalCompletion = new ExternalCompletion({ projects: ['shokai', 'hub', 'customize', 'scrapboxlab']
Optionの説明
projects : ここに列挙したprojectsのページを入力候補とする。
必須変数
projects_load : 任意のタイミングで読み込むprojects
PageMenuを押したときに読み込まれる
default: []
includeYourProject : このUserScriptを使用するprojectのpageも入力候補に加えるかどうか
default: false
falseの場合、 projects に入っている該当projectも除外する
maxSuggestionNum : 一度に表示する入力候補の最大数
default: 30
maxHeight : 入力補完windowの最大の高さ
この値に入力候補が入り切らなかった場合は、スクロール表示に切り替わる
default: calc(50vh - 100px)
import.js
// includeYourProject: false, // maxSuggestionNum: 30, // maxHeight: `calc(50vh - 100px)` }); externalCompletion.start();



更新情報
2020-10-15 18:21:44
読み込んだ外部project linkをWebWorker側に持たせるようにした
CPUのコア数分リストを分割して渡しておくことで、検索ごとに分割処理しなくて済むようになった
2020-10-13 08:52:51
backgroundでページを読み込むようにした
2020-10-12 01:12:00
ページ読み込み操作から then を減らした
可読性の向上
2020-09-30 19:36:45
LinkObserverを導入した
2020-09-17 09:10:50
emoji-completionと共通する部分をICompletionとして分離した。
2020-09-17 05:17:45
入力補完候補が自分のprojectの場合は、APIではなく/scrapboxlab/scrapbox.Project.pagesから取得するようにした
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
emoji-completionの処理をそのままコピペしていたのが原因
2020-09-05 22:20:57
/ の処理を変更
に変換してfizzSearchに渡すようにした
これで以前よりもうまく補完できるようになったはずtakker
2020-09-05 22:09:37
デバッグ用文字列が emojis になっていたのを links に直した
ページタイトルが入力候補に出てこなかったのを修正
2020-09-03 08:47:05 includeYourProject false にすると、 projects からも自分のprojectを除外するようにした
これにより、単一のproject読み込みリストを自分が所属している全てのprojectで使い回すことが出来るようになった
project読み込みリストに自分が所属しているprojectを片っ端から登録しておく
/takkerで使うときは/takkerを補完候補から除外し、/villagepumpで使うときは/villagepumpを除外する、なんてことが自動でできるようになった
要再読み込み
2020-09-03 08:22:43 includeYourProject のdefault値を false にした
てかなんで true だったんだ……?takker
自分のプロジェクトへのリンクを外部リンク形式にするなんていう需要は少数派だろう
ないとは言わない
2020-09-01 09:24:28 サロゲートペアに対応した?
完全には対応できなかった
2020-08-28 12:30:28 recyclerefactoring
singletonWorkerを採用した
2020-08-27 20:57:42
二重起動しないようにした
2020-08-27 06:58:18
WebWorkerを導入して処理を軽くした
projectからのページ読み込みを非同期化して、読み込み途中でも入力補完できるようにした
emoji-completionの改善点を移植した
2020-08-26 23:54:04
StringObserver.getLinkIncludingCursorが返すものを対象の文字列に限定する
文字番号が必要なくなった

既知の問題
なかなか検索結果が出てこない時がある
検索に時間がかかりすぎている
一定時間経ったら、検索を中断するようにしたい
done一部プロジェクトで何故か機能しない
補完がなんか変
fizzSearchの正規表現をダンプして確認したみたほうがいいかも
done補完が開始するまで少し待つ必要がある
外部プロジェクトのページ情報をfetchしている
done補完windowの表示でややもたつくかも
何かキーを入力しないと出てこなかったり
WebWorkerを使って解決する
done [/全角文字] はdefaultの入力補完windowが表示される場合もある
↑通常のリンク扱いになるため
出ても特に問題はない
項目が一つだけになったときの自動入力がなんかおかしい
別な項目が挿入される事がある?
2020-09-17 09:34:51 再現不能
done変にクリックするとwindowが残る?
doneScrapbox標準のpopup menuが出ていると、置換に失敗する?
doneemoji-selectorと干渉する
候補選択のキー入力でぶつかる
2020/8/26 02:09 修正済み
doneリストの下の方のprojectが補完候補に出てこない
例えば KJ法 で検索すると、/rashitamemoくらいまでは出てくるが、それ以降が出てこない
対策
全てのprojectから一つづつとってきて並べる
同じprojectのページは思考が似通っているから、出てきても/motoso/Serendipityがあまりない
似たようなページはprojectのリンク先から出てくる
入力候補を絞る前のsourceをシャッフルする
これが一番簡単?
projectを絞りたければ、そのproject nameを入力すればいい
簡単に実装できそう
2020/8/26 14:57 実装してみた
いい感じ?
KJ法 の場合はやはり/nishioのページがたくさん出てくる
単純にページ数が多い
ただその中にはちゃんと別のproject pageもちらほら出てくるようになった
それなりに/motoso/Serendipityは上がったと思う。
追加機能とか
done読み込むproject listをproject間で共通させたい
これはUserScriptで適当に組めばいけると思う
適当なページを作るかして、そこに/shokai/コードブロック記法で書き込む
それをfetchで読み込む
import-old#5f43aa651280f00000b38464を参考にするいいかも

以下、本体のscript
hr

以下をimportする
script.js
import {ICompletion} from '/api/code/takker/ICompletion/script.js';

メイン関数
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) を消すのを忘れていた
emoji-completionでは必要だが、本scriptでは不要
WebWorkerからはmain threadのobjectを使用できないので、 scrapbox.Project などにアクセスできない
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}) }); }
入力候補を更新する関数
WebWorkerの処理が終わるたびに実行される
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({ elements: [div], onComfirm: () => { this._log(`clicked [${link}]`); this._comfirm(cursor, `[${link}]`);}, }); }); }

WebWorkerに検索を依頼する
script.js
_postMessage(word) { this.searchWorker.postMessage({word: word, list: this.links, maxSuggestionNum: this.maxSuggestionNum}); } }

WebWorkerが処理するscript

入力補完候補をfetchする
fetchも並列処理用workerに任せてもいいと思う
仕事の分担が明確になる
searchWorker : 仕事の割り振りと、結果の統合
compute : 割り振られた仕事を実行する
listを本体で持つ必要ないな。
webworkerに持たせよう。

searchWorker.js
const range = n => [...Array(n).keys()]; // [0,1,...,n-1]を作る函数 // list: 補完候補が入ったリスト const workerNum = navigator.hardwareConcurrency; const workers = range(workerNum).map(_ => new Worker( '/api/code/takker/external-completion/compute.js'));
backgroundでfizzSearchを行う
並列で検索を行う
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 => linksData[i] ?? [])); _log(`Found ${matchedLinksData.length} links: %o`, matchedLinksData); self.postMessage(matchedLinksData.slice(0, message.data.maxSuggestionNum) .map(data => data.link)); }); }

外部projectからリンク候補を読み込む
searchWorker.js
async function addLinks(projects) { _log(`start loading projects...`); const externalLinks = await Promise.all(projects .flatMap(project => fetchExternalLinks(project))) .then(lists => lists.flat()

全ページを読み込み終わったら、入力候補をシャッフルして/motoso/Serendipityを上げる
シャッフルしたあとに、page titleの長さ順に並び替えてもよさそう
近い順かつタイトルが短い順に並べられる
2020-11-26 07:53:34 Most linked順にする
計算workerの方でもlinkedを使うので、dataにlinkedを含めて渡す
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); workers[i].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(); }); temp.push(...json.flatMap(page => [...page.links, page.title])); relations.push(...json); } while(followingId); // linkedを計算する前に重複を除いておく temp = [...new Set(temp)]; _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に応じて、読み込みを行うか検索を行うかを切り替える
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してあるのだから、別にいらない処理だったような……
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...`); list = [...list, ...message.data.list] // 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); }

#2020-12-02 22:50:59
#2020-11-19 08:32:00
#2020-11-12 03:41:32
#2020-10-16 04:43:54