﻿$engine JScript
$uname textEditorExt
$dname Расширение редактора текстов
$addin global
$addin stdlib

/* ======================================================================

AUTHOR: Василий Фролов aka Палыч, palytsh@mail.ru

DATE: 02.09.2011

COMMENT: 

   1. Макросы НайтиВыделенныйТекстВниз и НайтиВыделенныйТекстВверх. 
Горячие клавиши Ctrl + Down и Ctrl + Up.

   2. Макрос КлонироватьТекст. Позволяет скопировать выделенный фрагмент 
текста без использования буфера обмена. Горячие клавиши Ctrl + D.

   3. Макросы OnPressEnterInComment, OnPressBackspaceInComment, 
OnPressDeleteInComment предназначены для более удобного редактирования 
многострочных комментариев. Вызываются неявно при нажатии соответствующих 
клавиш.

========================================================================= */

global.connectGlobals(SelfScript);

stdlib.require('TextWindow.js', SelfScript);

function getPredefinedHotkeys(predef){
    predef.setVersion(7);
    predef.add("НайтиВыделенныйТекстВниз", "Ctrl + Down");
    predef.add("НайтиВыделенныйТекстВверх", "Ctrl + Up");
    predef.add("КлонироватьТекст", "Ctrl + D");
    predef.add("OnPressEnterInComment", "Enter");
    predef.add("OnPressDeleteInComment", "Del");
    predef.add("OnPressBackspaceInComment", "BkSpace");
    predef.add("OnPressBackspaceInBracket", "BkSpace");
    predef.add("OnPressDelInBracket", "Del");
    predef.add("Преобразовать регистр: ПРОПИСНЫЕ", "Ctrl + Shift + U");
    predef.add("Преобразовать регистр: строчные", "Ctrl + U");
}

function macrosНайтиВыделенныйТекстВниз(){
    return selectNextPattern(1);
}

function macrosНайтиВыделенныйТекстВверх(){
    return selectNextPattern(-1);
}

function macrosПоменятьОперандыПрисваиванияМестами() {
    
    var w = GetTextWindow(); 
    if (!w) return false;
    
    var selText = w.GetSelectedText();
    if (selText == '') return false;
    
    var sel = w.GetSelection();
    
    var lines = StringUtils.toLines(selText);
    for(var lineNo = 0; lineNo < lines.length; lineNo++)
        lines[lineNo] = lines[lineNo].replace(/(\s*)(\S*.+?)\s*=\s*(\S*.+)\s*;/, "$1$3=$2;");
    
    w.SetSelectedText(StringUtils.fromLines(lines));
    // Восстановим исходное выделение.
    w.SetSelection(sel.beginRow, sel.beginCol, sel.endRow, sel.endCol);
    
    if (lines.length > 1)
    {
        // Если было выделено несколько строк, выровняем в них по знакам '='.
        var formatScript = addins.byUniqueName('format_script');
        if (formatScript)
        {
            formatScript.invokeMacros('ВыровнятьЗнакиРавно');
            // И снова восстановим исходное выделение.
            w.SetSelection(sel.beginRow, sel.beginCol, sel.endRow, sel.endCol);            
        }
    }
    return true;
}

SelfScript.Self['macrosПреобразовать регистр: ПРОПИСНЫЕ'] = function() {
    return processSelectedText(function(selText){ return selText.toUpperCase(); });
}

SelfScript.Self['macrosПреобразовать регистр: строчные'] = function() {
    return processSelectedText(function(selText){ return selText.toLowerCase(); });
}

