generated at
ScrapVim-lite
だいぶ肥大化してきたので一旦整理し直した
このversionはmobile版scrapboxでコケるので非推奨
hr
突貫工事ともいうtakker


細かいことは考えず、 switch とかで雑に実装する
とりあえず動くものを作ることを目標にする
実装しないもの
status bar
めんどい
operator
diw とかを独立したcomamndとして実装してしまう
つかうやつ
preventDefault stopPropagation を挟む必要がある。
自前で作り直すか

実装したいこと
yank & paste
visual mode
行へ直接jumpする
行の click() では移動できない
<div> にclick eventがない
自前でMouseEventを発行してみる
対象は.linesのparentNode
click eventが設定されているから行けるはず
直接文字のspanを押せばいいみたい
これで確認した
js
const lineParent=document.getElementsByClassName('lines')[0].parentNode; lineParent.addEventListener('click',e=>console.log(e));
先に対象行が見えるところまでscrollする必要がある
js
element.dispatchEvent(new MouseEvent("click", { button: 0, clientX: element.getBoundingClientRect().left, clientY: element.getBoundingClientRect().top, bubbles: true, cancelable: true, view: window }));
自前で発行してもだめだった
仕方ないのでkeyboard emulationだけでなんとかする
2020-11-24 00:19:29 できた!!!!!!
js
// 画面を移動する window.scroll(0,0); const headChar = scrapVim.lines.firstElementChild.getElementsByClassName('c-0')[0]; const rect = headChar.getBoundingClientRect(); // 真ん中らへんを押す const clickPoint = { clientX: rect.left + rect.width / 2, clientY: rect.top + rect.height / 2, }; headChar.dispatchEvent(new MouseEvent("mousedown", { button: 0, ...clickPoint, bubbles: true, cancelable: true, view: window }));
PageDown で移動できる行の数
一回飛んだときに計測しておく
IMEの無効化
eventからIMEのon/offを監視
IMEがon & normal modeのときは、IMEをcancelする

2020-11-24
15:29:32 G gg の内部処理を変えた
これで G は正常に動くようになった
またcommand入力から実行までのタイムラグがなくなった
01:16:28 scrapbox-cursor-jumperを使ってcursorを移動するようにした
まず G gg に適用した
単語移動にも適用するかは悩む
選択範囲と併用しにくい?
MouseEventには shiftkey を渡せるので心配いらない
01:44:31 G がうまく動かない
不完全にscrollだけされる
cursorが動かない
後日調べる
2020-11-23
06:25:59 C を実装した
2020-11-22
21:19:04 pP を実装した。あと dd に切り取り機能をつけた
15:28:45 dd を直した
02:34:32
webge の実装を変更した
自前で単語分割して移動するように変更した
これで正しくjumpできるようになった
^ を非空白文字にjumpするように修正した
2020-11-21
20:42:03 Normal modeで <C-v> でpasteできるようにした
Firefoxでclipboardから独自のcommandでpasteすることは不可能なので、こうするしかない
16:59:39 Normal modeでtimestampとアイコン挿入できるようにした
16:56:24 O を押した後Insert modeに移行する処理をいれ忘れていたので入れた
12:33:18 Insert mode以外でIME入力を握りつぶすようにした
2020-11-17 19:56:20 webge を追加した
w ge は不完全
2020-11-17 18:07:58 <C-o> <C-]> を追加した
C-]> はcursorがなくても使えるみたい
2020-11-17 17:35:02 ^$ を追加した
2020-11-17 17:20:13 aIA を追加した
2020-11-17 17:07:16 とりあえず動いている

code
初期化とか
script.js
import {KeyStack} from '/api/code/takker/ScrapVim-lite/logger.js'; //import {emulateKeys} from '/api/code/takker/ScrapVim-lite/emulator.js'; import {KeyboardEmulator} from '/api/code/takker/scrapbox-keyboard-emulation/script.js'; import {getLinkIncludingCursor} from '/api/code/takker/Scrapboxでcursor下のリンクを取得する/script.js'; import {getCursorInfo} from '/api/code/takker/scrapbox-cursor-position/getCursorInfo.js'; import {Register} from '/api/code/takker/ScrapVim-lite/register.js'; import {jumpToLF} from '/api/code/takker/scrapbox-cursor-jumper/script.js'; const keyStack = new KeyStack({isBubble: false}); const emulator = new KeyboardEmulator(); let scrapVim = { statusBar: createKeyViewer(), mode: 'normal', state: '', // delete: delete operator起動, count: 繰り返し数を指定中 editor: document.getElementById('editor'), lines: document.getElementById('editor').getElementsByClassName('lines')?.[0], cursor: document.getElementById('text-input'), cursorBar: document.getElementsByClassName('cursor')?.[0], cursorLine: () => scrapVim.lines.getElementsByClassName('cursor-line')?.[0], register: new Register(), }; moveNormalMode();

