Peco
PPx

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

More than 1 year has passed since last update.


はじめに

もとの記事はこちらです。


コマンド一発でソースコード検索&表示できる「peco」改が凄い!


コマンド一発でインクリメンタル検索してキーワード周辺のソースコードを読めるツールが欲しいなぁって思ってたんです。



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


デモ

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

peco.gif

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

peco2.gif


ツール構成

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

紹介リンク


スクリプト

再掲


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;



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



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'
}