SelfScript.Self['macrosВыделить текст в скобках'] = function() {

    var w = GetTextWindow(); 
    if (!w) return false;    
        
    var sel = w.GetCaretPos();
    
    var startPos = { 'row' : sel.beginRow-1, 'col' : sel.beginCol-1 };
    
    var lines = w.GetLines();    
    var foundOpeningBrace = false;
    
    while (true)
    {
        var line = lines[startPos.row];
        while (startPos.col > -1) 
        {
            if (line.charAt(startPos.col) == '(')
            {
                foundOpeningBrace = true;
                break;
            }
            startPos.col--; 
        }
        
        if (foundOpeningBrace)
            break;
        
        startPos.row--;
        if (startPos.row < 0)
            break;
            
        startPos.col = lines[startPos.row].length - 1;
    }
    
    if (!foundOpeningBrace)
        return false;
        
    /* Счетчик непарных скобок. Если встречается открывающаяся скобка - прибавляем 1, 
    если закрывающая - вычитаем 1. Когда значение становится равным 0, значит мы нашли 
    парную закрывающую скобку и поиск можно прекратить. */
    var notPairedCount = 1; 
    
    var endPos = { 'row' : startPos.row, 'col' : startPos.col + 1 };
    while (endPos.row < lines.length)
    {
        var line = lines[endPos.row];
        while (endPos.col < line.length)
        {
            switch (line.charAt(endPos.col))
            {
            case '(':
                notPairedCount++;
                break;
            case ')':
                notPairedCount--;
                break;                
            default:
                break;
            }
            
            if (!notPairedCount)
                break;
            
            endPos.col++;
        }
        
        if (!notPairedCount)
            break;
            
        endPos.row++;
    }
    
    if (notPairedCount)
    {
        DoMessageBox('Не обнаружено парной скобки для скобки в позиции (' + (startPos.row + 1) + ', ' + (startPos.col + 1) + ')!');
        w.SetSelection(startPos.row + 1, startPos.col + 1, startPos.row + 1, startPos.col + 2);
        return false;
    }
    
    /* Не забываем, что у нас нумерация строк и колонок начинается с 1, 
    а индексы элементов массива и индексы символов в строке - с нуля. */
    w.SetSelection(startPos.row + 1, startPos.col + 2, endPos.row + 1, endPos.col + 1);
    return true;
}

SelfScript.Self['macrosЗаменить табуляцию в отступах на пробелы'] = function() {
	return replaceTabsToSpacesInSelectedText();
}

function replaceTabsToSpacesInSelectedText(doNotRestoreSelection) {	
    return processSelectedText(function(selText){
        var tabSize = profileRoot.getValue("ModuleTextEditor/TabSize");
        var spaces = ''; for (var i=0; i<tabSize; i++) { spaces += ' ' };
        return selText.replace(/^((<[^>]+>|\t)+)/gm, function(match, p1, offset, s) {
            return p1.replace(/\t/g, spaces);
        });
    }, doNotRestoreSelection);
}

function selectNextPattern(dir){
    //debugger;
    var w = GetTextWindow(); //snegopat.activeTextWindow();
    if (!w) return false;
    
//debugger;    
    
    var sel = w.getSelection();
    var selText = w.selectedText();
    if (selText == '') return false;

    var row = dir < 0 ? 
        searchPatternUp(w, selText, sel.beginRow) :
        searchPatternDown(w, selText, sel.beginRow);
    
    if (!row) return false;
    var str = w.line(row);
    var col = 1 + str.indexOf(selText);
    
    w.setCaretPos(1, 1);
    w.setSelection(row, col, row, col + selText.length);
    w.setCaretPos(row, col);
    w.setSelection(row, col, row, col + selText.length);
    
    return true;
}

function searchPatternDown(doc, pattern, startRow){
    var q = 0;
    for (var i = startRow + 1; i <= doc.linesCount(); i++){
        
        if (++q > 10000) return null;
        
        var str = doc.line(i);
        var j = str.indexOf(pattern);
        if (j > -1) return i;
    }
    return null;
}

function searchPatternUp(doc, pattern, startRow){
//debugger;    
    var q = 0;
    for (var i = startRow - 1; i >= 1; i--){
        
        if (++q > 10000) return null;
        
        var str = doc.line(i);
        var j = str.indexOf(pattern);
        if (j > -1) return i;
    }
    return null;
}

function macrosКлонироватьТекст(){
    var w = GetTextWindow();//snegopat.activeTextWindow();
    if (!w) return false;
    
    var sel = w.getSelection();
    var selText = w.selectedText();
    if (selText != ''){            
        w.SetSelectedText(selText + "\n" + selText);
    }
    else{
        // Если ничего не выделено, склонируем текущую строку
        var pos = w.getCaretPos();
        w.setSelection(pos.beginRow, 1, pos.beginRow, 1 + w.line(pos.beginRow).length);
        w.SetSelectedText(w.selectedText() + "\n" + w.selectedText());
    }
    return true;
}

