VimのNormal modeを実装してみる
どういうコードを書けば動くのか試してみる
設計とかは全然考えていません
(最終的にはclassで設計し直すつもり)
MousetrapやnativeのAPIを使ってどうcodingすればkey bindingを実現できるかを試しています
借らなくても、もっといい感じにできるかな?
既知の問題
dd
などの連続したキー入力で、最後の文字以外が入力されてしまう
別途 MousetrapOnEdit.bind('d',e=>{...})
などで1文字目を入力しないようにしてしまうと、 d
が押されなかったことになってしまう
Event.StopPropagation()
を削ってもうまく行かない
Mousetrapを使わずに、自前で入力されたkeyをstackするsystemを作ったほうが早いように思う
prototype1.jsimport {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.jsimport {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.jsmoustrapOnEdit.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.jsmoustrapOnEdit.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.jsmoustrapOnEdit.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.jsmoustrapOnEdit.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.jsmoustrapOnEdit.bind(['ctrl+[','j j'], e => {
e.stopPropagation();
e.preventDefault();
if (mode !== 'insert') return;
moveNormalMode();
});
jj
でもnormal modeに移行するようにしてみる
うまく動かない
script.jsmoustrapOnEdit.bind('j j', e => {
e.stopPropagation();
e.preventDefault();
if (mode !== 'insert') return;
moveNormalMode();
});
cursor移動とか
script.jsmoustrapOnEdit.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.jsmoustrapOnEdit.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.jsmoustrapOnEdit.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();
});
key
だと動かない
script.jsconst 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.jsmoustrapOnEdit.bind('/', e =>{
_log(`${e.key} is pressed.`);
if (mode !== 'normal') return;
e.stopPropagation();
e.preventDefault();
simulateKeyPress('f', {ctrlKey: true});
});
indent操作
script.jsmoustrapOnEdit.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.jsmoustrapOnEdit.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.jsmoustrapOnEdit.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');
});
貼り付け
代わりに ctrl
+ V
を実行する
↑これもうまく動かない
script.jsmoustrapOnEdit.bind('p', e =>{
_log(`${e.key} is pressed.`);
if (mode !== 'normal') return;
e.stopPropagation();
e.preventDefault();
simulateKeyPress('v', {ctrlKey: true});
});
戻る
script.jsmoustrapOnEdit.bind('u', e =>{
_log(`${e.key} is pressed.`);
if (mode !== 'normal') return;
e.stopPropagation();
e.preventDefault();
simulateKeyPress('z', {ctrlKey:true});
});
IMEを握りつぶす
script.js_disabledcursor.addEventListener('compositionend', e =>{
console.log('End composition: %o', e.data);
if (!e.data) return;
if (mode !== 'normal') return;
deleteText(e.data);
});
うまく行かなかった
IMEの入力をすると Esc
で握りつぶすようにする
script.jscursor.addEventListener('compositionstart', e =>{
_log('IME can\'t be used in the Normal mode');
if (mode !== 'normal') return;
simulateKeyPress('esc');
});
esc
が反応しない?
別の手段を使う必要がありそう
リンクを押す
script.jsmoustrapOnEdit.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_disabledvar 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.jsfunction 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.jsfunction isFirefox() {
const userAgent = window.navigator.userAgent.toLowerCase();
return userAgent.indexOf('firefox') != -1;
}