`(Hello ,world)

ツッコミ、添削大歓迎です。いろいろ教えてください。

2010-08-10

テキストエリアでオートインデントさせてみる

テキストエディタでどうやってオートインデントしてるか知りたくてxyzzyのソースを見てみたところ、地道にやるしかないということだったので、力任せにJavaScriptで組んでみた:

テスト

ChromeFireFoxで確認。Operaだとカーソル位置がおかしくなるっぽい。一応なんとなく動いてるけど、こうなるとタブ押したときもなって欲しいし、閉じ括弧を入力したときに対応する開き括弧を示して欲しい。タブはFireFoxではフックできるぽい。閉じ括弧はChromeでは取れるぽい。

  • リスト、シンボル、数値などのみ。文字列やコメントにはまだ非対応。
var onKeyPressConsole = (function() {

// ボディ部のインデント
var $lisp_body_indent = 2;

// キーワードのインデント
var $lisp_indent_hook = {
	"if": 2,
	"when": 1,
	"define": 0,
};

// 文字コードは空白か?
var is_white = function(c) {
	switch (c) {
	case 32:	// ' '
	case 9:		// TAB
	case 10:	// \n
		return true;
	default:
		return false;
	}
};

// スペースとタブを前方に読み飛ばす
var skip_space_forward = function(text, pos) {
	var len = text.length;
	for (; pos < len; pos += 1) {
		var c = text.charCodeAt(pos);
		if (!(c == 32 || c == 9))	break;
	}
	return pos;
};

// 空白を前方に読み飛ばす
var skip_white_forward = function(text, pos) {
	for (var len = text.length; pos < len; pos += 1) {
		if (!is_white(text.charCodeAt(pos)))	break;
	}
	return pos;
};

// 空白を後ろから読み飛ばす(マイナスになる可能性もあり)
var skip_white_backward = function(text, pos) {
	while ((pos -= 1) >= 0 && is_white(text.charCodeAt(pos))) { }
	return pos;
};

// シンボルを前方に読み飛ばす
//	飛ばせたらその位置、
//	飛ばせなかったら(すでに終端だったら)false を返す
var skip_symbol_forward = function(text, pos) {
	var len = text.length;
	var p;
	for (p = pos; p < len; p += 1) {
		var c = text.charCodeAt(p);
		// デリミタ
		switch (c) {
		case 32:	// ' '
		case 9:		// TAB
		case 10:	// \n
		case 39:	// クォート
		case 34:	// ダブルクォート
		case 96:	// バッククォート
		case 44:	// カンマ
		case 59:	// セミコロン
		case 40:	// (
		case 41:	// )
			return p;
		default:
			break;
		}
	}
	return (p > pos) ? p : false;
};

// シンボルを後ろから読み飛ばす
var skip_symbol_backward = function(text, pos) {
	while (pos > 0) {
		pos -= 1;
		var c = text.charCodeAt(pos);
		// デリミタ
		switch (c) {
		case 32:	// ' '
		case 9:		// TAB
		case 10:	// \n
		case 39:	// クォート
		case 34:	// ダブルクォート
		case 96:	// バッククォート
		case 44:	// カンマ
		case 59:	// セミコロン
		case 40:	// (
		case 41:	// )
			return pos + 1;
		default:
			break;
		}
	}
	return false;
};

// 対応する ')' の位置を返す
// なければ false、
// あればその位置を返す
var goto_matched_close = function(text, pos) {
	for (;;) {
		var newpos = skip_sexp_forward(text, pos);
		if (newpos === false)	return false;
		if (newpos < 0) {
			return -newpos - 1;
		}
		pos = newpos;
	}
};

// 対応する '(' の位置を返す
// なければ false、
// あればその位置を返す
var goto_matched_open = function(text, pos) {
	for (;;) {
		var newpos = skip_sexp_backward(text, pos);
		if (newpos === false)	return false;
		if (newpos < 0) {
			return -newpos - 1;
		}
		pos = newpos;
	}
};

// S式1つを前方に読み飛ばす
// テキストの終端でそれ以上戻れなかったら false
// 飛ばせたらポイントの位置、
// ')' など、リストの終端だったら位置の補数(負)を返す
var skip_sexp_forward = function(text, pos) {
	pos = skip_white_forward(text, pos);
	if (pos == text.length)	return false;

	var c = text.charCodeAt(pos);
	switch (c) {
	case 40:	// (
		pos = goto_matched_close(text, pos + 1);
		return (pos === false) ? false : pos + 1;
	case 41:	// )
		return -pos - 1;		// 負
	default:	// シンボルだとしてスキップ
		return skip_symbol_forward(text, pos);
	}
};

// S式1つを後ろに読み飛ばす
// テキストの先頭でそれ以上戻れなかったら false
// 飛ばせたらポイントの位置、
// '(' など、リストの終端だったら位置の補数(負)を返す
var skip_sexp_backward = function(text, pos) {
	pos = skip_white_backward(text, pos);
	if (pos < 0)	return false;

	var c = text.charCodeAt(pos);
	switch (c) {
	case 41:	// )
		return goto_matched_open(text, pos);
	case 40:	// (
		return -pos - 1;		// 負
	default:	// シンボルだとしてスキップ
		return skip_symbol_backward(text, pos);
	}
};

// リストを後ろに1つ上がる
// 上がれなかったら false
// 上がれたらポイントの位置を返す
var up_list_backward = function(text, pos) {
	// todo: コメント行の処理
	for (;;) {
		var up = skip_sexp_backward(text, pos);
		if (up === false)	return false;
		if (up < 0) {
			return -up - 1;
		}
		pos = up;
	}
};

// 現在の行の行頭の位置を返す
var goto_bol = function(text, x) {
	while (x > 0) {
		x -= 1;
		if (text.charCodeAt(x) == 10)	return x + 1;
	}
	return 0;
};

// 行末か?
var eolp = function(text, pos) {
	return pos >= text.length || text.charCodeAt(pos) == 10;
};

// Lispのインデント計算
var calc_lisp_indent = function(text, pos) {
	var uppos = up_list_backward(text, pos);
	if (uppos === false)	return 0;	// トップレベルにいる:インデントは0

	var symstart = skip_space_forward(text, uppos + 1);
	if (eolp(text, symstart))	return (uppos + 1) - goto_bol(text, uppos);

	var symend = skip_sexp_forward(text, symstart);
	var first_arg_pos = skip_space_forward(text, symend);
	if (eolp(text, first_arg_pos))	return (uppos + 1) - goto_bol(text, uppos);

	var maybe_symbol = text.slice(symstart, symend);
	var n = $lisp_indent_hook[maybe_symbol];
	switch (typeof(n)) {
	case "number":
		var p, count = 0;
		for (p = first_arg_pos; p < pos && count < n; count += 1) {
			p = skip_sexp_forward(text, p);
			if (p === false)	break;
		}
		var base = uppos - goto_bol(text, uppos);
		if (count < n) {
			return base + $lisp_body_indent * 2;
		} else {
			return base + $lisp_body_indent;
		}
		break;
	default:
		return first_arg_pos - goto_bol(text, uppos);
	}
};

var move_to = function(target, pos) {
	target.focus();
	if (target.createTextRange) {
		var range = target.createTextRange();
		range.move('character', pos);
		range.select();
	} else if (target.setSelectionRange) {
		target.setSelectionRange(pos, pos);
	}
};

var lisp_indent_line = function(target, pos) {
	var text = target.value;
	var indent = calc_lisp_indent(text, pos);
	var spc = "";
	for (var i=0; i<indent; ++i)  spc += " ";
	var wordpos = skip_space_forward(text, pos);
	target.value = text.slice(0, pos) + "\n" + spc + text.slice(wordpos);
	var newpos = pos + 1 + indent;
	move_to(target, newpos);
};

var lisp_newline_and_indent = function(target) {
	target.focus();
	var pos = getAreaRange(target);
	// ひとまず、選択範囲を削除
	if (pos.start < pos.end) {
		target.value = target.value.slice(0, pos.start) + target.value.slice(pos.end);
	}
	lisp_indent_line(target, pos.start);
};

var getAreaRange = function(obj) {
	var pos = new Object();
	if (isIE) {
		obj.focus();
		var range = document.selection.createRange();
		var clone = range.duplicate();
		clone.moveToElementText(obj);
		clone.setEndPoint( 'EndToEnd', range );
		pos.start = clone.text.length - range.text.length;
		pos.end = clone.text.length - range.text.length + range.text.length;
	} else if (window.getSelection()) {
		pos.start = obj.selectionStart;
		pos.end = obj.selectionEnd;
	}
	return pos;
};
var isIE = (navigator.appName.toLowerCase().indexOf('internet explorer')+1?1:0);

return function(e) {
	e = e ? e : event;
	switch (e.keyCode) {
	case 8:		//Event.KEY_BACKSPACE:
		break;
	case 9:		//Event.KEY_TAB:
		if (e.preventDefault)  e.preventDefault();
		return false;
	case 13:	// Event.KEY_RETURN:
		lisp_newline_and_indent($('codearea'));
		return false;
	case 37:	//Event.KEY_LEFT:
		break;
	case 38:	//Event.KEY_UP:
		break;
	case 39:	//Event.KEY_RIGHT:
		break;
	case 40:	//Event.KEY_DOWN:
		break;
	case 46:	//Event.KEY_DELETE:
		break;
	default:
//		log(e.keyCode);
		break;
	}
	return true;
};

})();
トラックバック - http://cadr.g.hatena.ne.jp/mokehehe/20100810