function macrosOnPressEnterInComment(){
    var w = GetTextWindow();//snegopat.activeTextWindow();
    if (!w) return false;
    
    var pos = w.getCaretPos();
    var str = w.line(pos.beginRow);
    if (str.match(/^\s*\/\/\s*[^\s]+/ig)) {     

        // Если курсор установлен левее символов комментария, то не обрабатываем нажатие,
        // чтобы добавлялась пустая строка, как и ожидается.
        if (pos.beginCol <= str.search('//') + 1)
            return false;

        // Это непустой комментарий - добавляем комментарий ниже 
        // с отступом как в предыдущей строке.
        var newStr = getCommentStringHeader(str);

        // Если справа от курсора есть текст, перенесем его 
        // на новую строку комментария.
        if (str.length >= pos.endCol) {
            var tail = str.substr(pos.beginCol - 1, str.length - pos.beginCol + 1);
            w.ReplaceLine(pos.beginRow, str.substr(0, pos.beginCol - 1));    // отрезаем хвост от текущей строки
            newStr = newStr + tail.replace(/^\s*/ig, "");
        }
        var newCaretRow = 1 + pos.beginRow;
        w.InsertLine(newCaretRow, newStr);
        w.setCaretPos(newCaretRow, 1 + newStr.length);
        return true;
    }
    return false;
}

function macrosOnPressDeleteInComment(){
    var w = GetTextWindow();//snegopat.activeTextWindow();
    if (!w) return false;

    if (w.selectedText() != ""){
        return false;    /// TODO: обработать этот вариант.
    }
    
    var pos = w.getCaretPos();
    
    if (pos.beginRow == w.linesCount) return false;
    
    var str = w.line(pos.beginRow);
    
    if (!isCommentString(str)) return false;
    
    if (pos.endCol > str.length){
        // Курсор стоит в самом конце строки и при нажатии 
        // Delete следующая строка должна приклеиться в конец текущей.
        var newTail = w.line(1 + pos.beginRow);
        
        if (!isCommentString(newTail)) return false;
        
        newTail = newTail.replace(/^\s*\/\/*\/\s*/ig, ""); 
        if (str.match(/[^\s]$/ig)) newTail = " " + newTail;
        
        w.ReplaceLine(pos.beginRow, str + newTail);
        w.DeleteLine(1 + pos.beginRow);
        w.setCaretPos(pos.beginRow, pos.beginCol);
        return true;
    }
    return false;
}

function macrosOnPressBackspaceInComment(){
    var w = GetTextWindow();//snegopat.activeTextWindow();
    if (!w) return false;

    if (w.selectedText() != ""){
        return false;    /// TODO: обработать этот вариант.
    }
    
    var pos = w.getCaretPos();
    if (pos.beginRow == 1) return false;    // И так уже стоим в первой строке.
    
    // Обрабатываем только если стоим  в самом начале строки и 
    // если эта и предыдущая строки являются комментариями.
    if (pos.beginCol > 1) return false;
    
    var str = w.line(pos.beginRow);
    if (!isCommentString(str)) return false;
    
    var prevStr = w.line(pos.beginRow - 1);
    if (!isCommentString(prevStr)) return false;
    
    // Курсор стоит в самом начале строки и при нажатии 
    // Backspace текущая строка должна приклеиться в конец предыдущей.
    var newTail = str.replace(/^\s*\/\/*\/\s*/ig, ""); 
    if (prevStr.match(/[^\s]$/ig)) newTail = " " + newTail;
    
    w.setSelection(pos.beginRow - 1, 1, pos.beginRow, 
        w.line(pos.beginRow).length + 1);
    w.SetSelectedText(prevStr + newTail);
    
    w.setCaretPos(pos.beginRow - 1, 1 + prevStr.length);
    
    return true;
}

