2008-08-26

Vimperatorでローカルなkey mappingを実現するプラグイン local_mappings.js

.

これは何?

(↓)のVimperatorでサイトごとにキーバインドを指定するコードの改良版。

ニコニコ動画を快適化するvimperator設定まとめ

http://anond.hatelabo.jp/20080803202321

これには致命的なバグがあり、グローバルなkey mappingを上書きした場合removeされたままになってしまう!

そこでスクラッチから書き直した。

設計はこんな感じ

key mappingの設定はどうやって登録するの?


_vimperatorrc内でaddSetting(array);という呼び出しをする。

すると、arrayがliberator.mappings.localMapSettingsという配列にpushされる。

ここでarrayは、

[
  regex,                     //  urlを表すregex 
  ar_1,
  ar_2,
    ...
  ar_n,
]

という形の配列。

ただし、ar_iは、

1.  [key1, action1],     //  keyは押すキー(文字列でも文字列からなる配列でも良い)、actionは関数
2.  [key2, cmd1, 1],     //  cmdはコマンド
3.  [modes, keys, desc, action, extra],

のどれかのタイプの配列。

2のタイプの配列の第3引数は、:cmdname<CR>のように、引数無しにそのままコマンドを実行するかどうかを指定する。

:cmdname<Space>のように、引数を必要とするので<CR>はユーザに入力させる場合は0を指定する。

内部動作

LocationChangeするたび以下のように動く。

  1. 前のロケーションで変えられたkey mappingを初期状態に戻す
    1. 前のロケーションでのローカルなkey mappingを除去する
    2. 前のロケーションで上書きされたグローバルマップを復帰させる
  2. 新しいロケーションでのローカルなkey mappingを付与する
    1. 配列liberator.mappings.localMapSettingsの要素のうち新しいロケーションにregexがマッチするものに対して以下の処理が行われる
    2. 上書きされようとしている(グローバルな)key mappingを退避させる。
    3. ローカルなkey mappingを付与する。


サンプルコード

_vimperatorrc

※以下の12行目の&amp;は&に変換してください。増田だとスーパーpre記法内でなぜか&を書けない件。

javascript <<EOF
(function(){
liberator.mappings.localMapSettings = [];
function addSetting(se){
	liberator.mappings.localMapSettings.push(se);
}
function $(id){
	return content.document.getElementById(id);
}
function $X(expr, index){
	var result;
	if(expr != undefined &amp;&amp; index != undefined){
		try{
			result = liberator.buffer.evaluateXPath(expr).snapshotItem(index);
		}catch(e){ liberator.echoerr('$X: ' + e) }
	}else if(index == undefined){
		try{
			result = liberator.buffer.evaluateXPath(expr);
		}catch(e){ liberator.echoerr('$X: ' + e) }
	}else{
		liberator.echoerr('invalid aruguments: $X');
	}
	if(result == null){
		liberator.echo('$X: none'); return 0;
	}else{
		return result;
	}
}
function url(url){
	if(url){ content.location.href = url }
	return content.location.href;
}

// nicovideo
var nico_general = [
	/^http:\/\/www\.nicovideo\.jp/,
	[["h"], "History", function(){ url('http://www.nicovideo.jp/history') } ],
	["cm", "MyPage", function(){ url('http://www.nicovideo.jp/my') } ],
	["cn", "New Arrival", function(){ url('http://www.nicovideo.jp/newarrival') } ],
	["cf", "Focus Search Input", function(){ $X('//*[@class="search_input"]',0).focus() } ],
];
addSetting(nico_general);
var nico_top = [
	/^http:\/\/www\.nicovideo\.jp(|\?g=\w+)/,
	[["Cm","CM"], "Music Category", function(){ url('http://www.nicovideo.jp/?g=music') } ],
	[["Cg","CG"], "Game Category", function(){ url( 'http://www.nicovideo.jp/?g=game') } ],
	[["Cs","CS"], "Science Category", function(){ url('http://www.nicovideo.jp/?g=science') } ],
	[["Cr","CR"], "R-18 Category", function(){ url('http://www.nicovideo.jp/?g=r_18') } ],
	[["Ca","CA"], "Animal Category", function(){ url('http://www.nicovideo.jp/?g=animal') } ],
	[["Cn","CN"], "Nature Category", function(){ url('http://www.nicovideo.jp/?g=nature') } ],
	[["Cl","CL"], "Lecture Category", function(){ url('http://www.nicovideo.jp/?g=lecture') } ],
];
addSetting(nico_top);
var nico_search = [
	/^http:\/\/www\.nicovideo\.jp\/(search|tag)/,
	[["cs"], "Sort Search Results", function(){ var select = $X('//form[@name="sort"]/select',0); select.focus(); } ],
];
addSetting(nico_search);
// cでコメント入力、Cでコマンド入力、sでシーク、lでボリューム調整、
// pで停止/再生、mでミュートのon/off、vでコメの表示トグル、zでズーム、Ctrl-t<e3><81><a7>"test"とアラート
var nico_player = [
	/^http:\/\/www\.nicovideo\.jp\/watch/,
	[ [1], ["p", "q"], "Pause", function(){ liberator.execute(":nicopause") }, {rhs: ':nicopause<CR>', noremap: true} ],
	[ "m", "Mute audio", "nicomute", 1],
	[ "v", "Toggle Visibility of Comments", "nicommentvisible", 1],
	[ "z", "Zoom", "nicosize", 1],
	[ "c", "Comment", "nicomment", 0],
	[ "C", "Command", "nicommand", 0],
	[ "l", "Volume", "nicovolume", 0],
	[ "s", "Seek", "nicoseek", 0],
	[ "<C-t>", "Test", function(){ alert('test') } ],
];
addSetting(nico_player);
})();
EOF