cursorをmodeに応じて変える
script.js
const cssId = 'scrapbox-normal-mode'; let style = document.getElementById(cssId); style?.remove() document.head.insertAdjacentHTML('beforeend',` <style id="${cssId}"> @import "/api/code/takker/ScrapVim-lite/cursor.css"; </style> `);

cursor用css
cursor.css
.cursor.normal-mode { width: 8px !important; background-color: rgb(57, 172, 134); /* 点滅を無効にする */ animation: none; } /* 黒い線が気になるのでカーソルと同じ色にする */ .cursor.normal-mode {f color: rgb(57, 172, 134); border-color: rgb(57, 172, 134); } .cursor.normal-mode svg { display: none; }

script.js
keyStack.onstackupdate = e => { // 今回は1文字ごとの入力しか考えない const newKey = e.detail.newKeys[0]; const keys = e.detail.stack; if (e.detail.newKeys.length > 1) _log('Mutiple new keys were passed!'); if (scrapVim.mode === 'normal') { switch (newKey) {

基本的な移動
script.js
case 'h': emulator.press('ArrowLeft'); keyStack.flush(); return; case 'j': emulator.press('ArrowDown'); keyStack.flush(); return; case 'k': emulator.press('ArrowUp'); keyStack.flush(); return; case 'l': emulator.press('ArrowRight'); keyStack.flush(); return;

単語移動
Shift + Ctrl +矢印キーをうまく使えば単語移動できそう
script.js
case 'e': if (keys[0] === 'g') { backWordEnd(); } else { goWordEnd(); } keyStack.flush(); return; case 'b': backWordHead(); keyStack.flush(); return; case 'w': goWordHead(); keyStack.flush(); return; case 'g': if (keys[1] === 'g') { goHeadLine(); keyStack.flush(); } break; case 'G': goTailLine(); keyStack.flush(); break;

行頭行末
^ は不完全
非空白文字で止まってくれない
script.js
case '^': case 'H': // takker用cutomize jumpHead() keyStack.flush(); return; case '0': jumpHeadWithSpaces() keyStack.flush(); return; case '$': case 'L': // takker用cutomize emulator.press('End'); keyStack.flush(); return;

Insert modeに移行
script.js
case 'i': moveInsertMode(); keyStack.flush(); return; case 'a': emulator.press('ArrowRight'); moveInsertMode(); keyStack.flush(); return; case 'I': emulator.press('Home'); moveInsertMode(); keyStack.flush(); return; case 'A': emulator.press('End'); moveInsertMode(); keyStack.flush(); return;

新しい行を作る
indentは維持する
script.js
case 'o': emulator.press('End'); emulator.press('Enter'); moveInsertMode(); keyStack.flush(); break; case 'O': emulator.press('ArrowUp'); emulator.press('End'); emulator.press('Enter'); moveInsertMode(); keyStack.flush(); break;
貼り付けcommand
script.js
case 'p': pasteBefore(); keyStack.flush(); break; case 'P': pasteAfter(); keyStack.flush(); break;
削除command
registerを実装でき次第、切り取りに変える
script.js
case 'D': emulator.press('End',{shiftKey: true}); emulator.press('Delete'); // 何故か改行が消えるので開業する //emulator.press('Enter'); keyStack.flush(); break; case 'd': if (scrapVim.state === 'delete') { deleteLine(); scrapVim.state = ''; keyStack.flush(); return; } scrapVim.state = 'delete';
本当はcursorを次行の同じ位置に置きたい
cursorの位置計算をする必要がある
2020-11-20 03:39:20
空行を削除したときに、次行の先頭文字を消してしまうバグがある
2020-11-22 15:28:12 なおした
script.js
break; case 'x': emulator.press('Delete'); keyStack.flush(); break;

変更command
script.js
case 'C': emulator.press('End',{shiftKey: true}); emulator.press('Delete'); moveInsertMode(); keyStack.flush(); return;
画面scroll
半分スクロールになってないや
<C-f> / <C-b> に変えたほうが良かったか
変えた
2020-11-19 07:55:46 半分スクロールはここを参考にした
script.js
case '<C-b>': emulator.press('PageUp'); keyStack.flush(); return; case '<C-f>': emulator.press('PageDown'); keyStack.flush(); return; case '<C-u>': window.scrollBy(0, -window.innerHeight / 2); scrapVim.cursor.blur(); scrapVim.cursor.focus(); keyStack.flush(); return; case '<C-d>': window.scrollBy(0, window.innerHeight / 2); scrapVim.cursor.blur(); scrapVim.cursor.focus(); keyStack.flush(); return;

undo/redo
script.js
case 'u': emulator.press('z',{ctrlKey: true}); keyStack.flush(); return; case '<C-r>': emulator.press('Z', {ctrlKey: true, shiftKey: true}); keyStack.flush(); return;

独自コマンド
script.js
// reload // 正常に動いていない /*case '<C-l>': emulator.press('F5'); keyStack.flush(); return;*/ // アウトライン編集 case '<C-h>': emulator.press('ArrowLeft',{ctrlKey: true}); keyStack.flush(); return; case '<C-j>': emulator.press('ArrowDown',{ctrlKey: true}); keyStack.flush(); return; case '<C-k>': emulator.press('ArrowUp',{ctrlKey: true}); keyStack.flush(); return; case '<C-l>': emulator.press('ArrowRight',{ctrlKey: true}); keyStack.flush(); return; case '<A-h>': emulator.press('ArrowLeft',{altKey: true}); keyStack.flush(); return; case '<A-j>': emulator.press('ArrowDown',{altKey: true}); keyStack.flush(); return; case '<A-k>': emulator.press('ArrowUp',{altKey: true}); keyStack.flush(); return; case '<A-l>': emulator.press('ArrowRight',{altKey: true}); keyStack.flush(); return; // timestamp入力 case '<A-t>': emulator.press('t',{altKey: true}); keyStack.flush(); return; // icon挿入 case '<C-i>': emulator.press('i',{ctrlKey: true}); keyStack.flush(); return;

リンク遷移
script.js
case '<C-o>': clickLinkUnderCursor(); keyStack.flush(); return; case '<C-]>': window.history.back(); keyStack.flush(); return;

script.js
default: keyStack.flush(); break; } } else { switch (newKey) { case '<Esc>': case '<C-[>': moveNormalMode(); break; default: break; } keyStack.flush(); return; } // 描画 scrapVim.statusBar.textContent = keys.join(''); }; keyStack.onflush = e => { // 描画 scrapVim.statusBar.textContent = ''; };

