generated at
VimのNormal modeを実装してみる
どういうコードを書けば動くのか試してみるtakker
設計とかは全然考えていません
(最終的にはclassで設計し直すつもり)
MousetrapやnativeのAPIを使ってどうcodingすればkey bindingを実現できるかを試しています

/scrapboxlab/Emacs key bindingを借りることで、いい感じに切り取りできそう
VImなのにEmacsの力を借らざるを得ないとは……
借らなくても、もっといい感じにできるかな?

既知の問題
dd などの連続したキー入力で、最後の文字以外が入力されてしまう
別途 MousetrapOnEdit.bind('d',e=>{...}) などで1文字目を入力しないようにしてしまうと、 d が押されなかったことになってしまう
Event.StopPropagation() を削ってもうまく行かない
Mousetrapを使わずに、自前で入力されたkeyをstackするsystemを作ったほうが早いように思うtakker
/yutaro/emoji selectorのコードを参考にすれば作れそう

prototype1.js
import {installMousetrap} from '/api/code/villagepump/scrapbox-mousetrap-installer/script.js'; installMousetrap(); // 本体をinstallする const oldScript = document.getElementById('scrapbox-click-link'); oldScript?.parentNode.removeChild(oldScript); const script = document.createElement("script"); script.src = '/api/code/villagepump/Normal modeを実装してみる/script.js'; script.id = 'scrapbox-click-link'; script.type = 'module'; document.body.appendChild(script); const cssId = 'scrapbox-normal-mode'; let style = document.getElementById(cssId); //if (style){ style.remove() } style?.remove() document.head.insertAdjacentHTML('beforeend',` <style id="${cssId}"> @import "/api/code/villagepump/Normal_modeを実装してみる/cursor.css"; </style> `);
Normal modeのカーソルの形
とりあえず区別できるようにした
理想は1文字分ハイライトされること
cursor.css
.cursor.normal-mode { width: 4px !important; background-color: rgb(57, 172, 134); } /* 黒い線が気になるのでカーソルと同じ色にする */ .cursor.normal-mode {f color: rgb(57, 172, 134); border-color: rgb(57, 172, 134); } .cursor.normal-mode svg { display: none; }

script.js
import {getLinkIncludingCursor} from '/api/code/takker/Scrapboxでcursor下のリンクを取得する/script.js'; const editor = document.getElementById('editor'); const cursor = document.getElementById('text-input'); let mode = ''; moveNormalMode(); const moustrapOnEdit = new Mousetrap(editor);

yank
script.js
moustrapOnEdit.bind('y y', e =>{ _log(`${e.key} is pressed.`); if (mode !== 'normal') return; e.stopPropagation(); e.preventDefault(); const cursorLine = editor.getElementsByClassName('cursor-line')[0]; if(navigator.clipboard){ navigator.clipboard.writeText(cursorLine.innerText); } });

特殊なキーと文字を握りつぶす
script.js
moustrapOnEdit.bind(['left','up','down','right', 'ctrl+left','ctrl+up','ctrl+down','ctrl+right', 'alt+left','alt+up','alt+down','alt+right', 'del','backspace','enter',], e =>{ _log(`${e.key} is pressed.`); if (mode !== 'normal') return; _log(`${e.key} will be prevented from inserted.`); e.stopPropagation(); e.preventDefault(); }); moustrapOnEdit.bind('1234567890-\\qwertyd@[sf;:]zcvbnm,.'.split(''), e =>{ _log(`${e.key} is pressed.`); if (mode !== 'normal') return; _log(`${e.key} will be prevented from inserted.`); e.preventDefault(); });
検知できていない?
Mousetrapの対象を#text-inputに変えてみる
検知できなかった
Mousetrapのバグか?
仕方ないのでaddEventListenerで握りつぶす
ファイル名を間違えていただけだった……
scrpt.js script.js

mode切り替え
script.js
moustrapOnEdit.bind('i', e =>{ _log(`${e.key} is pressed.`); if (mode !== 'normal') return; e.stopPropagation(); e.preventDefault(); moveInsertMode(); }); moustrapOnEdit.bind('a', e =>{ _log(`${e.key} is pressed.`); if (mode !== 'normal') return; e.stopPropagation(); e.preventDefault(); simulateKeyPress('right'); moveInsertMode(); }); moustrapOnEdit.bind('I', e =>{ _log(`${e.key} is pressed.`); if (mode !== 'normal') return; e.stopPropagation(); e.preventDefault(); simulateKeyPress('home'); moveInsertMode(); }); moustrapOnEdit.bind('A', e =>{ _log(`${e.key} is pressed.`); if (mode !== 'normal') return; e.stopPropagation(); e.preventDefault(); simulateKeyPress('end'); moveInsertMode(); });
新しい行を作る
script.js
moustrapOnEdit.bind('o', e =>{ _log(`${e.key} is pressed.`); if (mode !== 'normal') return; e.stopPropagation(); e.preventDefault(); simulateKeyPress('end'); simulateKeyPress('enter'); moveInsertMode(); });


