ScrapVim-lite
だいぶ肥大化してきたので一旦整理し直した
このversionはmobile版scrapboxでコケるので非推奨
/icons/hr.icon
細かいことは考えず、switchとかで雑に実装する
とりあえず動くものを作ることを目標にする
実装しないもの
status bar
めんどい
operator
diwとかを独立したcomamndとして実装してしまう
つかうやつ
preventDefaultやstopPropagationを挟む必要がある。
自前で作り直すか
実装したいこと
yank & paste
visual mode
行へ直接jumpする
行のclick()では移動できない
<div>にclick eventがない
対象は.linesのparentNode
clickeventが設定されているから行けるはず
直接文字のspanを押せばいいみたい
これで確認した
code:js
const lineParent=document.getElementsByClassName('lines')0.parentNode; lineParent.addEventListener('click',e=>console.log(e));
先に対象行が見えるところまでscrollする必要がある
code: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 できた!!!!!!
code: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入力から実行までのタイムラグがなくなった
まずGとggに適用した
単語移動にも適用するかは悩む
選択範囲と併用しにくい?
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
初期化とか
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に応じて変える
code: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
code: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;
}
code:script.js
keyStack.onstackupdate = e => {
// 今回は1文字ごとの入力しか考えない
const newKey = e.detail.newKeys0; const keys = e.detail.stack;
if (e.detail.newKeys.length > 1) _log('Mutiple new keys were passed!');
if (scrapVim.mode === 'normal') {
switch (newKey) {
基本的な移動
code: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+矢印キーをうまく使えば単語移動できそう
code:script.js
case 'e':
backWordEnd();
} else {
goWordEnd();
}
keyStack.flush();
return;
case 'b':
backWordHead();
keyStack.flush();
return;
case 'w':
goWordHead();
keyStack.flush();
return;
case 'g':
goHeadLine();
keyStack.flush();
}
break;
case 'G':
goTailLine();
keyStack.flush();
break;
行頭行末
^は不完全
非空白文字で止まってくれない
code: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に移行
code: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は維持する
code: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
code:script.js
case 'p':
pasteBefore();
keyStack.flush();
break;
case 'P':
pasteAfter();
keyStack.flush();
break;
削除command
registerを実装でき次第、切り取りに変える
code: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 なおした
code:script.js
break;
case 'x':
emulator.press('Delete');
keyStack.flush();
break;
変更command
code: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 半分スクロールはここを参考にした code: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
code: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;
独自コマンド
code: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;
リンク遷移
code:script.js
case '<C-o>':
clickLinkUnderCursor();
keyStack.flush();
return;
case '<C-]>':
window.history.back();
keyStack.flush();
return;
code: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を押して握りつぶす
これだとrによる置換などが正常に働かなくなるのでいずれ別な処理方法を考える必要あり
2020-11-21 16:40:41 これでIMEを潰すと、移行Insert modeで入力が一切できなくなるバグがある
compositionendが発火したときに、Deleteで1文字ずつ流し込まれた文字を消すしかなさそうだ
一部のキーを半角キーとして扱えると便利なんけど
例えばjが入力されたらjと解釈する
aはあになってしまうから無理だけど……
code:script.js_disabled
scrapVim.cursor.addEventListener('compositionstart', e => {
if (scrapVim.mode === 'insert') return;
// Escを押して文字を消す
emulator.press('Escape');
// cursorにfocusを戻す
scrapVim.cursor.focus();
});
入力できなくなるのは致命的すぎるので削った
code: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);
});
起動する
code:script.js
keyStack.start();
document.body.addEventListener('keydown',e=>console.log(e));
mode切り替え
code: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();
}
リンクを押す
code: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
code:script.js
実際のcommand実行
後方のWordsの先頭に移動する
code: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の先頭に移動する
code: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の末尾に移動する
code: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の末尾に移動する
code: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});
}
}
行頭の非空白文字に移動
code: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
code: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});
}
行削除
code: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');
}
貼り付け
code: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);
}
code: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を変える
code:script.js
function splitWords(text) {
return [...text.matchAll(/(?:\p{sc=Hira}+|ヲ-゚+|ァ-ヶ+|\p{sc=Han}+|\p{sc=Latin}+|0-9+|0-9+|.)^\S\n*\n*/ug)] .map(match => {return {word: match0, index: match.index, length: match0.length}}); }
文字列を空白文字で区切る
code:script.js
function splitWORDs(text) {
return [...text.matchAll(/(?:\S+|\s)^\S\n*\n*/ug)] .map(match => {return {word: match0, index: match.index, length: match0.length}}); }
行idから行番号を取得する
code: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;
}
function isHeadLine(id) {
return scrapVim.lines.firstElementChild.id === id;
}
function isTailLine(id) {
return scrapVim.lines.lastElementChild.id === id;
}
register
clipboardの操作もここで行う
code: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._noname = text;
}
paste({register = '"'} = {}) {
if (register === '"') return this._noname;
}
}
key logger
loggerからkeydown eventを分離したほうが良いな
bubbleするかしないかの判定が、command実行処理と分離してしまう
code:logger.js
import {isMobile} from '/api/code/takker/mobile版scrapboxの判定/script.js';
code: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,
}
// stackの中身を出しつつ、this_stackを空っぽにする
flush() {
this._stack = [];
this._editor.dispatchEvent(
new CustomEvent('stackflush', {
bubbles: true,
}
}
printable key以外は無視
code: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',
};
// どれか一つのmeta keyしか有効にしない
if (altKey) return <A-${specialKeys[key]}>;
if (ctrlKey) return <C-${specialKeys[key]}>;
if (shiftKey) return <S-${specialKeys[key]}>;
return <${specialKeys[key]}>;
}
return '';
}
これいらないかも
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 = specialKeyskey ?? ''; 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;
}
return result;
}
function splitCommands(keySequence) {
return keySequence.match(/<^>+?>|./g); }
キー入力を表示するやつを作る
code: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用
code:script.js
function _log(msg, ...objects){
if (objects.length > 0) {
console.log([scrapbox-vim-bindings] ${msg}, ...objects);
return;
}
console.log([scrapbox-vim-bindings] ${msg});
}