このように、一つのURLに複数の設定を被せることも出来る。


ソース

local_mappings.js

※以下の5行目の&amp;は&に変換してください。増田だとスーパーpre記法内でなぜか&を書けない件。

liberator.modules.options.add(["localmaploglevel","lmll"],
	"Define which loglevel local map logs are at",
	"number", 3,
	{
		validator: function(value){ return (value >= 0 &amp;&amp; value <= 9) }
	});
liberator.modules.mappings.storedGlobalMaps = [];
liberator.modules.mappings.curLocalMaps = [];

liberator.modules.mappings.localMapLog = function(txt){
	if(!txt)return;
	liberator.log(txt, liberator.modules.options["localmaploglevel"]);
}
// arg: 0 -> error console, 1 -> multiline output, 2 -> multiline output(detailed)
liberator.modules.mappings.listCurLocalMaps = function(arg){
	var lmaps = liberator.modules.mappings.curLocalMaps;
	var lmapstx = "現在適用されているローカルマップは\n";
	if(arg == 2){
		for(var i=0, L=lmaps.length; i<L; i++){
			let names = lmaps[i].names;
			names.forEach(function(name){
				lmapstx += name + "\t" + lmaps[i].rhs + "\n";
			});
		}
		lmapstx = lmapstx.replace(/</g,'&lt;').replace(/>/g,'&gt;');
		liberator.modules.commandline.echo(lmapstx, 'hl-Nomal', 0);
		return;
	}
	for(var i=0, L=lmaps.length; i<L; i++){
		let names = lmaps[i].names;
		names.forEach(function(name){
			lmapstx += '「'+ name + '」';
		});
	}
	if(arg == 0){
		liberator.modules.mappings.localMapLog(lmapstx);
	}else if(arg == 1){
		lmapstx = lmapstx.replace(/</g,'&lt;').replace(/>/g,'&gt;');
		liberator.echo(lmapstx);
	}else{liberator.echoerr('Invalid argument : listCurLocalMaps');}
};
liberator.modules.mappings.listStoredGlobalMaps = function(){
	var sgmtx = "現在退避されているグローバルマップは\n";
	var sgm = liberator.modules.mappings.storedGlobalMaps;
	for(var i=0; i<sgm.length; i++){
		sgmtx += sgm[i].names[0]+"\n"
			+ sgm[i].action+"\n";
	}
	liberator.modules.mappings.localMapLog(sgmtx);
};
liberator.modules.mappings.clearLocalMaps = function(){
	var logtx = '';
	for(var i=0; i<liberator.modules.mappings.curLocalMaps.length; i++){
		let names = liberator.modules.mappings.curLocalMaps[i].names;
		names.forEach(function(name){
			logtx += '「'+name+'」';
			liberator.modules.mappings.remove(1, name);
		});
	}
	liberator.modules.mappings.curLocalMaps = []
	liberator.modules.mappings.localMapLog('前のロケーションでのローカルマップ'+logtx+'が除去された');
};
liberator.modules.mappings.restoreGlobalMaps = function(){
	var logtx = '';
	for(var i=0; i<liberator.modules.mappings.storedGlobalMaps.length; i++){
		let m = liberator.modules.mappings.storedGlobalMaps[i];
		logtx += '「'+m.names[0]+'」';
		liberator.modules.mappings.addUserMap([1], m.names, m.description, m.action, {flags: m.flags, rhs: m.rhs, noremap: m.noremap});
	}
	liberator.modules.mappings.storedGlobalMaps = [];
	if(logtx)liberator.modules.mappings.localMapLog('上書きされていたグローバルマップ'+logtx+'が復帰された');
};
liberator.modules.mappings.storeGlobalMaps = function(se){
	var logtx = '';
	for(var i=1; i<se.length; i++){
		if(liberator.modules.mappings.get(1, se[i][1]) != null){
			var orig = liberator.modules.mappings.get(1, se[i][1]);
			var clone = new Map(orig.modes, orig.names, orig.description, orig.action,
					{flags: orig.flags, rhs: orig.rhs, noremap: orig.noremap});
			liberator.modules.mappings.storedGlobalMaps.push( clone );
			logtx += '「'+se[i][1]+'」';
		}
	}	
	if(logtx)liberator.modules.mappings.localMapLog('上書きされようとしているグローバルマップ'+logtx+'が退避された');
};
liberator.modules.mappings.addLocalMaps = function(se){
	var logtx = '';
	for(var i=1, L=se.length; i<L; i++){
		let ar = [], args = [], rhs, extra;
		if(se[i].length == 3){
			// ar : [ key, description, action ]
			ar = se[i];
			if(ar[0].constructor == String) ar[0] = [ ar[0] ];
			if(typeof ar[2] != 'function'){
				let logtx = 'type error @ ' + se[0] + ', ' + ar[0];
				liberator.echoerr(logtx);liberator.log(logtx, 1);continue;
			}
			extra = { rhs: ar[1] + "\n" + ar[2].toSource().replace(/^/m, "\t"), noremap: true };
			args = [ [1], ar[0], ar[1], ar[2], extra ];
		}else if(se[i].length == 4){
			// ar : [ key, description, command_name, whether_no_args ]
			ar = se[i];
			if(ar[0].constructor == String) ar[0] = [ ar[0] ];
			if(typeof ar[2] != 'string'){
				let logtx = 'type error @ ' + se[0] + ', ' + ar[0];
				liberator.echoerr(logtx);continue;
			}
			let action;
			if(ar[3]){
				action = function(){ liberator.execute( ':'+ar[2] ) };
				extra = { rhs: ':'+ar[2]+'<CR>'+"\t"+ar[1], noremap: true };
				args = [ [1], ar[0], ar[1], action, extra ];
			}else{
				action = function () liberator.modules.commandline.open(':', ar[2]+' ', liberator.modules.modes.EX);
				extra = { rhs: ':'+ar[2]+'<Space>'+"\t"+ar[1], noremap: true };
				args = [ [1], ar[0], ar[1], action, extra ];
			}
		}else if(se[i].length == 5){
			// ar : [ modes, key, description, action, extra ]
			ar = se[i];
			args = [ ar[0], ar[1], ar[2], ar[3], { rhs: ar[4].rhs, flags: ar[4].flags, noremap: ar[4].noremap } ];
			if(args[1].constructor == String) args[1] = [ args[1] ];
			args[4].rhs += "\t" + args[2];
		}else{ liberator.echoerr('Invalid argument : ' + se[i] ); continue }
		liberator.modules.mappings.addUserMap( args[0], args[1], args[2], args[3], args[4] );
		logtx += '「'+args[1]+'」';
		liberator.modules.mappings.curLocalMaps.push(new Map( args[0], args[1], args[2], args[3], args[4] ));
	}
	if(logtx)liberator.modules.mappings.localMapLog('ローカルマップ'+logtx+'が付与された');logtx = '';
};
liberator.modules.mappings.applyLocalMapSetting = function(se){
	if( !se[0].test(content.location.href) )return;
	liberator.modules.mappings.localMapLog('次の設定を開始します : ' + se[0], liberator.modules.options["localmaploglevel"]);
	// 上書きされようとしているグローバルマップを退避する
	liberator.modules.mappings.storeGlobalMaps(se);
	// ローカルマップを付与する
	liberator.modules.mappings.addLocalMaps(se);
};