IMEの無効化
とりあえずの対応
Normal Modeのときに入力すると即座に Esc を押して握りつぶす
compositionstartで検知する
これだと r による置換などが正常に働かなくなるのでいずれ別な処理方法を考える必要あり
2020-11-21 16:40:41 これでIMEを潰すと、移行Insert modeで入力が一切できなくなるバグがある
compositionend が発火したときに、 Delete で1文字ずつ流し込まれた文字を消すしかなさそうだ
一部のキーを半角キーとして扱えると便利なんけど
例えば が入力されたら j と解釈する
になってしまうから無理だけど……
script.js_disabled
scrapVim.cursor.addEventListener('compositionstart', e => { if (scrapVim.mode === 'insert') return; // Escを押して文字を消す emulator.press('Escape'); // cursorにfocusを戻す scrapVim.cursor.focus(); });
入力できなくなるのは致命的すぎるので削った
代わりにcompositionendで泥臭く実装する
script.js
scrapVim.cursor.addEventListener('compositionend', e => { if (scrapVim.mode === 'insert') return; // 流し込まれた文字列を取得 const text = e.data; // 少し待ってから消す setTimeout(()=>{ for (const _ of text) { emulator.press('Backspace'); }},50); });

起動する
script.js
keyStack.start(); document.body.addEventListener('keydown',e=>console.log(e));

mode切り替え
script.js
function moveInsertMode() { scrapVim.mode = 'insert'; scrapVim.editor.getElementsByClassName('cursor')?.[0].classList.remove('normal-mode'); keyStack.isBubble = keymap => !['<Esc>','<C-[>'].includes(keymap); keyStack.flush(); _log(`Moved to ${scrapVim.mode} mode.`); } function moveNormalMode() { scrapVim.mode = 'normal'; scrapVim.editor.getElementsByClassName('cursor')?.[0].classList.add('normal-mode'); keyStack.isBubble = keymap => /<F\w+>|<C-v>/.test(keymap); keyStack.flush(); _log(`Moved to ${scrapVim.mode} mode.`); scrapVim.cursor.focus(); }

リンクを押す
script.js
function clickLinkUnderCursor() { _log(`Searching for the link under the cursor...`); const targetLink = getLinkIncludingCursor(); if (!targetLink) { console.log('No link found.'); return; } _log('Target link: %o', targetLink); targetLink.click(); }

Utilites
script.js
const range = n => [...Array(n).keys()];

実際のcommand実行

後方のWordsの先頭に移動する
script.js
function goWordHead({repeat = 1, visual = false} = {}) { // 現在のcursorの位置を取得 const {id, column} = getCursorInfo({lines: scrapVim.lines, cursor: scrapVim.cursorBar}); const cursorLine = scrapVim.lines.getElementsByClassName('cursor-line')[0]; //後方検索 let match = splitWords(cursorLine.textContent) .find(word => word.index > column); // 単語の先頭がこれ以上なければ、次行に進む if (!match) { //先頭に進むのは確実なので、End+→で飛ぶ _log('Go to the next line.') emulator.press('End', {shiftKey: visual}); emulator.press('ArrowRight', {shiftKey: visual}); return; } const pressNum = match.index - column; _log('the present line: %o', cursorLine); _log('the next word: %o', match); _log(`press ${pressNum} times.`); for (const _ of range(pressNum)) { emulator.press('ArrowRight', {shiftKey: visual}); } }

前方のWordsの先頭に移動する
script.js
function backWordHead({repeat = 1, visual = false} = {}) { // 現在のcursorの位置を取得 const {id, column} = getCursorInfo({lines: scrapVim.lines, cursor: scrapVim.cursorBar}); const cursorLine = scrapVim.lines.getElementsByClassName('cursor-line')?.[0]; // 前方検索 let match = splitWords(cursorLine.textContent) .filter(word => word.index < column)?.pop(); let pressNum = 0; if (!match) { // なければ前の行に入る const prevLine = cursorLine.previousElementSibling; // 先頭行だったら何もしない if (!prevLine) return; _log('splitted: %o',splitWords(prevLine.textContent)); match = splitWords(prevLine.textContent)?.pop(); pressNum = match.length + 1; } else { pressNum = column - match.index; } _log('the present line: %o', cursorLine); _log('the next word: %o', match); _log(`press ${pressNum} times.`); for (const _ of range(pressNum)) { emulator.press('ArrowLeft', {shiftKey: visual}); } }

後方のWordsの末尾に移動する
script.js
function goWordEnd({repeat = 1, visual = false} = {}) { // 現在のcursorの位置を取得 const {id, column} = getCursorInfo({lines: scrapVim.lines, cursor: scrapVim.cursorBar}); const cursorLine = scrapVim.lines.getElementsByClassName('cursor-line')?.[0]; const words = splitWords(cursorLine.textContent); //後方検索 let match = words.find(word => word.index + (word.length - 1) > column); let pressNum = 0; if (!match) { // なければ次の行に入る const nextLine = cursorLine.nextElementSibling; // 最後の行だったら何もしない if (!nextLine) return; match = splitWords(nextLine.textContent)[0]; pressNum = match.length + 1; } else { pressNum = match.index + (match.length - 1) - column; } _log('the present line: %o', cursorLine); _log('the next word: %o', match); _log(`press ${pressNum} times.`); for (const _ of range(pressNum)) { emulator.press('ArrowRight', {shiftKey: visual}); } }

前方のWordsの末尾に移動する
script.js
function backWordEnd({repeat = 1, visual = false} = {}) { // 現在のcursorの位置を取得 const {id, column} = getCursorInfo({lines: scrapVim.lines, cursor: scrapVim.cursorBar}); const cursorLine = scrapVim.lines.getElementsByClassName('cursor-line')?.[0]; const words = splitWords(cursorLine.textContent); // 前に検索 let match = words.filter(word => word.index + (word.length - 1) < column)?.pop(); // 単語の末尾が前方になければ前の行に移動する if (!match) { // 行末に移動することがわかっているので、Home + ←を使う _log('Go to the previous line.') emulator.press('Home', {shiftKey: visual}); emulator.press('ArrowLeft', {shiftKey: visual}); emulator.press('ArrowLeft', {shiftKey: visual}); return; } const pressNum = column - match.index - ( match.length -1 ); _log('the present line: %o', cursorLine); _log('the next word: %o', match); _log(`press ${pressNum} times.`); for (const _ of range(pressNum)) { emulator.press('ArrowLeft', {shiftKey: visual}); } }

行頭の非空白文字に移動
script.js
function jumpHead() { const cursorLine = scrapVim.lines.getElementsByClassName('cursor-line')?.[0]; const headSpaces = cursorLine.textContent.match(/\s+|\S+\s*/ug)?.[0]; jumpHeadWithSpaces() if (!/^\s+$/.test(headSpaces))return; const pressNum = headSpaces.length for (const _ of range(pressNum)) { emulator.press('ArrowRight'); } } function jumpHeadWithSpaces() { emulator.press('Home'); emulator.press('Home'); }

画面scroll
script.js
function scrollUpByPage () { emulator.press('PageUp'); } function scrollDownByPage () { emulator.press('PageDown'); } function goHeadLine() { /*while (true) { const {id, column} = getCursorInfo({lines: scrapVim.lines, cursor: scrapVim.cursorBar}); if (isHeadLine(id)) { // 最後までscrollする window.scroll(0,0); return; } scrollUpByPage(); }*/ jumpToLF({id: scrapVim.lines.firstElementChild.id, margin: 50}) // 画面を移動する //window.scroll(0,0); // cursorを先頭に移動する //const headLine = scrapVim.lines.firstElementChild; //jumpCursor({id: headLine.id, index: 0}); /*const headChar = scrapVim.lines.firstElementChild.getElementsByClassName('c-3')[0]; const rect = headChar.getBoundingClientRect(); // 真ん中らへんを押す const clickPoint = { clientX: rect.left + rect.width / 2, clientY: rect.top + rect.height / 2, }; headChar.dispatchEvent(new MouseEvent("mousedown", { button: 0, clientX: rect.left, clientY: rect.top, bubbles: true, cancelable: true, view: window }));*/ // なぜか.selectionsが発生するので、適当に選択範囲を作って消す // これでも選択範囲が消えなかった…… //emulator.press('ArrowRight',{shiftKey: true}); //emulator.press('ArrowRight'); //emulator.press('ArrowLeft'); // mouseupも発行したら直った /*headChar.dispatchEvent(new MouseEvent("mouseup", { button: 0, clientX: rect.left, clientY: rect.top, bubbles: true, cancelable: true, view: window }));*/ } function goTailLine() { const tailLine = scrapVim.lines.lastElementChild; jumpToLF({id: tailLine.id, margin: 50}); // 一番下の行までscroll //const {bottom} = tailLine.getBoundingClientRect(); //window.scroll(0, bottom - window.innerHeight); //jumpCursor({id: tailLine.id, index: 0}); }

行削除
script.js
function deleteLine({repeat = 1} = {}) { jumpHeadWithSpaces(); // registerにcopyする const text = `${scrapVim.cursorLine().textContent}\n`; scrapVim.register.copy(text); navigator.clipboard?.writeText(text); // clipboardにもcopyしておく for (const _ of range(repeat)) { emulator.press('End', {shiftKey: true}); emulator.press('ArrowRight', {shiftKey: true}); } emulator.press('Delete'); }

貼り付け
script.js
function pasteBefore() { const text = scrapVim.register.paste({register: '"'}); // 改行を含んでいる場合は次行に挿入する if (text.includes('\n')) { emulator.press('ArrowDown'); jumpHeadWithSpaces(); emulator.press('Enter'); emulator.press('ArrowUp'); insertText(text.replace(/\n/g,'')); return; } emulator.press('ArrowLeft'); insertText(text); } function pasteAfter() { const text = scrapVim.register.paste({register: '"'}); // 改行を含んでいる場合は次行に挿入する if (text.includes('\n')) { jumpHeadWithSpaces(); emulator.press('Enter'); emulator.press('ArrowUp'); insertText(text.replace(/\n/g,'')); return; } insertText(text); }

script.js
function insertText(text) { const isFirefox = () => { const userAgent = window.navigator.userAgent.toLowerCase(); if (userAgent.indexOf('firefox') != -1) { return true; } return false; }; if (isFirefox()) { const start = scrapVim.cursor.selectionStart; // in this case maybe 0 scrapVim.cursor.setRangeText(text); scrapVim.cursor.selectionStart = scrapVim.cursor.selectionEnd = start + text.length; const uiEvent = document.createEvent('UIEvent'); uiEvent.initEvent('input', true, false); scrapVim.cursor.dispatchEvent(uiEvent); } else { document.execCommand('insertText', false, text); } }

文字列を単語単位で区切る
使いやすいようにpropertiesを変える
script.js
function splitWords(text) { if (text === '') return [{word: '', index: 0, length: 0}]; return [...text.matchAll(/(?:\p{sc=Hira}+|[ヲ-゚]+|[ァ-ヶ]+|\p{sc=Han}+|\p{sc=Latin}+|[0-9]+|[0-9]+|.)[^\S\n]*\n*/ug)] .map(match => {return {word: match[0], index: match.index, length: match[0].length}}); }
文字列を空白文字で区切る
script.js
function splitWORDs(text) { if (text === '') return [{word: '', index: 0, length: 0}]; return [...text.matchAll(/(?:\S+|\s)[^\S\n]*\n*/ug)] .map(match => {return {word: match[0], index: match.index, length: match[0].length}}); }

行idから行番号を取得する
script.js
function getLineNumber(id) { if (isHeadLine(id)) return 0; if (isTailLine(id)) return scrapVim.lines.children.length - 1; const line = document.getElementById(id); if (!line) return undefined; return [...scrapVim.lines.children].indexOf(line); } function isHeadLine(id) { return scrapVim.lines.firstElementChild.id === id; } function isTailLine(id) { return scrapVim.lines.lastElementChild.id === id; }

register
clipboardの操作もここで行う
local storageを使ってtab同士でdataを共有できないかな?
register.js
export class Register { constructor() { this._register ={ a: '', b: '', c: '', d: '', e: '', f: '', g: '', h: '', i: '', j: '', k: '', l: '', m: '', n: '', o: '', p: '', q: '', r: '', s: '', t: '', u: '', v: '', w: '', x: '', y: '', z: '', }; this._noname = ''; // 無名レジスタ } copy(text, {register = '"'} = {}) { if (register === '"') { navigator?.clipboard?.writeText(text); } this._register[register] = text; this._noname = text; } paste({register = '"'} = {}) { if (register === '"') return this._noname; return this._register[register] ?? ''; } }

key logger
loggerからkeydown eventを分離したほうが良いな
bubbleするかしないかの判定が、command実行処理と分離してしまう

logger.js
import {isMobile} from '/api/code/takker/mobile版scrapboxの判定/script.js';
logger.js
export class KeyStack { constructor() { if (isMobile()) { this._enabled = false; return; } this._enabled = true; this._stack = []; this._editor = document.getElementById('editor'); this.onstackupdate = undefined; this.onflush = undefined; this.isBubble = undefined; } // keyの監視を開始。 start() { if (!this._enabled) return; this._editor.addEventListener('keydown', e =>{ // scriptで生成したkey eventはそのまま通す if (!e.isTrusted) return; const keymap = convertKeyCode(e.key, e); if (!this.isBubble(keymap)) { e.preventDefault(); e.stopPropagation(); } if (keymap === '') return; this.push(keymap); }); this._editor.addEventListener('stackupdate', e => this.onstackupdate(e)); this._editor.addEventListener('stackflush', e => this.onflush(e)); } stop(){} // keyをstackする // 配列を使って複数のkeysを一度に入れられる push(...keys) { this._stack.push(...keys); // キーが追加されたというeventを発火する this._editor.dispatchEvent( new CustomEvent('stackupdate', { bubbles: true, detail: { stack: [...this._stack], newKeys: [...keys] } })); } // stackの中身を出しつつ、this_stackを空っぽにする flush() { this._stack = []; this._editor.dispatchEvent( new CustomEvent('stackflush', { bubbles: true, detail: { stack: [...this._stack] } })); } }
KeyboardEvent.keyからVimのkey codeに変換する函数
printable key以外は無視
logger.js
export function convertKeyCode(key, {ctrlKey,shiftKey,altKey}) { // 文字入力の場合 if (key.length === 1 && key !== ' ') { // どれか一つのmeta keyしか有効にしない if (altKey) return `<A-${key}>`; if (ctrlKey) return `<C-${key}>`; return key; // Shift keyの情報は文字に反映されているので何もしない } // 特殊なキー const specialKeys = { Backspace: 'BS', Tab: 'Tab', Enter: 'CR', Delete: 'Del', Escape: 'Esc', ' ': 'Space', PageUp: 'PageUp', PageDown: 'PageDown', End: 'End', Home: 'Home', ArrowLeft: 'Left', ArrowUp: 'Up', ArrowRight: 'Right', ArrowDown: 'Down', F1: 'F1', F2: 'F2', F3: 'F3', F4: 'F4', F5: 'F5', F6: 'F6', F7: 'F7', F8: 'F8', F9: 'F9', F10: 'F10', F11: 'F11', F12: 'F12', }; if (specialKeys[key]) { // どれか一つのmeta keyしか有効にしない if (altKey) return `<A-${specialKeys[key]}>`; if (ctrlKey) return `<C-${specialKeys[key]}>`; if (shiftKey) return `<S-${specialKeys[key]}>`; return `<${specialKeys[key]}>`; } return ''; }

scrapbox-keyboard-emulationにVim key codeを送れるように改造する
これいらないかも
emulator.js
import {KeyboardEmulator} from '/api/code/takker/scrapbox-keyboard-emulation/script.js'; const emulator = new KeyboardEmulator(); // 特殊なキー const specialKeys = {↲ BS: 'Backspace',↲ Tab: 'Tab',↲ CR: 'Enter',↲ Del: 'Delete',↲ Esc: 'Escape',↲ Space: ' ',↲ PageUp: 'PageUp',↲ PageDown: 'PageDown', End: 'End',↲ Home: 'Home',↲ Left: 'ArrowLeft',↲ Up: 'ArrowUp',↲ Right: 'ArrowRight',↲ Down: 'ArrowDown', F1: 'F1',↲ F2: 'F2',↲ F3: 'F3',↲ F4: 'F4',↲ F5: 'F5',↲ F6: 'F6',↲ F7: 'F7',↲ F8: 'F8',↲ F9: 'F9',↲ F10: 'F10',↲ F11: 'F11',↲ F12: 'F12',↲ };↲ export function emulateKeys(keySequence) { return splitCommands(keySequence) .map(key => convertVim2Key(key)) .forEach(props => emulator.press(props.key, props.metaKeys)); } function convertVim2Key(key) { let result = { key: '', metaKeys: { shiftKey: false, ctrlKey: false, altKey: false, }, }; // <...>でなければ if (!/<[^>]+?>/.test(key)) { result.key = specialKeys[key] ?? ''; return result; } // 一旦外す const command = key.replace(/<([^>]+)>/, '$1'); if(command.startsWith('A-')) { result.MetaKeys.altKey = true; } else if(command.startsWith('C-')) { result.MetaKeys.ctrlKey = true; } else if(command.startsWith('S-')) { result.MetaKeys.shiftKey = true; } result.key = specialKeys[command.slice(2)] ?? ''; return result; } function splitCommands(keySequence) { return keySequence.match(/<[^>]+?>|./g); }

キー入力を表示するやつを作る
script.js
function createKeyViewer() { const app = document.getElementsByClassName('app')[0]; app.insertAdjacentHTML('beforeend', ` <style> @import '/api/code/takker/Stackを使ってscrapVimを作れないか/mock1.css'; </style> `); const statusBar = document.createElement('div'); statusBar.id = 'scrapvim-status-bar'; statusBar.classList.add('status-bar'); app.appendChild(statusBar); return statusBar; }

debug用
script.js
function _log(msg, ...objects){ if (objects.length > 0) { console.log(`[scrapbox-vim-bindings] ${msg}`, ...objects); return; } console.log(`[scrapbox-vim-bindings] ${msg}`); }

#2020-12-11 22:54:01
#2020-11-24 15:36:23
#2020-11-23 06:27:28
#2020-11-22 00:49:05
#2020-11-21 16:43:55
#2020-11-17 14:36:07