<C-[> が押されたらnormal modeに移行
script.js
moustrapOnEdit.bind(['ctrl+[','j j'], e => { e.stopPropagation(); e.preventDefault(); if (mode !== 'insert') return; moveNormalMode(); });
jj でもnormal modeに移行するようにしてみる
うまく動かない
script.js
moustrapOnEdit.bind('j j', e => { e.stopPropagation(); e.preventDefault(); if (mode !== 'insert') return; moveNormalMode(); });

cursor移動とか
script.js
moustrapOnEdit.bind('hjkl^$'.split(''), e =>{ _log(`${e.key} is pressed.`); if (mode !== 'normal') return; e.stopPropagation(); e.preventDefault(); let key = ''; switch(e.key) { case 'h': key = 'left'; break; case 'j': key = 'down'; break; case 'k': key = 'up'; break; case 'l': key = 'right'; break; case '^': key = 'home'; break; case '$': key = 'end'; break; } simulateKeyPress(key); });
script.js
moustrapOnEdit.bind('ctrl+u', e =>{ _log(`${e.key} is pressed.`); if (mode !== 'normal') return; e.stopPropagation(); e.preventDefault(); simulateKeyPress('pageup'); }); moustrapOnEdit.bind('ctrl+f', e =>{ _log(`${e.key} is pressed.`); if (mode !== 'normal') return; e.stopPropagation(); e.preventDefault(); simulateKeyPress('pagedown'); });

タイトル行へ移動
うまく動かない
script.js
moustrapOnEdit.bind('g g', e =>{ _log(`${e.key} is pressed.`); if (mode !== 'normal') return; e.stopPropagation(); e.preventDefault(); simulateKeyPress('home'); editor.getElementsByClassName('line-title')?.[0].click(); });

キー入力のemulationは、Mousetrapを使わないと無理そう
Mousetrap.triggerも動かない
KeyboardEvent keyCode をつかって指定したらうまくいった
key だと動かない
script.js
const KEY_MAP = { backspace:8, tab: 9 , enter: 13, del: 46, esc: 27, space: 32, pageup: 33, pagedown: 34, end: 35, home: 36, left: 37, up: 38, right: 39, down: 40, v: 86, z: 90, }; function simulateKeyPress(key, {shiftKey = false, ctrlKey = false, altKey = false} = {}) { if (!KEY_MAP[key]) return; cursor.dispatchEvent(new KeyboardEvent('keydown', {bubbles: true, cancelable: true, keyCode: KEY_MAP[key], shiftKey: shiftKey,ctrlKey: ctrlKey, altKey: altKey})); }


検索
script.js
moustrapOnEdit.bind('/', e =>{ _log(`${e.key} is pressed.`); if (mode !== 'normal') return; e.stopPropagation(); e.preventDefault(); simulateKeyPress('f', {ctrlKey: true}); });

indent操作
script.js
moustrapOnEdit.bind('< <', e =>{ _log(`${e.key} is pressed.`); if (mode !== 'normal') return; e.stopPropagation(); e.preventDefault(); simulateKeyPress('home'); simulateKeyPress('tab', {shiftKey: true}); }); moustrapOnEdit.bind('> >', e =>{ _log(`${e.key} is pressed.`); if (mode !== 'normal') return; e.stopPropagation(); e.preventDefault(); simulateKeyPress('home'); simulateKeyPress('tab'); });

script.js
moustrapOnEdit.bind('ctrl+j', e =>{ _log(`${e.key} is pressed.`); if (mode !== 'normal') return; e.stopPropagation(); e.preventDefault(); simulateKeyPress('down', {ctrlKey: true}); }); moustrapOnEdit.bind('ctrl+k', e =>{ _log(`${e.key} is pressed.`); if (mode !== 'normal') return; e.stopPropagation(); e.preventDefault(); simulateKeyPress('up', {ctrlKey: true}); }); moustrapOnEdit.bind('alt+h', e =>{ _log(`${e.key} is pressed.`); if (mode !== 'normal') return; e.stopPropagation(); e.preventDefault(); simulateKeyPress('left', {altKey: true}); }); moustrapOnEdit.bind('alt+j', e =>{ _log(`${e.key} is pressed.`); if (mode !== 'normal') return; e.stopPropagation(); e.preventDefault(); simulateKeyPress('down', {altKey: true}); }); moustrapOnEdit.bind('alt+k', e =>{ _log(`${e.key} is pressed.`); if (mode !== 'normal') return; e.stopPropagation(); e.preventDefault(); simulateKeyPress('up', {altKey: true}); }); moustrapOnEdit.bind('alt+l', e =>{ _log(`${e.key} is pressed.`); if (mode !== 'normal') return; e.stopPropagation(); e.preventDefault(); simulateKeyPress('right', {altKey: true}); });



切り取り
うまく動かない
script.js
moustrapOnEdit.bind('d d', e =>{ _log(`${e.key} is pressed.`); if (mode !== 'normal') return; e.stopPropagation(); e.preventDefault(); const cursorLine = editor.getElementsByClassName('cursor-line')[0]; const text = cursorLine.innerText.trim(); _log(`innerText: ${text}`); if(navigator.clipboard){ navigator.clipboard.writeText(text); } simulateKeyPress('home'); simulateKeyPress('home'); deleteText(text); simulateKeyPress('backspace'); simulateKeyPress('home'); simulateKeyPress('home'); }); function deleteText(text) { for(const _ of text.match(/./ug)) { simulateKeyPress('del'); } } moustrapOnEdit.bind('x', e =>{ _log(`${e.key} is pressed.`); if (mode !== 'normal') return; e.stopPropagation(); e.preventDefault(); simulateKeyPress('del'); });

貼り付け
firefoxではnavigator.clipboard.readText()を使えない
代わりに ctrl + V を実行する
↑これもうまく動かない
script.js
moustrapOnEdit.bind('p', e =>{ _log(`${e.key} is pressed.`); if (mode !== 'normal') return; e.stopPropagation(); e.preventDefault(); simulateKeyPress('v', {ctrlKey: true}); });



戻る
script.js
moustrapOnEdit.bind('u', e =>{ _log(`${e.key} is pressed.`); if (mode !== 'normal') return; e.stopPropagation(); e.preventDefault(); simulateKeyPress('z', {ctrlKey:true}); });

IMEを握りつぶす
script.js_disabled
cursor.addEventListener('compositionend', e =>{ console.log('End composition: %o', e.data); if (!e.data) return; if (mode !== 'normal') return; deleteText(e.data); });
うまく行かなかった
IMEの入力をすると Esc で握りつぶすようにする
script.js
cursor.addEventListener('compositionstart', e =>{ _log('IME can\'t be used in the Normal mode'); if (mode !== 'normal') return; simulateKeyPress('esc'); });
esc が反応しない?
別の手段を使う必要がありそう

リンクを押す
script.js
moustrapOnEdit.bind('ctrl+]', e =>{ _log(`${e.key} is pressed.`); e.stopPropagation(); e.preventDefault(); if (mode !== 'normal') return; _log(`Searching for the link under the cursor...`); const targetLink = getLinkIncludingCursor(); if (!targetLink) { _log('No link found.'); return; } _log('Target link: %o', targetLink); targetLink.click(); });

これ動かない
this._handleKey is undefined になってしまう
script.js_disabled
var originalHandleKey = moustrapOnEdit.handleKey; moustrapOnEdit.handleKey = (character, modifiers, e) =>{ _log(`The current mode is ${mode} mode.`); // 文字入力を握りつぶす if (character.match(/^.$/) && mode === 'normal') { e.stopPropagation(); e.preventDefault(); } if (['del','backspace'].includes(character) && mode === 'normal') { e.stopPropagation(); e.preventDefault(); } return originalHandleKey(character, modifiers, e); };

script.js
function moveInsertMode() { mode = 'insert'; editor.getElementsByClassName('cursor')?.[0].classList.remove('normal-mode'); _log(`Moved to ${mode} mode.`); } function moveNormalMode() { mode = 'normal'; editor.getElementsByClassName('cursor')?.[0].classList.add('normal-mode'); _log(`Moved to ${mode} mode.`); } function _log(msg, ...objects){ if (objects.length > 0) console.log(`[scrapbox-vim-bindings] ${msg}`, objects); console.log(`[scrapbox-vim-bindings] ${msg}`); }

script.js
function isFirefox() { const userAgent = window.navigator.userAgent.toLowerCase(); return userAgent.indexOf('firefox') != -1; }