﻿$engine JScript
$uname TextWindow
$dname Класс TextWindow
$addin global
$addin stdlib

function GetTextWindow() {
    
    var activeWnd = snegopat.activeTextWindow();
    
    if (activeWnd)
        return new _TextWindow(activeWnd, windows.getActiveView());
        
    return null;
}

////////////////////////////////////////////////////////////////////////////////////////
//// _TextWindow
////

/** Класс-обертка вокруг ITextWindow, поддерживающий одновременно 
интерфейс объектов ITextWindow, так и ТекстовыйДокумент. */
_TextWindow = stdlib.Class.extend({

    construct : function (textWindow, view) {
        this.textWindow = textWindow;
        this._view = (view && (textWindow.hwnd == view.hwnd)) ? view : undefined;
    },
    
    IsActive : function() {
        
        if (!this.textWindow)
            return false;
            
        try 
        {
            /* Окно могло быть закрыто. Тогда при обращении 
            к его свойствам произойдет ошибка. */
            var hwnd = this.textWindow.hwnd;
        }
        catch (e)
        {
            return false;
        }
        
        return true;
    },

    GetHwnd : function () {
        return this.textWindow.hwnd;
    },

    GetText : function() {
        return this.textWindow.text;
    },

    SetText : function(text) {
        this.Range(1,1,this.textWindow.linesCount).SetText(text);
    },

    ExtName : function() {
        return this.textWindow.extName;
    },

    GetCaretPos : function() {
        return this.textWindow.getCaretPos();    
    },

    SetCaretPos : function(row, col) {
        return this.textWindow.setCaretPos(row, col);    
    },

    GetSelection : function() {
        return this.textWindow.getSelection();    
    },

    SetSelection : function(beginRow, beginCol, endRow, endCol) {
        return this.textWindow.setSelection(beginRow, beginCol, endRow, endCol);    
    },

    GetSelectedText : function() {
        return this.textWindow.selectedText;
    },

    SetSelectedText : function(text) {
        this.textWindow.selectedText = text;
    },

    GetLine : function(rowNum) {
        return this.textWindow.line(rowNum);    
    },

    LinesCount : function() {
        return this.textWindow.linesCount;    
    },

    IsReadOnly : function() {
        return this.textWindow.readOnly;    
    },

    DeleteLine : function(rowNum) {
        
        if (rowNum < 1 || rowNum > this.LinesCount())
            return;

        var nextLine = this.GetLine(rowNum + 1);
        this.Range(rowNum, 1, rowNum+1, nextLine.length + 1).SetText(nextLine);
    },

    AddLine : function(strLine) {
        var linesCount = this.LinesCount();
        if (linesCount > 0)
        {
            var lastLine = this.GetLine(linesCount);
            this.Range(linesCount, 1, linesCount).SetText(lastLine + "\n" + strLine);
        }
        else 
        {
            this.Range().SetText(strLine);
        }
    },

    InsertLine : function(rowNum, strLine) {

        var linesCount = this.LinesCount();

        if (rowNum < 0 || rowNum > linesCount + 1)
            throw "_TextWindow.InsertLine(): Индекс за границами диапазона!";
            
        if (rowNum == linesCount + 1)
        {    
            this.AddLine(strLine);
        }
        else 
        {
            var curLine = this.GetLine(rowNum);
            this.Range(rowNum, 1, rowNum).SetText(strLine + "\n" + curLine);
        }
    },

    ReplaceLine : function(rowNum, strLine) {
        this.Range(rowNum, 1, rowNum).SetText(strLine);
    },

    Clear : function () {
        this.Range().SetText("");
    },

    /** RangeObject _TextWindow::Range([beginRow [,beginCol [,endRow [,endCol]]]]) */
    Range : function() {
        var tw = this.textWindow;

        /* Нумерация строк и колонок в текстовом документе - с 1. 
        Если документ пустой, то linesCount == 0, поэтому для корректной работы 
        объекта Range() приводим значения аргументов при помощи выражения (index || 1). */
        
        var beginRow = (arguments.length > 0 ? arguments[0] : 1) || 1;
        var endRow  = (arguments.length > 2 ? arguments[2] : tw.linesCount) || 1;
        
        if (beginRow > endRow)
            throw "_TextWindow: Индекс первой строки области не может быть больше индекса последней строки области!";

        var beginCol = (arguments.length > 1 ? arguments[1] : 1) || 1;
        var endCol =  (arguments.length > 3 ? arguments[3] : tw.line(endRow).length) || 1;
        
        if (beginRow == endRow && beginCol > endCol)
            throw "_TextWindow: Индекс первого символа области строки не может быть больше индекса последнего символа области!";

         // Возвращает строки области как массив.
         var getLines = function() {
            
            var lines = [];        

            /* Чтобы не ошибиться в индексах, надо помнить:
             - в строках js нумерация символов начинается с 0;
             - строки и колонки в ITextWindow нумеруются с 1;
             - в substr второй параметр - длина подстроки, которую требуется получить.*/

            ////// Область - подстрока одной строки.

            if (beginRow == endRow)
            {
                lines.push(tw.line(beginRow).substr(beginCol-1, endCol));
                return lines;
            }

            ////// Область - несколько строк.            
            
            // 1. Первая строка - от первой колонки области и до конца этой строки.
            lines.push(tw.line(beginRow).substr(beginCol - 1));
            
            // 2. Строки, начиная со второй и до предпоследней.
            for (var row=beginRow + 1; row <= endRow - 1; row++)
                lines.push(tw.line(row));

            // 3. Последняя строка - от первого символа и до последней колонки области.
            lines.push(tw.line(endRow).substr(0, endCol));

            return lines;
        };

        // Возвращает строки области в виде одной мультистроки (разделитель строк - \n).
        var getText = function() {
            return getLines().join("\n");
        };

        var setText = function(text) {        
            
            ////1. Запомнить текущую позицию курсора и выделение.        
            var curPos = tw.getCaretPos();
            var curSel = tw.getSelection();
            
            ////2. установить выделение в соответствии с координатами Range
            
            /* И снова чехарда с индексами: выделение включает символы
            вплоть до позиции каретки, т.е. если мы хотим, чтобы символ 
            в позиции endCol попал в выделение, мы каретку должны поставить
            в позицию (endCol + 1). */
            
            var _line = tw.line(endRow); if ((_line == '') || (_line.charAt(_line.length-1) == '\r')) endCol--; //ITextWindow->line() получает всё что до символа '\n', т.е если конец строки равен '\r\n' то метод вернет строку с '\r' в конце
            tw.setSelection(beginRow, beginCol, endRow, endCol+1);
            
            ////3. установить выделенный текст
            tw.selectedText = text;
            
            ////4. вернуть положение курсора в прежнюю позицию.
            tw.setSelection(curSel.beginRow, curSel.beginCol, curSel.endRow, curSel.endCol);
            tw.setCaretPos(curPos.beginRow, curPos.beginCol);
        }
        
        // Возвращаем наш псевдо - Range
        return { GetLines: getLines, GetText: getText, SetText: setText };

    },

    /** Array _TextWindow::Lines([from [,to]])*/
    GetLines : function () {

        // Если не задано ни одного параметра, то возвращаем все строки.
        // Если задан только первый параметр, то возвращается заданная строка.
        // Если заданы оба параметра, возвращаем диапазон строк.

        var beginRow, endRow;

        if (!arguments.length)
        {
            beginRow = 1;
            endRow = this.textWindow.linesCount;
        }
        else if (arguments.length == 1) 
        {
            beginRow = arguments[0];
            endRow = beginRow;
        }
        else if (arguments.length > 1)
        {
            beginRow = arguments[0];
            endRow = arguments[1];
        }
        
        return this.Range(beginRow, 1, endRow).GetLines();
    },

    /** Возвращает слово под курсором. */
    GetWordUnderCursor : function (re) {

        /*TODO: Добавить необязательный параметр: регулярное выражение для проверки символов слова. */

        var pos = this.GetCaretPos();
        var line = this.GetLine(pos.beginRow);
        var isChar = re ? re : /[\w\dА-я]/;

        var wordBegPos = pos.beginCol - 2;
        
        if (!isChar.test(line.charAt(wordBegPos)))
            return '';
            
        while (wordBegPos > 0)
        {
            if (!isChar.test(line.charAt(wordBegPos - 1)))
                break;
                
            wordBegPos--;
        }
            
        var wordEndPos = pos.beginCol - 2;
        
        while (wordEndPos < line.length - 1)
        {
            if (!isChar.test(line.charAt(wordEndPos + 1)))
                break;
                
            wordEndPos++;    
        }

        return line.substr(wordBegPos, wordEndPos - wordBegPos + 1);
    },

    /** Возвращает строковый литерал, внутри которого находится курсор. 
    FIXME: Текущая реализация некорректно отрабатывает ситуацию, когда курсор
    находится вне строкового литерала между границами двух других строковых  
    литералов (см. тест macrosTest10 в testTextWindow_GetStringUnderCursor.js). */
    GetStringUnderCursor : function () {

        var pos = this.GetCaretPos();

        var beginRow = pos.beginRow;
        var wordBegPos = pos.beginCol - 1;
        
        // Далее везде помним, что нумерация строк начинаются с 1,
        // а нумерация символов в js-строке - с 0.
        var line = this.GetLine(pos.beginRow);
        var str = '';
        
        while (true)
        {            
            if (beginRow < 1 || wordBegPos < 0)
                return null;
        
            var curChar = line.charAt(wordBegPos);
            
            /* Если встретилась двойная кавычка, то либо строка завершилась, либо
            рядом есть еще одна экранирующая двойная кавычка. */
            if (curChar == '"')
            {
                if (wordBegPos - 1 > -1 && line.charAt(wordBegPos - 1) == '"')
                {
                    str = '"' + str;
                    wordBegPos -= 2;
                    continue;
                }                    
                break;
            }
            
            /* Если встретился символ вертикальной черты, то значит нам 
            встретилась мультистрока. Добавляем перевод строки и продолжаем 
            парсинг с конца близлежайшей верхней строки. */
            if (curChar == '|')
            {
                str = "\n" + str;
                beginRow--;
                if (beginRow < 1) 
                    return null;
                    
                line = this.GetLine(beginRow);
                wordBegPos = line.length - 1;
                continue;
            }
                
            /* В остальных случаях встретился обычный символ, добавляем его к строке. */
            str = curChar + str;        
            wordBegPos--;
        }
            
        var endRow = pos.beginRow;
        var wordEndPos = pos.beginCol;
        var linesCount = this.LinesCount();
        var line = this.GetLine(endRow);
        
        // Регулярное выражение для проверки начала очередной строки в мультистроке.
        var reMultilineString = new RegExp('^\s*\|');
        
        while (true)
        {
            if (endRow > linesCount)
                return null;
                            
            if (wordEndPos >= line.length) 
            {
                /* Надо проверить, находимся ли мы в мультистроке.
                Критерий проверки: наличие вертикальной черты в самом начале следующей строки.
                Если да, мы в мультистроке, то просто переходим к обработке следующей строки.
                Если нет, то значит имеет место быть синтаксическая ошибка. */
                
                endRow++;
                if (endRow > linesCount)
                    return null;
                
                line = this.GetLine(endRow);
                
                // Если следующая строка - не продолжение мультстроки, то это ошибка.
                var match = reMultilineString.exec(line);
                if (!match) return null;

                // Добавляем перевод строки и продолжаем со следующей за вертикальной чертой позиции.
                str += "\n";
                wordEndPos = reMultilineString.lastIndex + 1;
            }
                            
            var curChar = line.charAt(wordEndPos);
            
            /* Если встретилась двойная кавычка, то либо строка завершилась, либо
            рядом есть еще одна экранирующая двойная кавычка. */
            if (curChar == '"')
            {
                if (wordEndPos + 1 < line.length && line.charAt(wordEndPos + 1) == '"')
                {
                    str += '"';
                    wordEndPos += 2;
                    continue;
                }                    
                break;
            }

            /* В остальных случаях встретился обычный символ, добавляем его к строке. */
            str += curChar;        
            wordEndPos++;        
        }

        return str;
    },

    // Возвращает отображение, соответствующее данному экземпляру объекта TextWindow.
    GetView : function () {
        if (this._view == undefined) {
            var view = null;
            var twnd = this.textWindow;
            function walkWindows(views) {
                for(var i = 0; i < views.count; i++) {
                    var v = views.item(i);
                    if (v.isContainer != vctNo)
                        v = walkWindows(v.enumChilds());                        
                    if (v && ((v.hwnd == twnd.hwnd) 
                    || (v.mdObj && v.mdObj == twnd.mdObj && v.mdProp == twnd.mdProp)))
                        return v;
                }
                return null;
            }
            this._view = walkWindows(windows.mdiView.enumChilds());
        }
        return this._view;
    }
    
}); // stdlib.Class.extend