function isCommentString(str){
    return null != str.match(/^\s*\/\//ig);
}

function getTextBlockOffset(str){
    var match = str.match(/^([\s]+)/ig);
    var res = !match ? "" : match[0];
    return res;
}

function getCommentStringHeader(str){
    var match = str.match(/^([\s]*\/\/*\/[\s]?)*/ig);
    var res = !match ? "" : match[0];
    return res;
}

function processSelectedText(selTextHandler, doNotRestoreSelection) {
    
    var w = GetTextWindow(); 
    if (!w) return false;    
    
    var sel = w.GetSelection();            
    var selText = w.GetSelectedText();
    
    try 
    {
        selText = selTextHandler.call(null, selText);
    }
    catch (e)
    {
       Message(e.description);
       return false;
    }        
    
    w.SetSelectedText(selText);
    
    // Восстановим исходное выделение.
    if (!doNotRestoreSelection)
        w.SetSelection(sel.beginRow, sel.beginCol, sel.endRow, sel.endCol);
    return true;
}

SelfScript.Self['macrosУстановить символ | в строке/выделении'] = function() {
   
    var w = GetTextWindow(); //Получим активное текстовое окно
    if (!w) return false;   
   
    // Проверем есть ли выделенный текс, если нет, то выделим текущую строку , в результате обработаем выделенный текст и вернем на место.
    var selText = w.GetSelectedText();
    if (selText.length==0) {
        var pos = w.getCaretPos();
        w.setSelection(pos.beginRow, 1, pos.beginRow, 1 + w.line(pos.beginRow).length);
        var text = w.GetSelectedText();
    } else {
        var text = selText;
    }
    w.SetSelectedText(setSymbolInBeginnLine(text, "| "));
    return true;
}

function setSymbolInBeginnLine(text, symbol){
    var result = "";
    var Lines = text.split('\n');
    if (Lines.length == 0 && str.indexOf(symbol) == -1){
        result = result+text.replace(/^\s*/, "$&"+""+symbol+"");
    }
    for (var i=0; i<Lines.length; i++){
        var str = Lines[i];
        if (str.indexOf(symbol) == -1)
            str = str.replace(/^\s*/, "$&"+""+symbol+"");
           
        result = result+str+(((Lines.length-1)==i)?"":"\n");
    }
    return result
}

function macrosOnPressBackspaceInBracket(){
    var w = GetTextWindow();//snegopat.activeTextWindow();
    if (!w) return false;

    if (w.selectedText() != ""){
        return false;    /// TODO: обработать этот вариант.
    }
    
    var pos = w.getCaretPos();
    
    var beginRow = pos.beginRow;
    var wordBegPos = pos.beginCol - 1;
    var line = w.GetLine(pos.beginRow);
    var curChar = line.charAt(wordBegPos);
    if (wordBegPos>0) {
        var wordBacksp = wordBegPos-1;
        if (curChar==")" && line.charAt(wordBacksp)=="(") {
            //debugger;
            w.SetSelection(pos.beginRow, wordBacksp+1, pos.beginRow, wordBegPos+2);
            w.SetSelectedText("");
            w.setCaretPos(pos.beginRow, (line.charAt(wordBegPos+1)==";")?wordBegPos+1:wordBacksp+1);
            return true;
        } else {
            if (wordBacksp>0 
                && line.charAt(wordBacksp)==")" 
                && line.charAt(wordBacksp-1)=="("){
                w.SetSelection(pos.beginRow, wordBacksp, pos.beginRow, wordBegPos+1);
                w.SetSelectedText("");
                w.setCaretPos(pos.beginRow, (line.charAt(wordBacksp+1)==";")?wordBacksp+1:wordBacksp);
                return true;
            }
        }
    }

    return false;
    
}
function macrosOnPressDelInBracket(){
    var w = GetTextWindow();//snegopat.activeTextWindow();
    if (!w) return false;

    if (w.selectedText() != ""){
        return false;    /// TODO: обработать этот вариант.
    }
    
    var pos = w.getCaretPos();
    
    var beginRow = pos.beginRow;
    var wordBegPos = pos.beginCol - 1;
    var wordAfterPos = pos.beginCol;
    var line = w.GetLine(pos.beginRow);
    if (line.length < wordAfterPos || wordBegPos-1 < 0) return false;
    
    
    var curChar = line.charAt(wordBegPos); 
    if (curChar == ")" && line.charAt(wordBegPos-1)=="(")  {
        w.SetSelection(pos.beginRow, wordBegPos, pos.beginRow, wordBegPos+2);
        w.SetSelectedText("");
        //w.setCaretPos(pos.beginRow, (line.charAt(wordBegPos+1)==";")?wordBegPos+1:wordBegPos);
        w.setCaretPos(pos.beginRow, wordBegPos);
        return true;
    } else {
            if (curChar == "(" 
            && line.charAt(wordAfterPos)==")") {
            
            w.SetSelection(pos.beginRow, wordBegPos+1, pos.beginRow, wordBegPos+3);
            w.SetSelectedText("");
            w.setCaretPos(pos.beginRow, wordBegPos+1);
            //w.setCaretPos(pos.beginRow, (line.charAt(wordAfterPos+1)==";")?wordBegPos+2:wordBegPos+1);
            return true;
        }
    }
    
    return false;
    
}