liberator.modules.mappings.setLocalUserMaps = function(){
	var logtx = '';
	liberator.modules.mappings.localMapLog('ローカルマップの設定開始 @ '+ content.location.href);
	liberator.modules.mappings.listCurLocalMaps(0);
	liberator.modules.mappings.listStoredGlobalMaps();

	// ## マッピングを初期状態に戻す ##
	// ローカルマップを除去する
	liberator.modules.mappings.clearLocalMaps();
	// 上書きされていたグローバルマップを復帰する
	liberator.modules.mappings.restoreGlobalMaps();	

	// ## ローカルマップをセットする ##
	liberator.modules.mappings.localMapSettings.forEach(function(se){
		liberator.modules.mappings.applyLocalMapSetting(se)
	});

	liberator.modules.mappings.listCurLocalMaps(0);
	liberator.modules.mappings.listStoredGlobalMaps();
};
autocommands.add('LocationChange', '.*', 'js liberator.modules.mappings.setLocalUserMaps();');



更新履歴

08/10/21 nightlyバージョンのvimperator仕様変更に合わせてliberator.*** -> liberator.modules.*** とリネーム。nightlyバージョンのvimperatorを使ってない人はliberator.modulesのmodulesの部分を削除すれば動きます。

08/09/06 コマンド補完を出なくするパッチを適用(thanks to id:nokturnalmortum。local_mappings.js.diff - 地獄の猫日記 http://d.hatena.ne.jp/nokturnalmortum/20080906#1220643244)

08/08/29 liberator.mappings.listCurLocalMaps(2)で現在のkey mappingをエコー出来るようにした、addSetting()のインターフェースを柔軟化、サンプルコード改善

08/08/27 mなどextraInfo(addUserMap()の最後の引数)のあるマッピングが使えなくなるのを修正、ネーミング改善、addSetting()のインターフェースを柔軟化

08/08/26 リリース

今後の予定,既知のバグ

気づいた事や要望やアイデアあったらなんでも書いてちょうだい。

・同じURL内で共有される変数を定義できるようにする。例えば、Googleイメージ検索でj/kでtable row単位で移動できるようにするには、現在何行目にいるかを示す変数がkey mapping間で共有できると便利。同一URLでもタブが違えば異なる変数にする。どう書けばいいのか試行錯誤中。もしかして技術的に難しい?

→2008/9/18追記。どうやれば良いか分からないので放置中。誰か書いてくれると嬉しいです。

余談

local_mappings.jsのせいで_vimperatorrcが600行近くになった。

vimperatorプラグイン書いた - pekeblog!

http://d.hatena.ne.jp/pekepekesamurai/20080908/1220894045

で似たプラグイン作った人がいます。

.

記事への反応 -
  • . 8/27追記 本記事末尾のローカルなkey mappingを実現するコードを改良してプラグインにしました。 ↓ Vimperatorでローカルなkey mappingを実現するプラグイン local_mappings.js を書いた。 http://anond.h...

記事への反応(ブックマークコメント)

ログイン ユーザー登録
ようこそ ゲスト さん