//{ Русскоязычные аналоги основных методов объекта Текстовый документ (TextDocument).
_TextWindow.prototype.КоличествоСтрок = _TextWindow.prototype.LinesCount;
_TextWindow.prototype.УдалитьСтроку = _TextWindow.prototype.DeleteLine;
_TextWindow.prototype.ДобавитьСтроку = _TextWindow.prototype.AddLine;
_TextWindow.prototype.Очистить = _TextWindow.prototype.Clear;
_TextWindow.prototype.ВставитьСтроку = _TextWindow.prototype.InsertLine;
_TextWindow.prototype.ЗаменитьСтроку = _TextWindow.prototype.ReplaceLine; 
_TextWindow.prototype.ПолучитьТекст = _TextWindow.prototype.GetText;
_TextWindow.prototype.УстановитьТекст = _TextWindow.prototype.SetText;
//}

//{ Методы для обратной совместимости с интерфейсом ITextWindow Снегопата предыдущих версий.
_TextWindow.prototype.document = function () { return this; };
_TextWindow.prototype.Document = function () { return this; };
_TextWindow.prototype.extName = _TextWindow.prototype.ExtName;
_TextWindow.prototype.getCaretPos = _TextWindow.prototype.GetCaretPos;
_TextWindow.prototype.setCaretPos = _TextWindow.prototype.SetCaretPos;
_TextWindow.prototype.getSelection = _TextWindow.prototype.GetSelection;
_TextWindow.prototype.setSelection = _TextWindow.prototype.SetSelection;
_TextWindow.prototype.line = _TextWindow.prototype.GetLine;
_TextWindow.prototype.linesCount = _TextWindow.prototype.LinesCount;
_TextWindow.prototype.readOnly = _TextWindow.prototype.IsReadOnly;
_TextWindow.prototype.selectedText = _TextWindow.prototype.GetSelectedText;
_TextWindow.prototype.text = _TextWindow.prototype.GetText;
_TextWindow.prototype.hwnd = _TextWindow.prototype.GetHwnd;
//}

