'">>'.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});
}