サクラエディタでEclipse風コメントアウト

# Eclipse風のコメントアウト

範囲選択して`CTRL + /`でコメントアウト、アンコメントをトグルで行えて快適です。

サクラエディタにも欲しくなりました。

なので作りました。

(20180407追記 : よりEclipseの動作に近づけました。選択範囲が全て空行ならコメントアウト、選択範囲にコード行を含むならコメントアウト、それ以外はアンコメント、としました。）
(20170814追記 : アンコメント時に選択範囲の復元をするよう修正しました。ミスで、参照元記事の上のほうにある旧バージョンを参照してしまっていました。）

```js:commentOutEclipseLike.js
/*
 * @file   commentOutEclipseLike.js
 * @brief  選択範囲、もしくはカーソル行をコメントアウト/アンコメントするサクラエディタマクロ
 * @author cat2151
 * 
 * phodra氏作成の同様のマクロを改変したもの。
 *  http://sakura-editor.sourceforge.net/cgi-bin/cyclamen/cyclamen.cgi?log=macro&tree=c591
 * 
 * ・コメントアウト行を選択した場合はアンコメント。そうでない場合はコメントアウト。
 * ・クイックハックなのでリファクタリング不足。
 */

if(!String.prototype.trim) {
  String.prototype.trim = function () {
    return this.replace(/^\s+|\s+$/g,'');
  };
}

toggleCommentOut();

// コメントアウトをトグルで行う
// 選択時は選択範囲の先頭行の行頭、それ以外は現在行の行頭、にコメントステートメントがあればアンコメント、それ以外はコメントアウトする
function toggleCommentOut() {
    // エディタがビューモード(読み取り専用)なら終了。
    if( Editor.ExpandParameter("${R?読専$:$:$}") == "読専" ) return;

    // 拡張子別にコメントステートメントを定義
    var comment = new Array();
    comment["cpp"]  = "//"; comment["c"]    = "//"; comment["h"]    = "//"; comment["nut"]    = "//";
    comment["js"]   = "//"; comment["java"] = "//";
    comment["php"]  = "//"; comment["uws"]  = "//";
    comment["mac"]  = "//"; comment["ppa"]  = "//";
    comment["vbs"]  = "'";  comment["bas"]  = "'";  comment["frm"]  = "'";
    comment["cls"]  = "'";  comment["vb"]   = "'";
    comment["cgi"]  = "#";  comment["pl"]   = "#";  comment["pm"]   = "#";  comment["sh"]   = "#";
    comment["asm"]  = ";";  comment["ini"]  = ";";  comment["inf"]  = ";";  comment["s"]  = ";";
    comment["cnf"]  = ";";  comment["conf"] = ";";
    comment["tex"]  = "%";
    comment["cmd"]  = "REM ";
    comment["bat"]  = "REM ";
    comment["txt"]  = "　"; comment["noext"] = "　";

    // 拡張子を切り出し
    var fname = Editor.ExpandParameter("$f");
    var ext = fname.substring( fname.lastIndexOf(".") + 1);
    ext = ext==fname? "noext": ext.toLowerCase();
    // 登録されていない拡張子であれば終了。
    if( comment[ext] == null ) return;

    // 選択範囲の先頭行または、現在行を取得（コメントアウトされているかどうかの判断用）
    {
        var selmode = Editor.IsTextSelected();
        var fromLineNum, toLineNum;
        var isComment = false;
        if( selmode ){ // 選択状態
            // コメントアウトされているか判断
            isComment = !containsCodeOrContainsOnlySpace(comment, ext);
        }else{ // 選択していない
            // ロジック座標を取得
            fromLineNum = parseInt(Editor.ExpandParameter("$y"));
            // コメントアウトされているか判断
            if (GetLineStr(fromLineNum).trim().substring(0, comment[ext].length) == comment[ext]) isComment = true;
        }

        if (isComment){ // コメントアウトされている
            unComment(comment);
        } else { // コメントアウトされていない
            commentOut(comment);
        }
    }
}
 
function containsCodeOrContainsOnlySpace(comment, ext) {
  // レイアウト座標を取得
  var lay_fl = Editor.GetSelectLineFrom();
  var lay_tl = Editor.GetSelectLineTo();
  // レイアウト座標をロジック座標に変換
  //  ExpandParameterではカーソル位置しか分からないので……
  fromLineNum = Editor.LayoutToLogicLineNum( lay_fl );
  toLineNum   = Editor.LayoutToLogicLineNum( lay_tl );

  var numOfComments = 0;
  for (var i = fromLineNum; i < toLineNum; i++) {
    // 行頭空白をとりのぞき、文字列Aを得る
    var strA = GetLineStr(i).trim();
    // 文字列Aが空の場合、空行とみなし、チェック対象外とする
    if (strA == "") continue;
    if (strA.substring(0, comment[ext].length) != comment[ext]) {
      // コード行の場合、つまり文字列Aの先頭がコメントステートメント以外の場合
      return true;
    }
    numOfComments++;
  }
  // すべて空行の場合
  if (!numOfComments) return true;
  // コード行がなく、コメント行が1行以上ある場合
  return false;
}

// コメントアウト
function commentOut(comment) {
    // 拡張子を切り出し
    var fname = Editor.ExpandParameter("$f");
    var ext = fname.substring( fname.lastIndexOf(".") + 1);
    ext = ext==fname? "noext": ext.toLowerCase();
    // 登録されていない拡張子であれば終了。
    if( comment[ext] == null ) return;

    // ちらつき防止
    Editor.SetDrawSwitch(0);

    // 選択モード（この変数は復元する際にも使用する）
    var selmode = Editor.IsTextSelected();
    if( selmode ){ // 選択状態
        // レイアウト座標を取得
        var lay_fl = Editor.GetSelectLineFrom();
        var lay_tl = Editor.GetSelectLineTo();
        // レイアウト座標をロジック座標に変換
        //  選択位置のロジック座標を取得する関数はない？
        var fl = Editor.LayoutToLogicLineNum( lay_fl );
        var tl = Editor.LayoutToLogicLineNum( lay_tl );
        var fc = Editor.LineColumnToIndex( lay_fl, Editor.GetSelectColmFrom() );
        var tc = Editor.LineColumnToIndex( lay_tl, Editor.GetSelectColmTo() );
        //  行選択でマクロに入った場合、一つ上の行までにする
        var endl = tc==1? tl-1: tl;
 
        // 範囲内の行を配列としてとる
        var lines = new Array();
        var str = "";
        for( var i=fl; i<=endl; i++ ){
            str = Editor.GetLineStr(i)
            lines.push( comment[ext] + str );
        }
 
        // 必要な範囲を行選択
        Editor.MoveCursor( fl, 1, 0);
        Editor.MoveCursor( endl, str.length+1, 1);

        // 選択範囲を上書き
        Editor.InsText( lines.join("") );

        // 選択範囲を復元
        Editor.MoveCursor( fl, fc+comment[ext].length, 0);
        Editor.MoveCursor( tl, tc==1? tc: tc+comment[ext].length, selmode);
    }else{ // 選択していない
        // ロジック座標を取得
        //  選択範囲がないのでレイアウトから変換する必要はない
        var l = parseInt(Editor.ExpandParameter("$y"));
        var c = parseInt(Editor.ExpandParameter("$x"));

        // 行頭に移動
        Editor.MoveCursor( l, 1, 0);
        // コメントステートメントを挿入
        Editor.InsText(comment[ext]);

        // 位置を復元
        Editor.MoveCursor( l, c+comment[ext], selmode);
    }

    // 描画フラグを戻してから再描画
    Editor.SetDrawSwitch(1);
    Editor.ReDraw(0);
}

// アンコメント
function unComment(comment) {
    //拡張子を切り出し
    var fname = Editor.ExpandParameter("$f");
    var ext = fname.substring( fname.lastIndexOf(".") + 1);
    ext = ext==fname? "noext": ext.toLowerCase();
    // 登録されていない拡張子であれば終了。
    if( comment[ext] == null ) return;

    // ちらつき防止
    Editor.SetDrawSwitch(0);

    // 置換対象にする部分の正規表現
    //  なぜかエスケープしないといけない
    var uncmt = new RegExp("\(\^\\s\*\?\)" + comment[ext]);
    // 選択モード（この変数は復元する際にも使用する）
    var selmode = Editor.IsTextSelected();
 
    if( selmode ){ // 選択状態
        // レイアウト座標を取得
        var lay_fl = Editor.GetSelectLineFrom();
        var lay_tl = Editor.GetSelectLineTo();
        // レイアウト座標をロジック座標に変換
        //  選択位置のロジック座標を取得する関数はない？
        var fl = Editor.LayoutToLogicLineNum( lay_fl );
        var tl = Editor.LayoutToLogicLineNum( lay_tl );
        var fc = Editor.LineColumnToIndex( lay_fl, Editor.GetSelectColmFrom() );
        var tc = Editor.LineColumnToIndex( lay_tl, Editor.GetSelectColmTo() );
        //  行選択でマクロに入った場合、一つ上の行までにする
        var endl = tc==1? tl-1: tl;

        // 範囲内の行を配列としてとる
        var lines = new Array();
        var reped = new Array();
        var str = "";
        var cnt = 0;
        for( var i=fl; i<=endl; i++ ){
            str = Editor.GetLineStr(i);
            var rep = str.replace( uncmt, "$1" );
            lines.push( str );
            reped.push( rep );
            if( str == rep ) cnt++;
        }

        // 処理していない行が全体の行数に等しいときは何もしない
        if( cnt < reped.length ){
            // 必要な範囲を行選択
            Editor.MoveCursor( fl, 1, 0);
            Editor.MoveCursor( endl, str.length+1, 1);
 
            // 選択範囲を上書き
            Editor.InsText( reped.join("") );

            // 範囲復元時のX位置
            //  コメントステートメントより後ならコメントステートメントの字数分ずらす
            //  コメントステートメントの中ならコメントステートメントが現れた位置
            //  コメントステートメントより前ならそのままの位置
            var idx = lines[0].indexOf( comment[ext] );
            var gap_f =
                fc>idx+comment[ext].length? fc-comment[ext].length:
                fc>idx+1? idx+1:
                fc;
            idx = lines[lines.length-1].indexOf( comment[ext] );
            var gap_t =
                tc>idx+comment[ext].length? tc-comment[ext].length:
                tc>idx+1? idx+1:
                tc;
            // 選択範囲を復元
            Editor.MoveCursor( fl, gap_f, 0);
            Editor.MoveCursor( tl, gap_t, selmode);
        }
    }else{ // 選択していない
        // ロジック座標を取得
        //  選択範囲がないのでレイアウトから変換する必要はない
        var l = parseInt(Editor.ExpandParameter("$y"));
        var c = parseInt(Editor.ExpandParameter("$x"));
        // 対象行の文字列を取得
        var str = Editor.GetLineStr(l);
        // コメントステートメントを除去した文字列
        var rep = str.replace( uncmt, "$1" );

        if( str != rep ){
            // 行選択
            Editor.MoveCursor( l, 1, 0);
            Editor.MoveCursor( l, str.length+1, 1);

            // 置換した文字列で上書き
            Editor.InsText( rep );

            // 復元時のカーソルX位置
            //  コメントステートメントより後ならコメントステートメントの字数分ずらす
            //  コメントステートメントの中ならコメントステートメントが現れた位置
            //  コメントステートメントより前ならそのままの位置
            var idx = str.indexOf( comment[ext] );
            var gap =
                c>idx+comment[ext].length? c-comment[ext].length:
                c>idx+1? idx+1:
                c;
            // カーソル位置を復元
            Editor.MoveCursor( l, gap, selmode);
        }
    }

    // 描画フラグを戻してから再描画
    Editor.SetDrawSwitch(1);
    Editor.ReDraw(0);
}
```

[phodra氏のマクロ](http://sakura-editor.sourceforge.net/cgi-bin/cyclamen/cyclamen.cgi?log=macro&tree=c591)を元にしています。

# 関連情報
[gist サクラエディタ用マクロ Eclipse風コメントアウト](https://gist.github.com/cat2151/495854927eaffe0e50556a5738a06b59)