//{ Объект TextWindow - альтернатива использованию GetTextWindow().
// Вместо 
// 	var twnd = GetTextWindow();
// 	if (twnd !== null) ...
// Позволяет использовать объектную технику:
// 	var twnd = new TextWindow();
// 	if (twnd.IsActive()) ...
TextWindow = _TextWindow.extend({
    construct : function () {
        var activeWnd = snegopat.activeTextWindow();	    
        if (activeWnd)
            this._super(activeWnd, windows.getActiveView());	        
    }	
});
//}

////////////////////////////////////////////////////////////////////////////////////////
//// StringUtils
////

//{ Вспомогательные методы для работы со строками и текстовыми блоками.
StringUtils = {
    
    /* Получить отступ блока текста (по первой строке блока).
     Возвращает строку - пробельные символы, формирующие отступ. */
    getIndent: function (code) {
        var matches = code.match(/(^\s+?)(\S|\n|\r)/);
        
        if (matches)
            return matches[1].replace(/\n|\r/, '');
            
        return '';
    },

    /* Увеличивает отступ у текстового блока, добавляя строку пробельных символов,
    переданных в качестве значения второго параметра ind. 
    Возвращает текстовый блок с добавленным отступом. */
    shiftRight: function(code, ind) {
        if (ind)
            return ind + code.replace(/\n/gm, "\n" + ind);
            
        return code;
    },

    /* Уменьшает отступ у текстового блока, удаляя строку пробельных символов,
    совпадающую со строкой, переданной в качестве значения второго параметра ind.
    Возвращает текстовый блок с уменьшенным отступом. */
    shiftLeft: function(code, ind) {
        if (ind)
        {
            var re = new RegExp("^" + ind, 'gm');
            return code.replace(re, "");
        }
            
        return code;
    },

    /* Проверяет, оканчивается ли строка str подстрокой suffix.
    Возвращает true, если хвост строки совпадает с suffix, и false в противном случае. */ 
    endsWith: function(str, suffix)  {
        return str.indexOf(suffix, str.length - suffix.length) !== -1;
    },
    
    /* Разбивает переданный блок текста на строки и возвращает массив строк. */
    toLines: function(code, nl) {
        return code.split(nl ? nl : "\n");
    },
    
    /* Объединяет массив строк в строку - блок текста. */
    fromLines: function(linesArray, nl) {
        return linesArray.join(nl ? nl : "\n");
    },
    
    /* Экранирует все символы в строке. */
    addSlashes: function(str) {
        return str.replace(/([^\d\w\sА-я])/g, "\\$1");
    }

}
//} Вспомогательные методы для работы со строками и текстовыми блоками.