Paper Plane xUI(PPx)に一発でソースコード検索&表示できる「peco」改を移植した

# はじめに

もとの記事は[こちら](http://blog.kazuhooku.com/2017/03/peco.html)です。
> コマンド一発でソースコード検索＆表示できる「peco」改が凄い！
>
>> コマンド一発でインクリメンタル検索してキーワード周辺のソースコードを読めるツールが欲しいなぁって思ってたんです。

こちらが使ってみると大変便利でしたので、PPxでも使えるように移植しました。


# デモ

全てのCソースから、main関数を検索

![peco.gif](https://qiita-image-store.s3.amazonaws.com/0/51728/10b1d97f-2995-bf99-10b6-441de09a904f.gif)

こっちはPPxならではの応用で、srcディレクトリのCソースを一端検索し、そこから「m」の文字が含まれるソースの中からmain関数を検索

![peco2.gif](https://qiita-image-store.s3.amazonaws.com/0/51728/a5de40f9-9067-8c2d-ce73-ef21bca90616.gif)


# ツール構成

ここからは以前紹介した記事のを利用しています。

紹介リンク

- [スクリプト一覧(GitHub)](https://github.com/wordijp/ppx-tools)
- [Paper Plane xUI(PPx)からエントリーへのfind(Qiita)](http://qiita.com/wordijp/items/4d73c77f2993c309e246)
- [Paper Plane xUI(PPx)とVimの連携(Qiita)](http://qiita.com/wordijp/items/52fdf4e0ec4603315ed6)

## スクリプト

再掲

```js:ppxshell.js
//! script

//
// コマンド実行結果を返す(コマンド置換対応)
//

function _read(file) {
	var fs = new ActiveXObject("Scripting.FileSystemObject");
	var cwd = fs.GetParentFolderName(PPx.ScriptFullName);
	var fullpath = cwd + '\\' + file;
	if (!fs.FileExists(fullpath)) { PPx.Echo('* file not found:' + file); PPx.Quit(-1); }
	var f = fs.OpenTextFile(fullpath, 1);
	try { return f.ReadAll(); } finally { f.Close(); }
}

var util = eval(_read('./lib/util.js'));

///

// 環境変数の記号を置換する
function replaceEnvSymbol(cmd, sep, newsep) {
	while (true) {
		var m = cmd.match(new RegExp(sep + "[^ \\\/]+?" + sep));
		if (!m) break;

		var left = cmd.substring(0, m.index);
		var target = cmd.substring(m.index + sep.length, m.lastIndex - sep.length);
		var right = cmd.substring(m.lastIndex);

		cmd = left + newsep + target + newsep + right;
	}
	return cmd;
}
function escapeEnv(cmd) { return replaceEnvSymbol(cmd, '%', '_@ENV@_'); }
function restoreEnv(cmd) { return replaceEnvSymbol(cmd, '_@ENV@_', '%'); }


// 文字列を置換する
var g_escape_i = 1;
var g_escaped = {};
function escapeString(cmd) {
	while (true) {
		var m = null;
		if (!m) m = cmd.match(/'.*?[^\\]'/);
		if (!m) m = cmd.match(/".*?[^\\]"/);

		if (!m) break;

		var left = cmd.substring(0, m.index);
		var l = cmd.substring(m.index, m.index + 1);
		var target = cmd.substring(m.index + 1, m.lastIndex - 1);
		var r = cmd.substring(m.lastIndex - 1, m.lastIndex);
		var right = cmd.substring(m.lastIndex);

		target = internalCommand(target).cmd;

		var key = "__@ESCAPED{" + g_escape_i++ + "}@__";
		g_escaped[key] = l + target + r;
		cmd = left + key + right;
	}
	return cmd;
}
function restoreString(cmd) {
	var m;
	while ((m = cmd.match(/__@ESCAPED{\d+}@__/))) {
		var key = cmd.substring(m.index, m.lastIndex);
		cmd = cmd.replace(key, g_escaped[key]);
	}
	return cmd;
}


// 内部のコマンドを実行し、結果へと置き換える
function internalCommand(_cmd) {
	// 文字列を退避
	var cmd = escapeString(_cmd);

	var m, l, r;
	if ((m = cmd.match(/`.+?`/))) {
		l = '`';
		r = '`';
	} else if ((m = cmd.match(/\$\((?!.*\$\().*?\)/))) {
		l = '$(';
		r = ')';
	}

	if (!m) {
		return {
			'cmd': _cmd,
			'exec': false
		};
	}

	// 内部のコマンドを発見
	var left = cmd.substring(0, m.index);
	var target = cmd.substring(m.index + l.length, m.lastIndex - r.length);
	var right = cmd.substring(m.lastIndex);

	left = restoreString(left);
	target = restoreString(target);
	right = restoreString(right);

	var ret = internalCommand(target);

	if (ret.exec) {
		// コマンドのネスト
		cmd = left + l + ret.cmd + r + right;
	} else {
		var ret = util.execSync('cmd /c ' + target);
		var stdout = ret.StdOut.ReadAll().replace(/\r?\n/g, ' ').replace(/ $/, '');
		var stderr = ret.StdErr.ReadAll().replace(/\r?\n/g, ' ').replace(/ $/, '');
		if (ret.ExitCode != 0) {
			PPx.Echo('エラー発生!\n'
				+ 'command:' + target + "\n"
				+ 'StdOut:' + stdout + "\n"
				+ 'StdErr:' + stderr);
			PPx.Quit(-1);
		}
		cmd = left + stdout + right; // 対話モードへの対応はこっち
		//cmd = left + util.execSyncBackground(target)[0] + right; // 対話モードが不要ならこっち
	}

	return {
		'cmd': cmd,
		'exec': true
	};
}


var cmd = PPx.Arguments.item(0);
while (true) {
	// エイリアス/マクロ文字を展開、環境変数はそのまま
	while (true) {
		var exec = false;
		cmd = escapeEnv(cmd); // 環境変数をPPx.Extractの対象にならないように退避して

		if (cmd.match(/%/)) { // エイリアス/マクロ文字を展開
			cmd = PPx.Extract(cmd);
			exec = true;
		}

		cmd = restoreEnv(cmd); // 退避してた環境変数を戻す
		if (!exec) break;
	}

	var ret = internalCommand(cmd);
	cmd = ret.cmd;
	if (!ret.exec) break;
}

PPx.Result = cmd;
```

```js:entry2vimargs.js
//!script

//
// エントリーをvim用の起動引数へと変換する
//
// 従来のパスのみの他に、行番号付きのgrep検索結果にも対応
// * 行番号付きのgrep検索結果の場合
//   パス:行番号:内容 -> パス +行番号
//

function getPath(str) {
	return str.split(/\:[^\\]/)[0];
}

function getLineNumber(str) {
	var m = str.match(/:\d+:/);
	if (!m) return null;

	return parseInt(str.substring(m.index + 1, m.lastIndex - 1));
}

///

var line_path = (function() {
	var args = PPx.Arguments;
	if (('' + args) == "") {
		// キャンセル
		PPx.Quit(-1);
	}
	var arg = PPx.Arguments.item(0);
	if (arg == "") {
		PPx.Echo('arg is empty');
		PPx.Quit(-1);
	}

	return [getLineNumber(arg), getPath(arg)];
})();

var args = [];
if (line_path[0] != null) args.push('+' + line_path[0]);
args.push(line_path[1]);

PPx.Result = args.join(' ');Gs
```

```js:findargsparser.js
//! script

//
// find用の引数を展開する
// 独自オプションを使えるようにするヘルパスクリプト
//
// PPx.Arguments.item(0) : 検索オプション
//
// usage)
//   -SEPname "*.cpp;*.h" ->
//     \( -name "*.cpp" -o -name "*.h" \)
//
//   -SEPname "cc:" ->
//     -SEPname "*.H;*.c;*.h" ->
//     \( -name "*.H" -o -name "*.c" -o -name "*.h" \)
//
//   -SEPname "cc:;h:" ->
//     -SEPname "*.H;*.c;*.h;*.h;*.hpp" ->
//     \( -name "*.H" -o -name "*.c" -o -name "*.h" -o -name "*.h" -o -name "*.hpp" \)


function argsJoin(args) {
	if (args.length == 0) return "";
	if (args.length == 1) return args[0];

	return '\\( ' + args.join(' -o ') + ' \\)';
}

///

// 独自オプション内の、ファイルタイプリストを展開する
// NOTE : ripgrepの--type-listを真似ている
function preReplacePattern(pattern) {
	pattern = ';' + pattern; // 先頭をなくし、常に/\Wxxx:/で単語単位検索になるように
	// C/C++
	pattern = pattern.replace(/\Wcpp:/, ';*.C;*.H;*.cc;*.cpp;*.cxx;*.h;*.hh;*.hpp');
	pattern = pattern.replace(/\Wcc:/, ';*.H;*.c;*.h');
	pattern = pattern.replace(/\Wh:/, ';*.h;*.hpp');
	// JavaScript
	pattern = pattern.replace(/\Wjs:/, ';*.js;*.jsx;*.vue');
	// Rails
	pattern = pattern.replace(/\Wrails:/, ';*.erb;*.rb');
	pattern = pattern.replace(/\Wcss:/, ';*.css;*.scss');
	// その他
	// ...

	pattern = pattern.substring(1); // ';'を取り除く
	//PPx.Echo(pattern);

	var m = pattern.match(/\w+:/);
	if (m) {
		PPx.Echo('未実装のファイルパターンがあります\n>' + pattern.substring(m.index, m.lastIndex));
	}

	return pattern;
}

// 独自オプションをオリジナルへ置換する
function sepReplaceInternal(str, sepstr, restr, origname) {
	var m2 = sepstr.match(/"[^\"]*"/);
	if (!m2) return str;

	var pattern = sepstr.substring(m2.index + 1, m2.lastIndex - 1);
	pattern = preReplacePattern(pattern);
	var patterns = pattern.split(';');

	var origs = [];
	for (var i = 0; i < patterns.length; i++) {
		if (patterns[i] != "") origs.push(origname +' "' + patterns[i] + '"');
	}
	return str.replace(new RegExp(restr), argsJoin(origs));
}
function sepReplace(str, sepname, origname) {
	var restr = sepname + ' "[^\"]*"';
	while (true) {
		var m = str.match(new RegExp(restr));
		if (!m) break;

		var sepstr = str.substring(m.index, m.lastIndex);
		str = sepReplaceInternal(str, sepstr, restr, origname);
	}

	return str;
}

///

var result;
var arg = PPx.Arguments.item(0);
if (arg == "") {
	result = "";
} else {
	// 独自オプションの展開
	arg = sepReplace(arg, '-SEPiname', '-iname');
	arg = sepReplace(arg, '-SEPname', '-name');
	//PPx.Echo(arg);

	result = arg;
}

PPx.Result = result;
```

## PPx設定

今回のコマンドはCtrl+Pに割り当てています。

**※自身の設定が消えないように、適用前にバックアップを取るか、追加取り込みをしてください**
**※パスは自身の環境へと合わせて下さい**

```:PPX.CFG
A_exec	= {	; エイリアス
gvim	= E:\tool\editor\vim80-kaoriya-win64\gvim.exe
}

KC_main	= {	; PPcメイン窓
^P	,
	*findcmd
	*script %0\script\entry2file.js, %'ENTRY_FILE'
	*set PECO=cat %'ENTRY_FILE' | nkf -Lu -w | %'CMD' | xargs grep -nH --text -E . | nkf -Lu -w | peco
	*set SELECT=%*script(%0\script\ppxshell.js, `%'PECO'`)
	*set VIM_ARGS=%*script(%0\script\entry2vimargs.js, %'SELECT')
	%Ob %'gvim' %'VIM_ARGS'
}

_Command	= {	; ユーザコマンド・関数
findcmd	=
	*set dummy='エントリへのfind'
	*set ENTRY_FILE=%'temp'\entryfile.tmp
	*set SEARCH_LSTFILE=%'temp'\searchlistfile.tmp
	*set dummy='---検索内容の入力-------------'
	*set ARG=%"_findx entry... [オプション](-SEPname:;区切り、i付きで大小無視)"%{-SEPiname "%|*%|"%}
	*set EXTRACT_ARG=%*script(%0\script\findargparser.js, %'ARG')
	*set dummy='---コマンド内容---------------'
	*set OPT=\( -type d \( -name .git -o -name .svn \) -prune \( -wholename .git -wholename .svn \) -o -type f \)
	*set CMD_BAK=xargs -P4 -I'{}' find {} %'OPT' %'EXTRACT_ARG'
	*set CMD=findx - %'OPT' %'EXTRACT_ARG'
}
```